Webhooks API
The Endpoint object
{
"id": "66d5050b-1cd5-4953-b93b-f4ae9fee3fd9",
"url": "https://your-domain.com/webhooks/nowpesa",
"event_types": ["*"],
"created_at": "2026-05-19T10:59:34Z",
"signing_secret_rotated_at": null
}signing_secret and signing_secret_next are returned only by create and rotate responses (one-time reveals).
POST /v1/webhooks
Register an endpoint. We POST a one-time verification probe to the URL before activation; it must return 2xx.
Request
{
"url": "https://your-domain.com/webhooks/nowpesa",
"event_types": ["*"]
}| Field | Type | Required | Notes |
|---|---|---|---|
url | string | ✓ | Must be HTTPS in production |
event_types | array | ✗ | ["*"] for all; else a list like ["payment.succeeded","payment.refunded"] |
Response 201:
{
"id": "...",
"url": "...",
"event_types": ["*"],
"created_at": "...",
"signing_secret": "whsec_..."
}Save the secret now — we don't store the plaintext server-side.
Errors
400 invalid_request— bad URL shape422 precondition_failed— URL verification probe failed
GET /v1/webhooks
List endpoints.
{ "data": [ /* Endpoint objects */ ] }signing_secret is omitted on list responses.
GET /v1/webhooks/{id}
Retrieve one endpoint.
DELETE /v1/webhooks/{id}
Soft-delete (sets disabled_at). No more deliveries. Past delivery rows remain in the dashboard.
POST /v1/webhooks/{id}/rotate-secret
Generate a new signing secret. Both signing_secret (current) and signing_secret_next (new) ship on subsequent deliveries until you Promote.
Response 200:
{
"id": "...",
"url": "...",
"signing_secret_next": "whsec_...",
"signing_secret_rotated_at": "2026-05-19T10:59:45Z"
}The new secret is returned once — save it before processing the response.
POST /v1/webhooks/{id}/promote-secret
Retire the old signing secret. From now on, only X-Nowpesa-Signature ships (with the new secret).
Errors
422 precondition_failed— no rotation in flight
Per-payment and per-payout callbacks
Instead of (or alongside) registered endpoints, you can route a single payment's or payout's
events to a specific URL by passing callback_url at creation time:
POST /v1/payments— routespayment.succeeded/payment.failedonly to that URLPOST /v1/payouts— routespayout.succeeded/payout.failedonly to that URL
When set, your registered endpoints are skipped for that specific payment or payout. Everything else (retries, backoff, the signed headers) is identical to endpoint deliveries.
These callbacks are signed with a per-merchant callback signing secret (separate
from per-endpoint secrets), so verify them exactly as you verify endpoint deliveries —
recompute the HMAC over the raw body with this secret and compare to
X-Nowpesa-Signature.
GET /v1/webhooks/callback-secret
Returns your callback signing secret (created on first request). Unlike endpoint secrets, it's retrievable any time.
{
"signing_secret": "whsec_...",
"created_at": "2026-06-06T10:00:00Z",
"rotated_at": null
}POST /v1/webhooks/callback-secret/rotate
Issue a new callback signing secret. The previous one stops working immediately, so update your verifier before rotating. Returns the same shape with the new secret.
GET /v1/webhooks/{id}/deliveries
List deliveries for an endpoint. Newest first.
Query parameters: status, event_type, limit (1..100, default 25).
{
"data": [
{
"id": "...",
"endpoint_id": "...",
"event_id": "...",
"event_type": "payment.succeeded",
"status": "delivered",
"attempts": 1,
"last_status_code": 200,
"last_attempt_at": "2026-05-19T10:23:14Z",
"next_attempt_at": "2026-05-19T10:23:14Z",
"created_at": "2026-05-19T10:23:13Z",
"payload": { /* signed event body */ }
}
]
}GET /v1/webhooks/{id}/deliveries/{did}/attempts
Per-attempt audit log. Oldest first.
{
"data": [
{ "id": 1, "delivery_id": "...", "status_code": 502, "error": "...", "attempted_at": "..." },
{ "id": 2, "delivery_id": "...", "status_code": 200, "error": "", "attempted_at": "..." }
]
}POST /v1/webhooks/{id}/deliveries/{did}/replay
Re-fire a delivery. Status resets to pending; the worker picks it up next tick. Same event_id as the original — your handler should be idempotent.
Errors
422 precondition_failed— delivery is currentlypending(only terminal rows are replayable)
