Webhooks
Webhooks are how Soxara tells you a payment settled — asynchronously, server to server, so you never trust the client or sit on a polling loop.
You register an endpoint URL with us. We POST a signed JSON payload to it. You
verify it and return 200.
When to use webhooks
- Your customer paid via the hosted checkout — mark their order paid when
payment.completedarrives, not when the browser says so. - A recurring mandate charge settled (or failed) — roll the subscription forward, or start dunning.
- A pending MoMo payment finally cleared minutes later — react the moment it does.
If you’re polling GET /v1/api/payments/{id} every few seconds, you want a
webhook instead. (That read endpoint is for reconciliation / a missed delivery,
not a polling loop.)
Registering an endpoint
Register with the same API key you pay with —
payments:create scope, no dashboard needed:
curl -X POST $SOXARA_BASE/v1/api/webhook-endpoints \
-H "Authorization: Bearer $SOXARA_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/soxara",
"events": ["payment.completed", "payment.failed"]
}'The response includes the signing secret for this endpoint — returned once, at creation:
{
"success": true,
"data": {
"endpoint": {
"id": "…",
"url": "https://your-app.example.com/webhooks/soxara",
"events": ["payment.completed", "payment.failed"],
"secret_hint": "…AbCd",
"created_at": "2026-07-04T14:00:00Z"
},
"secret": "KciRV7ED9…" // ← store this now; never shown again
}
}Save secret immediately. You verify every delivery with it — see
Signature verification. Later reads
(GET /v1/api/webhook-endpoints) only return secret_hint, never the secret.
Prefer clicking to curling? The same thing lives in the Soxara dashboard under
Developers → Webhooks. You can register multiple endpoints (e.g. a test
URL and a prod URL) — each gets its own secret. Revoke with
DELETE /v1/api/webhook-endpoints/{id}.
Allowed events today: payment.completed, payment.failed (see
Event reference).
Payload
Every delivery has the same shape:
{
"type": "payment.completed",
"createdAt": "2026-07-04T14:05:33Z",
"data": {
"payment": {
"id": "…", "amount": 1200, "currency": "USD", "status": "completed",
"payment_method": "card", "environment": "live",
"mandate_id": "…", "payment_link_id": null,
"idempotency_key": "sub_42:2026-07", "created_at": "…"
},
"link": { … } // present only for a link payment
}
}| Field | Purpose |
|---|---|
type | Event name — payment.completed or payment.failed. |
createdAt | ISO-8601 UTC when the event was generated (not delivery time). |
data.payment | The payment — same shape as GET /v1/api/payments/{id}. |
data.link | A snapshot of the payment link, when the payment is link-bound. |
Alongside the body, each request carries three headers:
| Header | Purpose |
|---|---|
Soxara-Signature | t=<unix>,v1=<hmac> — verify this. |
Soxara-Event | The event type (matches type). |
Soxara-Delivery | A stable id for this delivery — dedupe on it (it’s the same across retries of one event). |
Delivery semantics
- At-least-once. The same event may arrive more than once (a retry after your
server was slow, say). Dedupe on the
Soxara-Deliveryheader — or make your handler idempotent ondata.payment.idempotency_key. - Respond quickly. Return
2xxwithin ~10 seconds. Anything else is treated as a failure and retried. - Retry schedule: up to 7 attempts with exponential backoff —
1s, 5s, 25s, 2m, 10m, 1h, 6h(~10 hours total). After that the delivery is dead-lettered; you can force a redeliver from the dashboard, or re-pull withGET /v1/api/payments/{id}.
What your handler should do
1. Read the request body as raw bytes (don't parse JSON yet).
2. Verify the `Soxara-Signature` header against the secret you stored at
registration. Reject anything that doesn't verify. (See /webhooks/signature.)
3. Parse the JSON.
4. Check `Soxara-Delivery` — if you've seen it, return 200 and stop.
5. Do the minimum to acknowledge (enqueue a job / write a row), return 200.
6. Do the heavy lifting asynchronously, outside the handler.See the Handle a webhook guide for a working Node + Python pattern.