API referenceWebhook endpoints

Webhook endpoints

CRUD for webhook subscriptions. Each endpoint is bound to one (merchant, env), sandbox + live environments have separate endpoints.

For the actual signing scheme + verification examples, see Webhooks.

The webhook_endpoint object

{
  "object": "webhook_endpoint",
  "id": "ckwhe_xxxxxxxxxxxx",
  "url": "https://your-app.com/webhooks/swappr",
  "events": ["payout_paid", "payout_failed", "wallet_funded"],
  "is_active": true,
  "env": "live",
  "last_success_at": "2026-05-05T12:34:56Z",
  "last_failure_at": null,
  "consecutive_failures": 0,
  "created_at": "2026-01-15T10:00:00Z",
  "updated_at": "2026-05-05T12:34:56Z"
}

POSTCreate a webhook endpoint

POST/v1/webhook_endpoints
⚠️

The signing secret is returned ONCE in the response body. Store it before discarding the response, it can’t be retrieved later, only rotated.

Creating an endpoint is idempotent on the Idempotency-Key, a same-key + same-body retry returns the original endpoint (including its secret) with 200; same key + different body returns 409 idempotency_key_conflict.

Body parameters
urlstringRequired

Must be HTTPS. HTTP is rejected.

eventsarrayRequired

Non-empty array of event types

Request
curl https://api.swappr.me/api/v1/webhook_endpoints \
  -H "Authorization: Bearer sk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/swappr",
    "events": ["payout_paid", "payout_failed", "wallet_funded"]
  }'
Response
{
  "object": "webhook_endpoint",
  "id": "ckwhe_xxxxxxxxxxxx",
  "url": "https://your-app.com/webhooks/swappr",
  "events": ["payout_paid", "payout_failed", "wallet_funded"],
  "is_active": true,
  "env": "test",
  "secret": "whsec_test_aBcDeFgHiJkLmNoPqRsTuVwXyZ012345",
  "created_at": "2026-05-05T12:34:56Z",
  ...
}
200 OK

GETList webhook endpoints

GET/v1/webhook_endpoints
Query parameters
limitintegerOptional

1-100, default 50

starting_afterstringOptional

Cursor

Cursor-paginated. The signing secret is never returned on list/retrieve, only on create + rotate.

Request
curl https://api.swappr.me/api/v1/webhook_endpoints \
  -H "Authorization: Bearer sk_test_..."

GETRetrieve a webhook endpoint

GET/v1/webhook_endpoints/{id}

Returns the endpoint without the signing secret.

Request
curl https://api.swappr.me/api/v1/webhook_endpoints/ckwhe_xxx \
  -H "Authorization: Bearer sk_test_..."

PATCHUpdate a webhook endpoint

PATCH/v1/webhook_endpoints/{id}

Narrow update: url / events / is_active only.

Pass the fields you want to change; others are left as-is.

200 OK with the updated endpoint.

Request
{
  "events": ["payout_paid", "payout_failed", "wallet_funded", "customer_verified", "wallet_auto_converted"],
  "is_active": true
}

DELDelete a webhook endpoint

DEL/v1/webhook_endpoints/{id}

Permanently removes the endpoint + cascades all delivery history. Cannot be undone.

Request
curl https://api.swappr.me/api/v1/webhook_endpoints/ckwhe_xxx \
  -X DELETE \
  -H "Authorization: Bearer sk_test_..."
Response
{
  "object": "webhook_endpoint_delete_result",
  "id": "ckwhe_xxxxxxxxxxxx",
  "deleted": true
}
200 OK

POSTSend a test event

POST/v1/webhook_endpoints/{id}/test

Fires a synthetic payout_paid-shaped event with _test: true marker. Verifies your signature handler + receiver are wired correctly. Common during integration setup or after rotating a signing secret.

Headers on the synthetic event

X-Swappr-Signature: t=<unix-seconds>,v1=<hex-hmac-sha256>
X-Swappr-Event: payout_paid
X-Swappr-Test: true
User-Agent: Swappr-Webhooks/1.0

200 OK on success. 502 Bad Gateway if your receiver returned non-2xx OR a network error. 400 Bad Request if the endpoint is disabled (is_active=false), re-enable via PATCH first.

Synthetic event body
{
  "_test": true,
  "event": "payout_paid",
  "data": {
    "reference": "test_<timestamp>",
    "amount_minor": "100000",
    "currency": "NGN",
    "recipient_name": "TEST RECIPIENT",
    "bank_code": "044",
    "timestamp": "2026-05-05T12:34:56Z"
  }
}
Response
{
  "object": "webhook_test_result",
  "endpoint_id": "ckwhe_xxx",
  "delivery_id": "ckwhd_xxx",
  "status": "delivered",
  "response_status": 200,
  "attempts": 1
}
200 OK
Response
{
  "error": {
    "type": "provider_error",
    "code": "delivery_failed",
    "message": "Receiver returned non-2xx status: 500."
  }
}
502 Bad Gateway

Best practices

  1. Use separate endpoints for sandbox + live: they’re env-scoped, so don’t try to share. Different secrets, different delivery histories.
  2. Subscribe to only the events you handle: un-subscribed events still fire 0 webhooks. Don’t list events your handler doesn’t process, saves you from accidentally returning 5xx and burning retry budget.
  3. Rotate signing secrets periodically: once per quarter is reasonable. Use the dashboard’s rotate flow; old + new both work for 60 minutes during the cutover.
  4. Test before going live: call /test endpoint as part of your CI/CD pipeline post-deploy, so you know the receiver is wired correctly.