Skip to main content
Mobile money payments finish on the customer’s phone, after your API call has returned. A webhook is how you learn the result: PrimePay sends an HTTPS POST to your server when a payment, refund or payout succeeds or fails.

Set up an endpoint

1

Build a route on your server

It must accept a POST with a JSON body at a public https:// address, for example https://api.yourshop.mw/webhooks/primepay.
2

Add it in the dashboard

Open your account in the dashboard, go to Settings › Webhooks and add the address. Choose which events it receives.
3

Save the signing secret

Each endpoint has its own signing secret, starting with whsec_. Store it on your server like a password. You use it to check every webhook.
4

Send a test event

Send a test event from the endpoint’s page in the dashboard. Your endpoint receives a webhook.test event; answer 200 and the dashboard shows it as delivered.
Rules for the address:
  • It must start with https://, on port 443 or 8443.
  • It must use a public host name, not an IP address or localhost.
  • It can’t contain a user name or password.
  • An account can have up to 5 endpoints.

Events

Transfers have no events: a transfer’s result is in the API response.

What a webhook looks like

Webhook bodies use snake_case names and send amounts as text with two decimals ("25000.00"). The API itself uses camelCase and numbers. merchant_reference is the merchantOrderId you sent.
Inside data.object:
  • Payments have customer.msisdn (the payer’s number, partly hidden), checkout_session when the payment came through a hosted checkout or payment link, and sub_merchant (the account code) on aggregator payments.
  • Refunds have payment, the id of the payment refunded, and destination.
  • Payouts have destination, where the money went.
  • metadata is whatever you sent when you created it.

Check the signature

Anyone can send a POST to your address, so check every webhook before you trust it. The PrimePay-Signature header has a timestamp and one or more signatures:
To check it:
  1. Read t and every v1 from the header.
  2. Refuse the webhook if t is more than 5 minutes from now. This stops an old webhook being replayed.
  3. Compute an HMAC-SHA256 of t, a full stop, and the raw request body, using your whole signing secret (whsec_…) as the key. Write it as lowercase hex.
  4. Accept the webhook if your result equals any v1, compared in constant time.
Use the body exactly as it arrived, byte for byte. If your framework parses the JSON and you turn it back into text, the signature won’t match.

Changing a signing secret

You can change an endpoint’s secret in the dashboard. If you keep the old one working during the change, PrimePay signs with both for 24 hours and the header carries two v1 values. Accepting a webhook when any v1 matches, as the code above does, means nothing breaks while you switch.

Answer quickly

  • Answer with any 2xx status as soon as you have stored the event. Do slow work (emails, stock updates) afterwards.
  • PrimePay waits up to 10 seconds for your answer. No answer in that time counts as a failure.
  • Redirects are not followed. A 3xx answer counts as a failure.
  • Answer with JSON or an empty body. If your address answers with a web page, the dashboard flags it, because that usually means a website answered and not your webhook code.

Retries

If your endpoint doesn’t answer 2xx, PrimePay tries again, up to 9 times over about 3 days: If you answer with a Retry-After header, PrimePay waits at least that long, up to 24 hours. After 5 failures in a row you get an email. An endpoint that has failed every delivery for 3 days is switched off; fix it, switch it back on in the dashboard and resend what you missed from the endpoint’s delivery list.

Handle each event once

Because of retries, the same event can reach you more than once. Before acting on an event, check whether you have already handled its id (also in the PrimePay-Event-Id header), and answer 2xx without doing the work again if you have. Events can also arrive out of order. Don’t rely on the order; use the status in the event, or fetch the payment with GET /v1/payments/{reference}.

A payment’s own callbackUrl

When you create a payment you can send a callbackUrl. That payment’s events are then sent there as well as to your endpoints.
  • The address must be HTTPS and its host must be one of your endpoints’ hosts, or a host listed under Settings › Security as an allowed callback host. Otherwise the delivery is skipped.
  • These deliveries are signed with the secret of your account’s default endpoint, so check them the same way.

Testing

  • A test event from the dashboard sends webhook.test to one endpoint.
  • Sandbox payments, refunds and payouts send webhooks too, with "livemode": false.
  • Every delivery, with your server’s answer, is listed under Settings › Webhooks for each endpoint, and you can resend one from there.