TeePayDocs
Integration guides

Webhooks

Receive signed notifications when payments and payouts complete, fail or are reversed.

TeePay sends an HTTPS POST to your server when a payment or payout reaches a final state. Webhooks are the recommended way to learn results; combine them with status reads for orders whose notification has not arrived.

Set up

Add your webhook URL

Open Payment profiles in the dashboard and set the webhook URL on your default profile (or on the profile you use for a checkout). It must be a public https:// URL.

Create the signing secret

Open Developers → Webhooks and create the signing secret (whsec_…). Store it on your server. You can rotate it at any time; the previous secret stops signing immediately.

Verify every request

Check the signature before trusting the body, as shown below.

Mobile money push without a profile

Webhooks are delivered to a payment profile's URL. If your account has no payment profile, no webhooks are sent; read the status with the API instead.

Events

EventSent when
payment.completedA payment was verified and posted to your ledger. Fulfil the order.
payment.failedA payment did not complete.
payment.voidedThe customer or provider cancelled the payment.
payment.expiredThe payment request expired before it was paid.
payout.completedA payout was delivered and settled.
payout.failedA payout did not complete.
payout.reversedA completed payout was reversed.

Payload

{
  "id": "0f8d7c2e-5b1a-5e3f-9c4d-7a6b2e1f0c9d",
  "event": "payment.completed",
  "environment": "live",
  "created_at": "2026-10-28T10:00:05.123456Z",
  "data": {
    "reference": "TP8K2M4QZ1",
    "transaction_id": "pay_6b1f…",
    "status": "completed",
    "occurred_at": "2026-10-28T10:00:03Z",
    "money": { "currency": "TZS", "amount_minor": "500000" },
    "source": { "type": "checkout_session", "id": "7c0e5d2a-1b3f-4c8e-9a6d-2f4b8e1c0d9a" }
  }
}

data.source tells you what was paid:

source.typesource.id
checkout_sessionThe checkout session id
control_numberThe Control Number id; source.control_number is the number
payment_link, payment_page, invoiceThe dashboard link, page or invoice
nullOther payments, such as a mobile money push: match on data.reference

Headers

HeaderValue
X-TeePay-Event-IDSame as id. Stable across retries: use it to de-duplicate.
X-TeePay-TimestampUnix time in seconds when this attempt was signed.
X-TeePay-Signaturev1= followed by the hex HMAC-SHA256 of <timestamp>.<raw body> with your secret.

Verify the signature

Compute the signature over the raw request body bytes, before any JSON parsing, and compare in constant time. Reject requests older than five minutes.

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyTeePayWebhook(rawBody: Buffer, headers: Record<string, string>, secret: string) {
  const timestamp = headers['x-teepay-timestamp'];
  const received = headers['x-teepay-signature'] ?? '';
  if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = 'v1=' + createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  return a.length === b.length && timingSafeEqual(a, b);
}

Respond and retry

  • Respond with any 2xx status within a few seconds, then do slow work in the background.
  • Any other status, a timeout or a connection error is retried with exponential backoff starting at about 30 seconds and capped at one hour, for up to 5 attempts.
  • The same event can arrive more than once. Process each X-TeePay-Event-ID once.
  • Delivery history and status codes are shown in Developers → Webhooks.

On this page