Accept local payment methods
Accept local payment methods — a runnable, end-to-end guide verified against the VINR sandbox.
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:
| Method | Type | Markets | Currency | Notes |
|---|---|---|---|---|
ideal | Redirect | NL | EUR | Single-use, instant confirmation |
bancontact | Redirect | BE | EUR | Can generate a reusable mandate |
sepa_debit | Pull | EUR SEPA zone | EUR | Mandate required; asynchronous |
sofort | Redirect | DE, AT | EUR | Delayed confirmation (hours possible) |
p24 | Redirect | PL | EUR, PLN | Bank 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 unitsReconcile 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:
| Method | Sandbox action | Result |
|---|---|---|
ideal | Choose "Authorize" | payment.completed |
ideal | Choose "Cancel" | payment.expired |
sepa_debit | Submit any IBAN starting NL | payment.completed, then settles |
sepa_debit | IBAN starting DE00 | payment.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
Accept a one-time payment
The full create-checkout-confirm flow.
Local methods reference
Every method, its markets, and behavior.
Reconcile settlements
Match payouts to payments and fees.
Last updated on