Webhooks
Configure, verify, acknowledge, and replay OmniPay event delivery through Svix.
Webhooks are the authoritative asynchronous signal for deposit and withdrawal changes. OmniPay delivers them through Svix to endpoints configured in the developer webhook portal.
Common envelope
Every delivery uses the same outer structure:
{
"eventType": "<event-name>",
"payload": {
"data": {
"id": "<OmniPay-resource-id>"
},
"type": "<event-name>"
},
"tags": ["<OmniPay-resource-id>"]
}eventType and payload.type contain the same event name. The resource data is
in payload.data, and the first tags value is its OmniPay id.
Subscribe only to events your integration handles. New event types and payload fields may be added, so ignore unknown values safely.
Deposit webhooks
| Event | payload.data.status | When it is emitted |
|---|---|---|
deposit.created | pending | Deposit creation committed |
deposit.succeeded | succeeded | Payment completed successfully |
deposit.failed | failed | Payment or submitted verification failed |
deposit.expired | expired | No valid completion before the deadline |
deposit.cancelled | cancelled | Checkout was cancelled |
deposit.refunded | refunded | Funds were refunded |
Redirect deposit example
{
"eventType": "deposit.succeeded",
"payload": {
"data": {
"id": "cm7deposit01",
"amount": "1000",
"payment_method": "bkash",
"status": "succeeded",
"metadata": {
"invoice_id": "INV-124"
},
"merchant_id": "order-124"
},
"type": "deposit.succeeded"
},
"tags": ["cm7deposit01"]
}H2H deposit example
{
"eventType": "deposit.succeeded",
"payload": {
"data": {
"id": "cm7deposit02",
"amount": "500.00",
"payment_method": "bkash_merchant",
"status": "succeeded",
"metadata": {
"invoice_id": "INV-123"
},
"merchant_id": "order-123",
"checkout_mode": "h2h",
"provider_transaction_id": "ABC123DEF4"
},
"type": "deposit.succeeded"
},
"tags": ["cm7deposit02"]
}All deposit payloads include id, amount, payment_method, status,
metadata, and merchant_id. H2H deposit payloads additionally include
checkout_mode: "h2h" and provider_transaction_id. The transaction ID is
null until a successful one is known.
Withdrawal webhooks
| Event | payload.data.status | When it is emitted |
|---|---|---|
withdraw.created | processing | Withdrawal accepted for processing |
withdraw.succeeded | succeeded | Payout completed |
withdraw.failed | failed | Payout failed |
Successful withdrawal example
{
"eventType": "withdraw.succeeded",
"payload": {
"data": {
"id": "cm7withdraw01",
"amount": "1000.00",
"payment_method": "bkash",
"status": "succeeded",
"metadata": {
"beneficiary_id": "beneficiary-42"
},
"merchant_id": "payout-123"
},
"type": "withdraw.succeeded"
},
"tags": ["cm7withdraw01"]
}Withdrawal payloads include id, amount, payment_method, status,
metadata, and merchant_id. Persist recipient details when creating the
withdrawal; webhook payloads do not repeat them.
Dispatch events
Branch on eventType only after verifying the webhook signature:
switch (event.eventType) {
case "deposit.succeeded":
await fulfillOrder(event.payload.data);
break;
case "withdraw.succeeded":
await completePayout(event.payload.data);
break;
case "deposit.failed":
case "deposit.expired":
case "deposit.cancelled":
case "withdraw.failed":
await recordTerminalFailure(event.eventType, event.payload.data);
break;
default:
// Acknowledge event types this version of the integration does not handle.
break;
}Configure an endpoint
Open the developer webhook portal from the OmniPay dashboard, add an HTTPS URL you control, choose event types, and copy the endpoint signing secret. Use a dedicated path such as:
https://merchant.example/webhooks/omnipayDisable browser-oriented CSRF enforcement for this machine endpoint. Continue requiring HTTPS and signature verification.
Verify before parsing
Svix signs the exact request body and sends svix-id, svix-timestamp, and
svix-signature. Read the raw bytes first. Re-serializing parsed JSON changes
the signed content and causes verification failure.
import { Webhook } from "svix";
export async function POST(request: Request) {
const rawBody = await request.text();
const webhook = new Webhook(process.env.OMNIPAY_WEBHOOK_SECRET!);
let event: unknown;
try {
event = webhook.verify(rawBody, {
"svix-id": request.headers.get("svix-id") ?? "",
"svix-timestamp": request.headers.get("svix-timestamp") ?? "",
"svix-signature": request.headers.get("svix-signature") ?? "",
});
} catch {
return new Response("Invalid signature", { status: 400 });
}
await enqueueVerifiedEvent(event);
return new Response(null, { status: 204 });
}Use your framework's raw-body mode if it parses JSON automatically.
Endpoint checklist
Before enabling production delivery, confirm that the endpoint:
- is reachable over HTTPS without browser login or CSRF checks;
- reads and verifies the raw request body before parsing it;
- requires all three Svix signature headers;
- stores
svix-idwith a unique constraint; - ignores unknown
eventTypevalues safely; - returns
2xxwithin 15 seconds after persisting or enqueueing the event; - never logs signing secrets, authorization headers, or full sensitive payloads.
Acknowledge quickly
Return any 2xx response within 15 seconds. Verify, persist or enqueue the
event, and acknowledge; perform fulfillment and other network calls
asynchronously.
Svix considers the HTTP status authoritative. A 200 response with
{"ok": false} is still success; a correct JSON response with status 500 is
still failure.
Idempotent consumption
Store the svix-id or another immutable event identifier with a unique
constraint before applying side effects. Replays and ambiguous network failures
can produce another delivery attempt.
For order fulfillment:
- verify the signature;
- insert the delivery ID transactionally;
- find the order by
merchant_idand cross-check the OmniPayid; - compare amount and expected currency;
- transition the order only if its current state permits it;
- commit the processed delivery and business change together.
Never fulfill from deposit.created or a browser return. Fulfill an order only
from deposit.succeeded or a separately authenticated status response. Mark a
payout complete only from withdraw.succeeded or its authenticated status
response—not from withdraw.created.
Retries and replay
Svix retries failed deliveries with exponential backoff. Operators can also resend one message or recover a window of failed messages from the portal. Deleting or disabling an endpoint stops its delivery attempts.
Creation events are delivered before terminal events. Consumers must remain idempotent for retries, manual replay, and ambiguous network failures.
Troubleshooting
| Symptom | Check |
|---|---|
| Every signature fails | Verify the raw body, correct endpoint secret, and all three Svix headers |
| Intermittent timeout retries | Acknowledge after enqueue instead of doing work inline |
| Valid messages marked failed | Return a 2xx status |
| Duplicate fulfillment | Add a unique processed-delivery record and transactional state transition |
| Missing terminal event | Confirm the creation event was acknowledged, read the authenticated resource status, then contact OmniPay support with its id |