Supporting iDEAL and Delayed-Notification Payment Methods

Redirect methods send the customer away from your checkout to their bank, where they approve a single transfer, and return them with a query parameter that may or may not reflect what actually happened. They are the dominant way consumers pay in several European markets, they convert extremely well, and they authorise exactly one payment — which makes them a poor fit for subscriptions until paired with something that authorises the next one. This guide sits under Alternative Payment Methods & Local Rails and covers both halves of that problem.

The single most common defect in these integrations is trusting the browser’s return. A customer approves the payment, closes the tab before the redirect fires, and your system records an abandoned checkout for a payment that succeeded. Or the redirect arrives with a success parameter while the payment is still pending at the bank. The browser is a hint; the webhook is the fact.

Trade-offs

Method Recurring capable Confirmation Reversal risk Notes
iDEAL Only via a paired mandate Seconds to minutes Effectively none — push payment Dominant in the Netherlands
Bancontact Only via a paired mandate Seconds Very low Dominant in Belgium
Sofort / online banking No Minutes to hours Low but non-zero Delayed notification
Card Native Seconds Chargebacks The comparison baseline
Redirect flow and confirmation sources The customer is redirected to their bank and returns to the site, while the authoritative outcome arrives separately by webhook and may precede or follow the browser. Checkout intent created Bank approval customer leaves Browser return a hint only Webhook authoritative Order state either can arrive first · only one of them decides
Two inbound paths, one authority — the browser return drives the page the customer sees, never the state of the order.

The reversal column explains why these methods are attractive despite the recurring limitation. A push payment initiated by the customer inside their own banking app carries almost no dispute risk, which makes them a good first charge for higher-value plans even when the ongoing rail is something else.

Step-by-Step Implementation

1. Create the intent before redirecting

The payment object must exist, with your own reference attached, before the customer leaves. If they never return, you still have a record to reconcile against.

const intent = await provider.createPayment({
  amountMinor: plan.firstChargeMinor,
  currency: "EUR",
  method: "ideal",
  metadata: { checkoutId, accountId, planKey: plan.key },
  returnUrl: `${BASE}/checkout/return?checkout_id=${checkoutId}`,
});
await checkouts.markRedirecting(checkoutId, intent.id);
return redirect(intent.redirectUrl);

Storing checkoutId in metadata rather than relying solely on the return URL is what makes webhook-first confirmation possible: the event carries enough context to complete the order without any browser involvement.

2. Treat the return as a hint

// GET /checkout/return — renders a page, decides nothing.
export async function handleReturn(req, res) {
  const checkout = await checkouts.get(req.query.checkout_id);
  const state = await checkouts.currentState(checkout.id);   // set by the webhook
  if (state === "paid") return res.render("success", { checkout });
  if (state === "failed") return res.render("failed", { checkout });
  return res.render("pending", { checkout, pollUrl: `/checkout/${checkout.id}/state` });
}

The pending page is not a fallback; it is the normal outcome for a meaningful fraction of sessions, because the webhook and the browser race. Render something honest — “we’re confirming your payment” with a small poll — rather than optimistically showing success and correcting it later.

3. Confirm from the webhook, reconcile by polling

Confirmation and reconciliation The webhook confirms the payment normally, and a scheduled reconciler polls any intent left in a pending state past a timeout. Webhook arrives normal path Still pending at T+15m reconciler polls Idempotent apply same handler, one effect Order completed or expired
Both paths converge on one idempotent handler, so a webhook and a poll for the same payment produce exactly one order.

The reconciler is not optional. Webhook delivery fails occasionally, and a payment the customer made and you never recorded is the worst possible outcome — they have been charged, they have no subscription, and only they know about it.

4. Pair the first payment with a mandate

For a subscription, the redirect payment covers the first cycle and a mandate covers the rest. Most providers expose this as one checkout session producing two objects; treat them as two objects in your model, because they fail independently.

def complete_subscription_checkout(session) -> None:
    payment, mandate = session.payment, session.mandate
    if payment.status != "paid":
        return                       # nothing to do yet
    if mandate is None or mandate.status not in ("pending", "active"):
        # Paid, but no way to bill the next cycle — recover this explicitly.
        subscriptions.create_awaiting_mandate(session.account_id, session.plan_key)
        notifications.enqueue(session.account_id, "mandate_required")
        return
    subscriptions.create_active(session.account_id, session.plan_key, mandate_id=mandate.id)

The awaiting-mandate branch is the one teams forget. A customer who paid but whose mandate did not complete is a paying customer with no renewal path, and the only signal is a renewal that silently fails a month later.

