TeronaPay

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": ["*"]
}
FieldTypeRequiredNotes
urlstringMust be HTTPS in production
event_typesarray["*"] 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 shape
  • 422 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:

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 currently pending (only terminal rows are replayable)
API: Webhooks · TeronaPay docs