WebhooksOverview

Webhooks

Webhooks are how Soxara tells you about things that happened asynchronously. A customer paid. A refund cleared. A gift card got redeemed. A bill payment settled.

You register an endpoint URL with us. We POST a JSON payload to it. You return 200.

When to use webhooks

  • Your customer just paid. You want to mark their order as paid in your database — but you can’t trust the client to tell you (they could close the tab, lose connection, fake the request). Instead, listen for payment.succeeded on your server.
  • A pending MoMo deposit just settled. The customer initiated it, the user approved on their phone, MTN confirmed — you want to credit the right account in your system the moment it settles.
  • A dispute was opened on a card payment from two weeks ago. You’re not sitting on a polling loop — you get notified the moment it happens.

If you find yourself polling the Soxara API every few seconds to check status, you almost certainly want a webhook instead.

Registering an endpoint

curl -X POST $SOXARA_BASE/v1/webhook-endpoints \
  -H "Authorization: Bearer $SOXARA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/soxara",
    "events": ["payment.succeeded", "payment.failed", "refund.created"]
  }'

Response includes the signing secret for this endpoint:

{
  "success": true,
  "data": {
    "id": "we_3TZ...",
    "url": "https://your-app.example.com/webhooks/soxara",
    "events": ["payment.succeeded", "payment.failed", "refund.created"],
    "signing_secret": "whsec_AbCdEfGh...",
    "livemode": false,
    "created_at": "2026-05-23T14:00:00Z"
  }
}

Save the signing secret immediately. It’s only shown at creation time. You’ll use it to verify every webhook delivery — see Signature verification.

You can register multiple endpoints. Each one gets its own signing secret. Common patterns:

  • One endpoint per environment (test endpoint, live endpoint)
  • One endpoint per concern (payments endpoint, refunds endpoint, gift-cards endpoint) so each has minimal blast radius
  • One endpoint for “everything” plus shadow endpoints for ops dashboards

Payload shape

Every webhook delivery has the same outer shape:

{
  "id": "evt_3TZ...",
  "type": "payment.succeeded",
  "livemode": true,
  "created_at": "2026-05-23T14:05:33Z",
  "data": {
    "object": {
      "id": "pi_3TZ...",
      "amount": 1250,
      "currency": "USD",
      "status": "succeeded",
      "..."
    }
  }
}
FieldPurpose
idStable event ID. Use this to dedupe — Soxara may deliver the same event more than once if your server didn’t respond fast enough the first time.
typeEvent name. See Event reference.
livemodetrue for production events, false for sandbox.
created_atISO-8601 UTC when the event was generated. Not the delivery time.
data.objectThe resource the event is about. Same shape as the API response for that resource type.

Delivery semantics

  • At-least-once. Soxara may deliver the same event more than once. Always dedupe by event.id.
  • Ordered per resource, best-effort overall. Events for the same payment will arrive in order (e.g., createdsucceeded). Events across unrelated resources may interleave.
  • 3-second response budget. If your endpoint doesn’t return 2xx within 3 seconds, we treat it as a failure and retry.
  • Retry schedule: 5 attempts over ~24 hours with exponential backoff (1s, 30s, 5m, 1h, 12h).
  • After 5 failed attempts, the event is dead-lettered. You can replay it from the dashboard for up to 30 days.

What your handler should do

1. Read the request body as raw bytes (don't parse JSON yet).
2. Verify the signature header (`Soxara-Signature`) against the signing
   secret you stored at endpoint-creation. Reject anything that doesn't
   verify. (See /webhooks/signature for the exact pattern.)
3. Parse the JSON.
4. Check `event.id` — if you've seen it before, return 200 and stop.
5. Otherwise: do the minimum work to acknowledge the event (e.g., enqueue
   a job, write a row), then return 200.
6. Do the heavy lifting asynchronously, outside the webhook handler.

The 3-second budget is generous for “write a row + return 200.” It is not generous for “fan out emails + update analytics + reconcile ledgers.” Push that to a queue.

See the Handle a webhook guide for a working pattern in Node + Python.