Webhooks
TeronaPay POSTs signed JSON events to your endpoint when state changes — payments succeeded, refunds finalized, payouts completed. Webhook delivery is the canonical mechanism for learning about async state; don't poll.
Registering an endpoint
curl -X POST https://api.teronapay.com/v1/webhooks \
-u "$NOWPESA_KEY_ID:$NOWPESA_SECRET" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-domain.com/webhooks/nowpesa",
"event_types": ["*"]
}'event_types can be ["*"] (everything) or a comma-list like ["payment.succeeded","payment.refunded"].
Before activation we POST a verification probe to your URL. It must return 2xx — otherwise registration fails with failed_precondition.
The response includes the signing secret (plaintext, returned once). Store it.
Verifying signatures
Every delivery carries X-Nowpesa-Signature: t=<unix>,v1=<hex_hmac>. Verify with HMAC-SHA256 over <t>.<raw_body>:
// Node.js example
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (!parts.t || !parts.v1) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(parts.v1);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Reject with 401 on failure. We recommend also rejecting if now - t > 5min (replay protection).
Retry semantics
We retry failed deliveries up to 6 times with exponential backoff: 30s, 2m, 10m, 30m, 2h, 12h. After the 6th failure the delivery is marked failed_permanently and stops retrying.
Your handler should:
- Respond
2xxquickly (under 10s). - Be idempotent on
event_id. Same event id = same logical event, possibly delivered twice.
Secret rotation
Rotate the signing secret without dropping events:
# 1. Rotate — stores a new secret alongside the existing one.
curl -X POST https://api.teronapay.com/v1/webhooks/$EP_ID/rotate-secret \
-u "$NOWPESA_KEY_ID:$NOWPESA_SECRET"
# Subsequent deliveries carry BOTH signatures:
# X-Nowpesa-Signature: t=...,v1=... (old secret)
# X-Nowpesa-Signature-Next: t=...,v1=... (new secret)
# 2. Deploy your verifier to accept either header.
# 3. Promote — retires the old secret.
curl -X POST https://api.teronapay.com/v1/webhooks/$EP_ID/promote-secret \
-u "$NOWPESA_KEY_ID:$NOWPESA_SECRET"After Promote, only X-Nowpesa-Signature ships (with the new secret).
Alerts when your endpoint fails
If your endpoint accumulates ≥ 3 permanent failures in 1 hour, we POST a failure-burst alert to a per-account ops URL (configurable in the dashboard). When the endpoint starts succeeding again we POST a recovery alert. The kind field distinguishes the two.
This is opt-in via the statement email schedule dashboard section (we use the same notification rail).
Per-payment callbacks
Instead of a globally registered endpoint, you can route a single payment's events to a specific URL by passing callback_url when you create the payment:
curl -X POST https://api.teronapay.com/v1/payments \
-u "$NOWPESA_KEY_ID:$NOWPESA_SECRET" \
-H "Content-Type: application/json" \
-d '{
"reference": "order-5678",
"amount": 200.00,
"currency": "KES",
"channel": "mpesa_stk_push",
"payer_phone": "+254712345678",
"callback_url": "https://your-domain.com/callbacks/order-5678"
}'When callback_url is set: payment.succeeded and payment.failed events go only to that URL — your registered endpoints are skipped for that payment. Retries, backoff, and the X-Nowpesa-Signature header work identically.
When callback_url is omitted: events fan out to all active registered endpoints as usual. Existing integrations are unaffected.
Callback signing secret
Per-payment callbacks use a separate per-merchant callback signing secret (distinct from per-endpoint secrets). Find it in the dashboard under Webhooks → Per-payment callbacks, or via the API:
curl https://api.teronapay.com/v1/webhooks/callback-secret \
-u "$NOWPESA_KEY_ID:$NOWPESA_SECRET"Verify signatures exactly as you would for endpoint deliveries — HMAC-SHA256 over <t>.<raw_body>. Rotating the callback secret retires the old one immediately (no two-step Promote like endpoint rotation).
Replay
Any past delivery — including failed_permanently — can be re-fired from the dashboard. The event_id is preserved, so a properly-idempotent handler treats the replay the same as the original.
What's next
- Webhook events reference — every event type and payload shape.
- API: Webhooks — every endpoint, including the callback-secret routes.
