npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bancontact-pro

v0.1.1

Published

Unofficial Node.js SDK for the Bancontact Pro (Payconiq) Merchant Payment API v3 — payments and webhook signature verification.

Downloads

18

Readme

bancontact-pro

Unofficial Node.js SDK for the Bancontact Pro (Payconiq) Merchant Payment API v3. Create payments and verify webhook signatures, with full TypeScript types and zero runtime dependencies.

Not affiliated with or endorsed by Bancontact Payments NV/SA. "Bancontact" is a trademark of its owner.

Features

  • Payments — create, fetch by id, and search by reference.
  • Webhook verification — verify inbound callback signatures (detached JWS, ES256) using only Node's built-in crypto.
  • Typed — every request and response is typed; helpers for the payment status model.
  • Safe defaults — talks to preprod unless you explicitly ask for prod.
  • Dual build — works with both import (ESM) and require (CommonJS). No runtime dependencies.

Requires Node 18+ (uses the global fetch).

Why Bancontact Pro?

Bancontact is Belgium's most-used payment method — the default way people pay online and in shops. If you sell to Belgian customers, you need to accept it.

Bancontact Pro (the Payconiq-operated merchant API) bills a low flat fee per transaction rather than a percentage. On the Integrated product that fee is on the order of ~€0.06 per payment (contract-dependent) — far below what a general PSP charges to route Bancontact:

| Provider | Bancontact fee per transaction (indicative, 2026) | | --- | --- | | Bancontact Pro (direct) | ~€0.06 flat (depends on your contract/volume) | | Stripe | €0.35 flat (+2% on currency conversion) | | Mollie | €0.39 flat |

On a €10 sale that's roughly 0.6% vs 3.5–3.9% — about 6× cheaper, and the gap widens on smaller tickets. Figures are indicative and current as of 2026; always confirm live pricing with each provider (Bancontact Pro pricing is contract-dependent).

Install

npm install bancontact-pro

Getting an API key

  1. Get a Bancontact Pro merchant account. Apply via bancontactpro.com or your Bancontact contact. A registered business is required (Belgian merchant); onboarding includes KYC.
  2. In the merchant portal, open Stores and create a store.
  3. In that store, create a Display token (the Integrated "Display" product).
  4. Copy the API key it generates — that's your Bearer key. Pass it to the client as apiKey.

The key from the portal is a live key, so use environment: "prod":

const bc = new BancontactPro({
  apiKey: process.env.BANCONTACT_API_KEY!,
  environment: "prod",
});

The SDK defaults to "preprod" purely as a safety net — you don't pick an environment in the portal. preprod is a separate Bancontact sandbox (different credentials), not a toggle on your live key.

Portal labels can change — see the official Bancontact Pro docs or contact [email protected].

Quick start

import { BancontactPro } from "bancontact-pro";

const bc = new BancontactPro({
  apiKey: process.env.BANCONTACT_API_KEY!,
  environment: "prod", // "prod" | "preprod" (default: "preprod")
});

// Create a payment and redirect the payer to the checkout URL.
const payment = await bc.payments.create({
  amountCents: 1250, // €12.50 (integer cents)
  reference: "order-123", // your order id (max 35 chars, sanitized)
  description: "2x Cola",
  returnUrl: "https://shop.example/return?ref=order-123",
  callbackUrl: "https://shop.example/api/bancontact-webhook", // optional
});

const checkoutUrl = payment._links.checkout?.href;

When the payer returns, confirm the real outcome server-side — never trust the redirect alone:

import { isSuccessful } from "bancontact-pro";

const p = await bc.payments.get(payment.paymentId);
if (isSuccessful(p.status)) {
  // SUCCEEDED — funds guaranteed. Fulfil the order.
}

Didn't persist the paymentId? Look it up by your own reference:

const p = await bc.payments.findByReference("order-123");

Payment status model

The API reports ten states. Two helpers cover the common questions:

import { isFinal, isSuccessful } from "bancontact-pro";

isSuccessful("SUCCEEDED"); // true — the ONLY status that guarantees funds
isSuccessful("AUTHORIZED"); // false — intermediary; funds not yet guaranteed
isFinal("CANCELLED"); // true — terminal, will not change
isFinal("PENDING"); // false

Per the Bancontact docs, AUTHORIZED is intermediary — only SUCCEEDED guarantees the payment completed and funds were received. Don't ship goods on AUTHORIZED.

Final states: SUCCEEDED, AUTHORIZATION_FAILED, FAILED, CANCELLED, EXPIRED, VOIDED.

Verifying webhooks

Bancontact signs each callback with a detached JWS in the signature header. Pass the raw request body (not a re-serialized object) so the signature matches:

import express, { type Request, type Response } from "express";
import { BancontactPro } from "bancontact-pro";

const app = express();
const bc = new BancontactPro({ apiKey: process.env.BANCONTACT_API_KEY!, environment: "prod" });

// Use a raw body parser on this route so the signature matches the exact bytes.
app.post("/api/bancontact-webhook", express.raw({ type: "*/*" }), async (req: Request, res: Response) => {
  try {
    const event = await bc.callbacks.verify({
      rawBody: (req.body as Buffer).toString("utf8"),
      signature: req.header("signature") ?? "",
    });
    // event is the verified JSON payload, e.g. { paymentId, status, reference, ... }
    res.sendStatus(200);
  } catch {
    res.sendStatus(400); // signature invalid — ignore
  }
});

verify() fetches and caches the signing keys (JWKS) automatically. You can also supply them yourself or override the URL:

await bc.callbacks.verify({
  rawBody,
  signature,
  jwksUrl: "https://jwks.bancontact.net/.well-known/jwks.json", // override if your account differs
  // or: jwks: { keys: [ /* JWK */ ] }
  maxKeyAgeMs: 3_600_000, // cache TTL (default 1h)
});

The JWKS host is documented (jwks.bancontact.net / jwks.preprod.bancontact.net); the exact path is not, so it defaults to /.well-known/jwks.json. If your account serves it elsewhere, pass jwksUrl.

API

new BancontactPro(options)

| Option | Type | Default | Notes | | --- | --- | --- | --- | | apiKey | string | — | Required. Bearer API key for your payment profile. | | environment | "prod" \| "preprod" | "preprod" | Selects the API + JWKS host. | | baseUrl | string | — | Override the API base URL. | | fetch | typeof fetch | global | Inject a custom fetch (tests, proxy). | | timeoutMs | number | 15000 | Per-request timeout. |

bc.payments

  • create(input)PaymentPOST /v3/payments.
  • get(paymentId)PaymentGET /v3/payments/{id} (strongly consistent).
  • search(reference)Payment[]POST /v3/payments/search.
  • findByReference(reference)Payment | undefined — most recent match.

bc.callbacks

  • verify(input) → parsed payload — throws SignatureVerificationError if invalid.

Also exported

sepaSafe(input, maxLength?), isFinal(status), isSuccessful(status), BancontactError, SignatureVerificationError, and all types (Payment, PaymentStatus, CreatePaymentInput, ClientOptions, …).

Errors

Non-2xx responses throw BancontactError with httpStatus, apiCode (when present), and the parsed body.

Scope

This is v0.1 — Payments + webhook verification. The Refund and Reconciliation APIs require outbound ES256 request signing (a scheme not yet fully documented publicly) and are planned for a later release.

License

MIT © Koray Sels