mjb.js
Live demo
bank: 'ING'Quick start
Render a button and give it an async createPayment that returns the paymentUrl from your backend.
<div id="pay-button"></div>
<script src="https://metjebank.nl/mjb.js"></script>
<script>
const button = mjb.button({
outlet: '#pay-button',
createPayment: async () => {
// Note this is your backend :)
const response = await fetch('/checkout/create-payment', { method: 'POST' });
return (await response.json()).paymentUrl;
}
});
button.on('completed', (event) => {
if (event.success) {
location.href = '/thanks';
}
});
</script>
Your backend endpoint creates the payment with the MetJeBank API and returns the response to your frontend:
POST https://metjebank.nl/api/v1/payments
X-Api-Key: your-api-key
X-Request-ID: 61c7d9e4-6f3a-4c8e-9f21-8a4b5c6d7e8f
{ "redirectUrl": "https://shop.example/return", "creditorIban": "NL91ABNA0417164300",
"creditorName": "Your Shop B.V.", "amount": "12.50", "currency": "EUR",
"description": "Order 8472" }
201 Created
{ "paymentId": "01890a5c-…", "status": "open",
"paymentUrl": "https://metjebank.nl/pay/9f8e7d6c-…", "remittance": "…" }
How it works
- The button renders inside a small MetJeBank iframe, so it can show the payer's remembered bank without any cookies on your site.
- The payer clicks the button. The iframe opens a MetJeBank popup and the SDK emits
opened. - The SDK calls your
createPayment. Your backend creates the payment with the API and you return thepaymentUrl(or thepaymentId). - The popup shows the hosted pay page: the payer's remembered or passed bank pre-selected, otherwise the bank picker. Bank authentication happens inside the popup.
- When the payment reaches a final state the SDK emits
completed. On success the popup closes by itself; on failure it shows the regular cancelled or expired screen.
Handing over the payment
Prefer createPayment. If you need full control, listen for opened and call submit yourself:
const button = mjb.button({ outlet: '#pay-button' });
button.on('opened', async () => {
// Note this is your backend :)
const response = await fetch('/checkout/create-payment', { method: 'POST' });
button.submit((await response.json()).paymentUrl);
});
Both forms accept either handle. The paymentUrl is the preferred handle; a bare paymentId is resolved server-side and only works while the payment is still open or pending.
Pay again
Give the button the payer's email and/or phone. The SDK hashes them in the browser before anything leaves your page — MetJeBank only ever sees the hash — and remembers which bank that payer last completed a payment with, across shops and devices. The button becomes "Pay again with ING" and the pay page pre-selects that bank.
mjb.button({
outlet: '#pay-button',
email: customer.email,
phone: customer.phone,
createPayment
});
Initiating server-side? Pass payerEmail / payerPhone on POST /api/v1/payments and the hosted pay page pre-selects the remembered bank there too. Phone numbers are matched on digits only — send E.164 (+31612345678) when you can. Pass bank to override the remembered bank.
Options
| Option | Type | Description |
|---|---|---|
| outlet | element | selector | Required. The container the button renders into. |
| createPayment | async () => handle | Called on click; return the paymentUrl or the paymentId. |
| bank | string | Bank to show, e.g. 'ING'. Overrides the remembered bank and pre-selects it on the pay page. |
| string | Payer email. Hashed in the browser (SHA-256) before it leaves your page; keys the remembered bank. | |
| phone | string | Payer phone number, E.164 preferred. Hashed like email. |
| locale | 'nl' | 'en' | 'de' | 'fr' | 'es' | Button label and popup language. Defaults to the browser language. |
| label | string | Overrides the button text. |
| theme | { color, textColor, radius } | Button styling: hex colors and a simple radius (px, rem, em or %). |
Events
| Event | Payload | When |
|---|---|---|
| opened | — | The popup opened (or the redirect is about to happen). Manual flow: call submit now. |
| blocked | — | The popup was blocked; the SDK falls back to a full-page redirect. |
| status | { status, paymentId, bank } | The pay page reported a non-final state. |
| completed | { status, paymentId, bank, success } | The payment reached a final state. Emitted once per attempt. |
| closed | — | The payer closed the popup before completion. |
| error | { reason } | create-failed, invalid-handle, resolve-failed or timeout. |
Treat the events as UX signals. The source of truth for order fulfilment is your backend: the signed webhooks and GET /api/v1/payments/{paymentId}.
Instance methods
| on(event, fn) / off(event, fn) | Subscribe to or unsubscribe from an event. Chainable. |
| submit(handle) | Hands the payment to the SDK in the manual flow. |
| update({ bank, email, phone, locale, label, theme }) | Re-renders the button with new settings. |
| destroy() | Removes the button and all listeners. |
Statuses
| status | success | Meaning |
|---|---|---|
| paid | true | Settled successfully. |
| cancelled | false | Cancelled by the payer or declined at the bank. |
| expired | false | Not completed in time. |
| failed | false | Technical failure. |
| cancel_reversed / expired_reversed / double_payment | true | Money moved after all; reconcile via webhooks. |
Mobile
On touch devices the SDK never opens a popup: after opened it redirects the page to the hosted pay flow. No completion events fire on mobile — the payer returns via your redirectUrl and your backend hears about the outcome through webhooks.
Versioning
/mjb.js is evergreen and changes are additive. The loaded version is available as mjb.version.