5. Expire abandoned redirects

Give redirect sessions a bounded lifetime — fifteen to thirty minutes is typical — after which the intent is cancelled and the checkout marked abandoned. Without expiry, pending checkouts accumulate indefinitely and the reconciler’s queue grows without bound. Cancel through the provider rather than only locally, so a customer who returns to a stale bank page cannot complete a payment your system has already given up on.

Delayed-Notification Methods Are Different Again

Some redirect methods confirm within seconds; others may take hours, and a few can fail after appearing to succeed. Treating them as one category is where integrations break.

Model the expected confirmation window per method rather than globally. A method that normally confirms in ten seconds should have its reconciler timeout at minutes; a method that can take hours needs a timeout measured in hours and a customer-facing message that sets the same expectation. Using one global timeout either abandons slow payments prematurely or leaves fast failures pending far too long.

Decide provisioning per method too. For an effectively instant push payment, provisioning on confirmation is simply correct. For a method that can take hours, waiting means a customer who paid at 22:00 cannot use the product until morning — so many products provision optimistically and accept a small risk, exactly as they do for bank debit rails.

Watch for methods where a later reversal is possible. Where it is, the payment’s terminal state is not reached at confirmation, and the same “provisional then confirmed” split used for direct debit applies. The practical rule is to look up each method’s reversal rights before enabling it, and to record them as configuration next to the timeout rather than as knowledge in someone’s head.

Per-method configuration Each redirect method carries its own confirmation timeout, provisioning policy, and reversal rights rather than sharing one global setting. Instant push timeout: minutes provision on confirm reversal: effectively none Delayed notification timeout: hours provision optimistically reversal: low, non-zero Mandate-backed timeout: days provisional then confirm reversal: weeks
Three columns of configuration per method — a single global timeout serves none of them correctly.

Keeping these as configuration rows rather than branches in code has a practical benefit beyond tidiness: enabling a method in a new market becomes a data change that a payments owner can review, and the reconciler, the customer-facing copy, and the provisioning decision all read from the same source. When they are separate constants scattered across three services, a method added on Monday is confirmed with one timeout, displayed with another, and provisioned by a rule nobody updated.

Finally, be careful with refunds on push methods. Because the customer initiated the transfer, the refund is a separate outbound payment that requires their account details, which the provider may or may not expose. Confirm the refund path exists before enabling the method for a product where refunds are routine — discovering that a method cannot be refunded programmatically during a support incident is an unpleasant way to learn it.

Verification & Testing

Test the race explicitly. Simulate the webhook arriving before the browser return, after it, and not at all, and assert the same terminal state in all three cases. The third scenario should be resolved by the reconciler within its window, and the test should assert that rather than assuming it.

Test the abandoned path: redirect, never return, never send a webhook, and assert the checkout is expired and the intent cancelled at the provider after the timeout. Then test the awkward variant where the customer completes the payment after your expiry has fired, and assert the resulting webhook is handled — refunding or fulfilling deliberately rather than crashing on an unknown checkout.

Test the mandate hand-off both ways: payment succeeds with a mandate, and payment succeeds without one. The second must produce a subscription in an explicit awaiting-mandate state with a notification, not an active subscription that cannot renew.

Gotchas & Production Pitfalls

  • Trusting the return URL. A success parameter in a query string is not proof of payment and can be forged. Read state from your own store, populated by the webhook.
  • No pending state in the UI. Showing success optimistically and correcting it later is worse than showing an honest confirming state for a few seconds.
  • Mandate treated as guaranteed. The payment and the mandate can succeed independently; handle the paid-without-mandate case explicitly.
  • Reconciler missing. Webhook delivery is reliable, not perfect. A polling backstop for pending intents is what prevents an unrecorded payment.
  • Global expiry for all methods. A timeout tuned for an instant method will abandon a slow one that was going to succeed.

Frequently Asked Questions

Can iDEAL be used for the recurring charges directly? No. It authorises a single transfer. Recurring billing needs a mandate collected alongside it, after which the renewals run as SEPA debits.

What should the customer see while a payment is confirming? A clear pending state with an automatic poll and an explanation that they can close the page safely. Most confirmations resolve within seconds, and the honest message costs nothing when they do.

How long should the reconciler keep polling? Until the provider reports a terminal state or the intent’s own expiry passes, whichever comes first. Polling indefinitely on an intent the provider has expired just consumes rate limit.

Do these methods reduce disputes? Substantially, because the customer authenticated inside their own banking application. That is a genuine advantage for higher-value first charges even where the ongoing rail is a card.