OmniPayDocs
Integration guides

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

Eventpayload.data.statusWhen it is emitted
deposit.createdpendingDeposit creation committed
deposit.succeededsucceededPayment completed successfully
deposit.failedfailedPayment or submitted verification failed
deposit.expiredexpiredNo valid completion before the deadline
deposit.cancelledcancelledCheckout was cancelled
deposit.refundedrefundedFunds 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

Eventpayload.data.statusWhen it is emitted
withdraw.createdprocessingWithdrawal accepted for processing
withdraw.succeededsucceededPayout completed
withdraw.failedfailedPayout 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/omnipay

Disable 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-id with a unique constraint;
  • ignores unknown eventType values safely;
  • returns 2xx within 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:

  1. verify the signature;
  2. insert the delivery ID transactionally;
  3. find the order by merchant_id and cross-check the OmniPay id;
  4. compare amount and expected currency;
  5. transition the order only if its current state permits it;
  6. 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

SymptomCheck
Every signature failsVerify the raw body, correct endpoint secret, and all three Svix headers
Intermittent timeout retriesAcknowledge after enqueue instead of doing work inline
Valid messages marked failedReturn a 2xx status
Duplicate fulfillmentAdd a unique processed-delivery record and transactional state transition
Missing terminal eventConfirm the creation event was acknowledged, read the authenticated resource status, then contact OmniPay support with its id

On this page