Skip to content

Direct API

Charge a payment from your server, on a channel you chose, with no Wajub page.

With the Direct API you render every screen. Your app collects the phone number, your server charges the payment on the channel you picked, and you show the waiting state and the result yourself. Wajub sends the prompt to the payer's handset and tells you the outcome.

It takes two calls. POST /payments records the payment, exactly as in every other mode, and POST /payments/{id} charges it. The second call is the one this page covers.

1. Create the payment

Create the payment as the Payments Quickstart shows. Keep transaction.id from the answer. You will not use authorization_url or authorization_token: those are for the hosted page and the components.

The payment is pending, and nobody has been charged yet.

2. Charge it

POSThttps://api.wajub.com/payments/{id}
channelstringrequired
Where to charge: a channel slug from GET /channels, such as cm.mtn or cm.orange. Send mobile or a country aggregate such as cm.mobile to let Wajub find the operator from the phone number.
data.phonestringoptional
The Mobile Money number to charge. International format is safest; a local number is read in the country of the channel.
data.operatorstringoptional
With a country aggregate such as cm.mobile only: the operator the payer picked, such as orange. Used when the number alone does not tell Wajub which operator it belongs to, a ported number for instance.
data.ibanstringoptional
For a bank channel. data.account_number is accepted instead.
data.wallet_addressstringoptional
For a crypto channel.
data.save_payment_methodbooleanoptional
Save this payment method on the customer, to offer it again next time. Only with the payer's consent. Defaults to false.

Every field under data is also accepted at the root of the body. Send an Idempotency-Key header unique to this charge, so that a retry after a timeout never charges twice.

curl https://api.wajub.com/payments/trx_test_CSUGajfv9xh0XQ5wu2lx \
  -H "Authorization: sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: charge-order-4172-1" \
  -d '{
    "channel": "cm.mtn",
    "data": { "phone": "+237670000000" }
  }'

The answer

For Mobile Money the answer is almost always 202 Accepted. The prompt is on the payer's handset, the payment has moved to processing, and message is the line to show while you wait:

Response · 202 Accepted
{
"code": 202,
"status": "Accepted",
"message": "Confirm the transaction on your phone.",
"transaction": {
"id": "trx_test_CSUGajfv9xh0XQ5wu2lx",
"reference": "order-4172",
"amount": 5000,
"currency": "XAF",
"status": "processing",
"channel": "cm.mtn",
"sandbox": true
}
}

The outcome arrives later, as a payment.succeeded or payment.failed webhook. Before you fulfil the order, retrieve the payment and read its status, as in every other mode.

CodeWhat it meansWhat to do
200The channel confirmed at once. The payment is succeededVerify, then fulfil
202Waiting for the payer, or for a next step named in actionShow message, wait for the webhook
400The payment can no longer be charged: it already succeeded, expired or was cancelledCreate a new payment if needed
402The operator or the card issuer declined. The payment is back to pendingLet the payer try again, see below
403Refused by a restriction on your account, or by Shield. A Shield block carries a BLK-… referenceDo not retry the same attempt
404No payment with this id, in this environmentCheck the id and the key
422The channel is not accepted, the number does not match the operator, or the amount is above the channel's ceilingFix the request

A 402 carries error_code, a stable reason such as insufficient_funds that you can translate for the payer, and limit_exceeded when the payment would go past one of your account's limits.

When action is set

Mobile Money needs nothing more from you: action is absent and you wait for the webhook. Two other values exist.

  • confirm_3ds means the card issuer wants to authenticate the payer. That step happens in the payment page or the components, which is one more reason to take cards there.
  • confirm means the payer has to act on their side. For crypto, crypto.deposit gives the address and the amount to send.

Letting Wajub find the operator

With channel set to mobile, or to a country aggregate such as cm.mobile, Wajub reads the operator from the number and charges on it: +23767… goes to cm.mtn. When the number cannot be attributed, a ported one for instance, Wajub uses data.operator if you sent it with a country aggregate, and answers 422 otherwise.

With a precise channel such as cm.mtn, the number has to belong to that operator. An Orange number sent on cm.mtn is refused with a 422 that names the right channel, instead of being charged on the wrong network.

Retrying after a decline

A 402 leaves the payment pending, so the same payment can be charged again: with another number, another operator, or the same one once the payer has topped up. Use a new Idempotency-Key for that new attempt. Sending the first key again returns the first answer, the 402, without trying again.

The same key is still the right one after a timeout or a network error, when you do not know whether the first call went through.

Cards

Live refuses raw card data: a card number or a security code in the body is answered with a 422 and error_code: raw_card_data_not_accepted, before anything is charged. Cards are collected by Hosted Checkout, Embedded Components or a mobile SDK, which pass them to the payment provider's own secure fields.

Sandbox accepts channel: card with the published test cards only, so you can exercise the whole flow. Any other card number is refused there too.

In sandbox

The last six digits of the phone number decide the outcome, as on the hosted page. …000000 succeeds; …000001 is refused at once with a 422 on data.phone and the payment stays pending. The full table is in Test scenarios.

The Direct API is available on every plan, in sandbox and in live. Splitting a payment into instalments, with POST /payments/{id}/splits, works in sandbox only for now.

What did you think of this content?