Skip to content

Quickstart

A sandbox payment from create to verified webhook, for an account that already has keys.

A payment is four steps, and they never change: create it, send the customer to a page, ask the API what really happened, then let webhooks tell you about everything that lands after the customer is gone. The four integration modes differ only in who renders the second step. The rest of this page is identical for all of them.

This page starts after setup

It assumes an account, sandbox keys and an installed SDK. If you have none of that yet, the platform quickstart covers account creation, keys and install, then walks the same four steps from zero.

1. Create the payment

One request carries the whole intent: how much, in which currency, who is paying, and where that person comes back. Wajub records it, keeps it pending, and answers with a page to send the customer to. Nothing has reached an operator yet, and nobody has been charged.

Send it from your server with the secret key. The public key creates payments too, but it exists for the one case where the call happens inside a browser.

Describe the payer in the customer object. A top-level email also works and lands on the same customer record, but the object is what grows with you: it carries name, phone, address and metadata as well.

POSThttps://api.wajub.com/payments
curl https://api.wajub.com/payments \
  -H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 5000,
    "currency": "XAF",
    "customer": { "email": "amina@example.com" },
    "reference": "order-4172",
    "callback": "https://shop.example.com/order/complete"
  }'

Wajub answers 201 Created. Two fields matter here: transaction.id, which you store against your order, and authorization_url, which is where the customer goes next.

Response · 201 Created
{
"code": 201,
"status": "Created",
"message": "Payment initiated",
"authorization_url": "https://pay.wajub.com/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"authorization_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"transaction": {
"id": "trx_test_CSUGajfv9xh0XQ5wu2lx",
"reference": "order-4172",
"amount": 5000,
"amount_paid": 0,
"currency": "XAF",
"status": "pending",
"customer": {
"id": "cus_test_sAaim5apjocIgtlhzJY3wQ8s",
"email": "amina@example.com"
},
"callback": "https://shop.example.com/order/complete",
"sandbox": true,
"created_at": "2026-09-11T10:24:00+00:00"
}
}

Amounts are in the major unit, never in centimes

"amount": 5000 is 5 000 XAF, not 50. Wajub has no subunits. A payment in XAF or XOF must fall between 25 and 2 000 000; outside that range the API answers 422 with the bound in errors.amount.

reference is yours: your order number, returned on every read and searchable in the Dashboard. It carries no uniqueness constraint, so sending it twice creates two payments.

What does protect you from a double charge is the Idempotency-Key header. Generate it yourself, store it next to the order, and resend the same value on every retry; the API then returns the original payment instead of opening a second one, however long after, and refuses the key with a 422 if the amount, the currency or the reference changed. A key is spent for good: a new attempt on the same order needs a new one. The SDKs do attach a key automatically, but a fresh random one per call, which deduplicates nothing. Idempotency covers the strategies that work.

2. Send the customer to the page

Redirect to authorization_url. The customer picks an operator, types their number, confirms on their handset and lands back on your callback.

That is Hosted Checkout, and it is the mode we recommend. Wajub owns the screen, the operator list, the retry when a prompt times out and the wording of every failure. You own none of it, and none of it is code you maintain.

The session stays valid for 24 hours. Pass expires.in in minutes, anywhere from 5 to 43 200, to close the window sooner.

In sandbox, the last six digits of the number decide the outcome, and …000000 succeeds. The full suffix table and a number per operator are in the platform quickstart. Sandbox never asks for a PIN.

3. Verify before you fulfil

The customer landing back on your callback proves nothing. Anyone can open that URL by hand, and a customer who closes the tab never opens it at all. Ask the API what happened, from your server, with the id you stored in step 1.

GEThttps://api.wajub.com/payments/{id}
curl https://api.wajub.com/payments/trx_test_CSUGajfv9xh0XQ5wu2lx \
  -H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…"

The answer is the payment as Wajub knows it, whatever the browser was told. channel names the operator that was actually used, and amount_paid is what was really collected:

Response · 200 OK
{
"code": 200,
"status": "OK",
"message": "Payment retrieved",
"transaction": {
"id": "trx_test_CSUGajfv9xh0XQ5wu2lx",
"reference": "order-4172",
"amount": 5000,
"amount_paid": 5000,
"currency": "XAF",
"status": "succeeded",
"channel": "cm.mtn",
"customer": {
"id": "cus_test_sAaim5apjocIgtlhzJY3wQ8s",
"email": "amina@example.com"
},
"sandbox": true,
"created_at": "2026-09-11T10:24:00+00:00"
}
}

Ship the order only when status is succeeded, and only once amount and currency match what you charged for. A payment still pending is not a failure, it is a customer who has not finished yet: leave the order open and let the next step close it.

4. Let the webhook close the loop

Verifying on return covers the customers who come back. Webhooks cover everyone else, and every event that has no browser behind it at all: a confirmation the operator sends ten minutes late, a refund issued from the Dashboard, a session that expires untouched.

Subscribe to the six events a payment can produce, by name. A subscription matches exact names only, so payment.* would receive nothing.

EventFires when
payment.createdThe payment was recorded and is pending
payment.processingThe request reached the operator, the customer is being prompted
payment.succeededFunds captured. This is the one to fulfil on
payment.failedThe operator reported the attempt as failed after the prompt went out. A decline answered at once leaves the payment pending and sends nothing
payment.expiredThe session ran out before the customer confirmed
payment.cancelledCancelled with DELETE /payments/{id}, or with the Cancel button of the hosted page

Webhooks are documented once, not here

Endpoint registration, signature verification, the retry schedule and the full catalog live in Webhooks. One rule belongs here though: deduplicate on X-Wajub-Delivery-Id. A failed delivery is retried for up to 72 hours and every attempt carries that same id, which is what keeps a retry from shipping the order twice.

Then pick the mode you ship

What you just built is Hosted Checkout. The other three modes change exactly one thing, who renders the screen in step 2. Creating, verifying and handling webhooks stay word for word what they are above.

What's next?

What did you think of this content?