Skip to content

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.

PageThe failure it preventsWhat skipping it costs
IdempotencyA retry after a timeout charges twiceA refund, and a customer who stops trusting you
Error handlingTreating a decline like an outage, or an outage like a declineLost sales on one side, a retry storm on the other
SecurityA secret key leaving your serverFull API access for whoever finds it
PerformanceWalking a list one request at a timeSlow exports that eat the quota checkout needs
Rate limitingA batch job racing your live trafficA 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.

Tag a call with something you can grep
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.

What did you think of this content?