mjb.js

https://metjebank.nl/mjb.js TypeScript definitions Brand assets

Live demo

Default button
With bank: 'ING'
Handle passed to the SDK
Events

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

  1. The button renders inside a small MetJeBank iframe, so it can show the payer's remembered bank without any cookies on your site.
  2. The payer clicks the button. The iframe opens a MetJeBank popup and the SDK emits opened.
  3. The SDK calls your createPayment. Your backend creates the payment with the API and you return the paymentUrl (or the paymentId).
  4. 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.
  5. 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
outletelement | selectorRequired. The container the button renders into.
createPaymentasync () => handleCalled on click; return the paymentUrl or the paymentId.
bankstringBank to show, e.g. 'ING'. Overrides the remembered bank and pre-selects it on the pay page.
emailstringPayer email. Hashed in the browser (SHA-256) before it leaves your page; keys the remembered bank.
phonestringPayer phone number, E.164 preferred. Hashed like email.
locale'nl' | 'en' | 'de' | 'fr' | 'es'Button label and popup language. Defaults to the browser language.
labelstringOverrides the button text.
theme{ color, textColor, radius }Button styling: hex colors and a simple radius (px, rem, em or %).

Events

Event Payload When
openedThe popup opened (or the redirect is about to happen). Manual flow: call submit now.
blockedThe 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.
closedThe 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
paidtrueSettled successfully.
cancelledfalseCancelled by the payer or declined at the bank.
expiredfalseNot completed in time.
failedfalseTechnical failure.
cancel_reversed / expired_reversed / double_paymenttrueMoney 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.