Accept local payment methods

Accept local payment methods — a runnable, end-to-end guide verified against the VINR sandbox.

View as MarkdownInstall skills

Cards are not how most of Europe pays. iDEAL dominates the Netherlands, Bancontact owns Belgium, and SEPA Direct Debit underpins recurring billing across the eurozone. This guide shows how to present these local methods, handle their bank-redirect flows, and reconcile settlement — runnable against the VINR sandbox.

OverviewAsk

Local methods split into two families that behave differently after the customer pays:

  • Redirect methods (iDEAL, Bancontact, online banking) — the customer is sent to their bank, authorizes, and returns. Confirmation can be near-instant or take seconds.
  • Pull / debit methods (SEPA Direct Debit) — you collect a mandate up front, then debit the account. Settlement is asynchronous and can fail days later via a return.
your server          VINR                customer / bank
    │  create payment  │                       │
    │─────────────────►│                       │
    │  checkoutUrl     │                       │
    │◄─────────────────│   redirect to bank    │
    │──────────────────────────────────────────►│ authorizes
    │  payment.completed | payment.failed (webhook)
    │◄─────────────────│◄──────────────────────│

You never hard-code a method list. Create the payment with the customer's country, and the hosted page renders whatever is eligible.

Method availability by regionAsk

Eligibility depends on currency, the customer's country, and the amount. VINR filters automatically, but it helps to know the matrix:

MethodTypeMarketsCurrencyNotes
idealRedirectNLEURSingle-use, instant confirmation
bancontactRedirectBEEURCan generate a reusable mandate
sepa_debitPullEUR SEPA zoneEURMandate required; asynchronous
sofortRedirectDE, ATEURDelayed confirmation (hours possible)
p24RedirectPLEUR, PLNBank selector on hosted page

Don't maintain this table in your code. Call vinr.paymentMethods.list({ country, currency, amount }) to get the live, eligible set for a given context — the result already reflects your account configuration and any compliance gating.

Presenting local methodsAsk

Create the payment with the buyer context. Passing paymentMethods: ['auto'] (the default) lets VINR pick the eligible set; pass an explicit array to constrain it.

import { Vinr } from '@vinr/sdk';

const vinr = new Vinr({ secretKey: process.env.VINR_SECRET_KEY });

export async function POST(req: Request) {
  const { orderId, country } = await req.json();

  const payment = await vinr.payments.create(
    {
      amount: 2500,                 // €25.00
      currency: 'EUR',
      description: `Order ${orderId}`,
      customer: { country },        // e.g. 'NL' surfaces iDEAL
      paymentMethods: ['auto'],     // or ['ideal', 'bancontact', 'sepa_debit']
      returnUrl: `https://yoursite.com/orders/${orderId}/return`,
      metadata: { orderId },
    },
    { idempotencyKey: `order-${orderId}` },
  );

  return Response.json({ checkoutUrl: payment.checkoutUrl });
}

Redirect the customer to payment.checkoutUrl. The hosted page shows the bank selector (iDEAL, P24) or single-bank redirect (Bancontact) and routes them to their bank.

Handling redirectsAsk

Local methods route through the customer's bank, so two states matter that you rarely see with cards: pending (bank is still confirming) and expired (the customer abandoned the redirect).

When the customer lands on your returnUrl, confirm server-side — never trust the redirect parameters alone.

const payment = await vinr.payments.retrieve(paymentId);

switch (payment.status) {
  case 'completed':
    // safe to show success; fulfil on the webhook
    break;
  case 'pending':
    // bank hasn't confirmed yet — show a "processing" state
    break;
  case 'failed':
  case 'expired':
    // offer the customer another method
    break;
}

Because redirect methods can confirm after the browser returns, treat the webhook as the source of truth and fulfil there exactly once.

export async function POST(req: Request) {
  const event = vinr.webhooks.verify(
    await req.text(),
    req.headers.get('x-vinr-signature'),
  );

  switch (event.type) {
    case 'payment.completed':
      await fulfillOrder(event.data.metadata.orderId);   // idempotent
      break;
    case 'payment.failed':
    case 'payment.expired':
      await releaseReservation(event.data.metadata.orderId);
      break;
  }
  return new Response('OK', { status: 200 });
}

SEPA Direct Debit can succeed and then return (insufficient funds, mandate revoked) days later. Listen for payment.refunded with reason: 'debit_returned' and reverse fulfilment, claw back loyalty points, or re-bill.

Settlement currencyAsk

Local methods always settle in their native currency — EUR for iDEAL, Bancontact, and SEPA. If your payout currency differs, VINR converts at settlement and records the rate on the settlement object.

const payment = await vinr.payments.retrieve(paymentId);

console.log(payment.amount);              // 2500  (presented to buyer, EUR)
console.log(payment.settlement.currency); // 'EUR'
console.log(payment.settlement.amount);   // net after fees, minor units

Reconcile against settlement.id (prefix setl_), not the payment amount — fees and FX mean the deposited amount differs from what the customer paid.

Test itAsk

In the sandbox, the hosted page renders a simulator for each method instead of a real bank screen:

MethodSandbox actionResult
idealChoose "Authorize"payment.completed
idealChoose "Cancel"payment.expired
sepa_debitSubmit any IBAN starting NLpayment.completed, then settles
sepa_debitIBAN starting DE00payment.refunded (debit_returned)

Go liveAsk

Enable methods on your account

Activate iDEAL, Bancontact, or SEPA in the Dashboard. Some methods require business verification before they leave test mode.

Swap to live keys

Replace your sandbox VINR_SECRET_KEY with the live key from Authentication.

Walk the go-live checklist

Confirm webhook handling for pending, expired, and debit_returned against the go-live checklist.

Next stepsAsk

Was this page helpful?
Edit on GitHub

Last updated on

On this page