Best practices
What keeps an integration working once the happy path stops being the only path.
A quickstart shows you the call that works. These five pages are about the calls that do not: the timeout that leaves you unsure whether a payment exists, the webhook delivered three times, the key that ends up in a browser bundle, the nightly export that trips a ceiling nobody measured.
Nothing here is exotic. Every item comes from a failure the API can actually produce, and each page names it.
| Page | The failure it prevents | What skipping it costs |
|---|---|---|
| Idempotency | A retry after a timeout charges twice | A refund, and a customer who stops trusting you |
| Error handling | Treating a decline like an outage, or an outage like a decline | Lost sales on one side, a retry storm on the other |
| Security | A secret key leaving your server | Full API access for whoever finds it |
| Performance | Walking a list one request at a time | Slow exports that eat the quota checkout needs |
| Rate limiting | A batch job racing your live traffic | A 429 on the one call with a customer waiting |
Cheaper before production than after
Each of these is a few lines when you write the integration, and a postmortem when you add it later. Put them in your definition of done.
Rule 1: the browser never confirms a payment
callback is a redirect. The payer's browser lands on it because the hosted page sent it there, and
nothing in that URL is signed or trustworthy. A customer can open it by hand, bookmark it, or reach
it after closing the tab mid payment.
The two sources of truth are the payment.succeeded webhook and GET /payments/{id} asked by your
own server. Use the return page to show something, never to decide something.
Rule 2: verify the signature before you read the body
A webhook endpoint that parses first and checks later is an endpoint anyone on the internet can
drive. Verify X-Wajub-Signature against the raw request bytes, then parse.
# What Wajub sends you
POST /webhooks/wajub HTTP/1.1
X-Wajub-Signature: v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-Wajub-Timestamp: 1748083260
X-Wajub-Event: payment.succeeded
X-Wajub-Delivery-Id: whd_7Yh2MpL4tRb3nP8sZcXv
Content-Type: application/json
{"id":"evt_…","event":"payment.succeeded","data":{…}}The signing secret comes from the client you built, not from a fourth argument. Passing it there overwrites the tolerance window and silently disables replay protection. Signature verification covers the manual path when you are not using an SDK.
Rule 3: give every call an id you can search for
Wajub stamps X-Request-Id on every response, and honours the one you send if it is 64 characters
or fewer. Send your own order id and the same string identifies the call in your logs, in
Konsole, and in any ticket you open with support.
curl https://api.wajub.com/payments \
-H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
-H "X-Request-Id: order-4172-attempt-1" \
-H "Idempotency-Key: ORDER-4172" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"currency": "XAF",
"email": "buyer@example.com"
}' -D --D - prints the response headers, so you can see the id come back alongside X-Trace-Id and the
W3C traceparent that OpenTelemetry collectors understand.
What to record on your side is short: the request id, the payment uid, the HTTP status, and the event id of every webhook you accept. That is enough to answer the only question that matters during an incident, which is whether a given order ever reached Wajub.
Related pages
- IdempotencyMake a retry safe, so a timeout never becomes two payments.
- Error handlingWhich failures to retry, which to surface, and which to never repeat.
- SecurityKeys, scopes, IP allow-lists, and what happens when one leaks.
- PerformancePaginate, cache what is stable, and parallelise without tripping a ceiling.
- Rate limitingThe four ceilings, and how to keep batch work away from checkout.
- Guides & tutorialsThe same patterns applied end to end, one integration at a time.