TeronaPay

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 2xx quickly (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

Webhooks · TeronaPay docs