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"
}Create 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.
Request
{
"url": "https://your-app.com/webhooks/swappr",
"events": ["payout_paid", "payout_failed", "wallet_funded"]
}| Field | Required | Notes |
|---|---|---|
url | yes | Must be HTTPS. HTTP is rejected. |
events | yes | Non-empty array of event types |
Example
curl https://api.swappr.me/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",
...
}List webhook endpoints
GET /v1/webhook_endpoints
| Param | Notes |
|---|---|
limit | 1-100, default 50 |
starting_after | Cursor |
Cursor-paginated. The signing secret is never returned on list/retrieve — only on create + rotate.
Retrieve a webhook endpoint
GET /v1/webhook_endpoints/{id}
Returns the endpoint without the signing secret.
Update a webhook endpoint
PATCH /v1/webhook_endpoints/{id}
Narrow update — url / events / is_active only.
Request
{
"events": ["payout_paid", "payout_failed", "wallet_funded", "customer_verified"],
"is_active": true
}Pass the fields you want to change; others are left as-is.
Response
200 OK with the updated endpoint.
Delete a webhook endpoint
DELETE /v1/webhook_endpoints/{id}
Permanently removes the endpoint + cascades all delivery history. Cannot be undone.
curl https://api.swappr.me/v1/webhook_endpoints/ckwhe_xxx \
-X DELETE \
-H "Authorization: Bearer sk_test_..."Response
{
"object": "webhook_endpoint_delete_result",
"id": "ckwhe_xxxxxxxxxxxx",
"deleted": true
}Send 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.0Body
{
"_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
200 OK on success:
{
"object": "webhook_test_result",
"endpoint_id": "ckwhe_xxx",
"delivery_id": "ckwhd_xxx",
"status": "delivered",
"response_status": 200,
"attempts": 1
}502 Bad Gateway if your receiver returned non-2xx OR a network error:
{
"error": {
"type": "provider_error",
"code": "delivery_failed",
"message": "Receiver returned non-2xx status: 500."
}
}400 Bad Request if the endpoint is disabled (is_active=false) — re-enable via PATCH first.
Best practices
- Use separate endpoints for sandbox + live — they’re env-scoped, so don’t try to share. Different secrets, different delivery histories.
- 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.
- 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.
- Test before going live — call
/testendpoint as part of your CI/CD pipeline post-deploy, so you know the receiver is wired correctly.