Completion and fulfillment
Handle browser returns, polling, statuses, and webhooks without duplicate fulfillment.
Redirect checkout produces three different signals. They are useful for different purposes:
| Signal | Use it for | Payment authority |
|---|---|---|
| Browser return | Restore your UI and show progress | No |
| Authenticated status response | Read the current deposit state | Yes |
| Verified webhook | Process asynchronous state changes | Yes |
Handle the browser return
The payer returns to the redirect_url supplied in your create request. The
return means only that navigation reached your application; it does not prove
that payment succeeded.
Your return page should:
- identify the order from server-side session state or a safe opaque value;
- show a processing state;
- ask your backend for the current deposit status;
- display success only when your backend reports
succeeded.
Do not accept a client-supplied status, amount, or transaction ID as proof.
Interpret statuses
| Status | Terminal? | Your action |
|---|---|---|
pending | No | Continue waiting |
processing | No | Continue waiting |
succeeded | Yes | Fulfill exactly once |
failed | Yes | Show failure and stop polling |
expired | Yes | Offer a new checkout with a new merchant_id |
cancelled | Yes | Show cancellation |
rejected | Yes | Show failure and follow your support workflow |
refunded | Yes | Apply your refund workflow |
Poll with a limit
Webhooks are preferred for backend processing, but short-lived polling improves the payer experience after a browser return.
const terminal = new Set([
"succeeded",
"failed",
"expired",
"cancelled",
"rejected",
"refunded",
]);
for (let attempt = 0; attempt < 20; attempt += 1) {
const deposit = await getDepositFromYourBackend(orderId);
if (terminal.has(deposit.status)) {
renderResult(deposit.status);
break;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
}Keep API keys in your backend. The browser should call your authenticated application endpoint, not OmniPay directly.
Fulfill from webhooks
Subscribe to deposit events and verify the Svix signature against the exact raw request body. Fulfillment should be idempotent because a webhook may be retried or replayed.
For a successful order:
- verify the webhook signature;
- reject or ignore duplicate
svix-idvalues; - locate the order by
merchant_id; - cross-check the OmniPay
idand amount; - transition the order only if it has not already been fulfilled;
- commit the event record and business transition together.
Configure and verify webhooks →
Production checklist
- Persist
merchant_idand the OmniPay depositid. - Record
X-Request-Idfrom API responses. - Treat browser returns as navigation only.
- Handle every documented terminal status.
- Use bounded polling with a delay.
- Verify webhooks before parsing and make fulfillment idempotent.
- Test payer cancellation, timeout, webhook retry, and duplicate delivery.