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, which is for the hosted page. Keep authorization_token as well if you charge a channel whose provider asks the payer for a code: it is the credential that confirms that step, and it is returned at creation only.

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 cm.mobile for Cameroon, to let Wajub find the operator from the phone number. These two are the only aggregates.
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 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.xxxxxxxxxxxxxxxx…" \
  -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, complete the step in action if there is one, 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 pending. Also error_code: merchant_not_verified while your account's verification is pendingLet the payer try again, see below. For merchant_not_verified, finish your verification
403Refused by a restriction on your account, or by Shield. A Shield block carries error_code: shield_blocked and its BLK-… reference in block_referenceDo not retry the same attempt
404No payment with this id, in this environmentCheck the id and the key
409A charge is already in progress on this payment (error_code: payment_in_progress), a call with the same Idempotency-Key is still running, or the payment was prepared and its order not validated yetWait for the outcome of the attempt in progress, by webhook or by retrieving the payment. Charge again only if it is back to pending
422The channel is not accepted, the number does not match the operator, the amount is above the channel's ceiling, or the Idempotency-Key was already used for a different requestFix the request. A reused key needs a new key for a new charge

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

Most Mobile Money charges need nothing more from you: action is absent and you wait for the webhook. When the provider behind the channel wants the payer to do something first, the answer is still a 202, action names that step, and the answer carries what the step needs.

actionWhat the payer doesWhat the answer carries
redirectApproves the payment on the provider's page or in its appaction_url, where to send the payer
confirm_otpTypes the code the provider sent themotp_id, and otp_masked_contact when the provider says where the code went
confirm_pin, confirm_phone, confirm_birthday, confirm_addressGives the provider one more detailpin_id and pin_instruction, or the same pair named after phone, birthday or address
client_sessionPays in a window the provider runs itselfclient_session, with its id and, when the provider has a page of its own, hosted_url
confirmFollows the instruction in messageNothing more in live. In sandbox, a crypto channel adds crypto.deposit
confirm_3dsAuthenticates with the card issuerNothing for you to handle: that step happens in the payment page or the components, one more reason to take cards there

The step depends on the provider your account routes the channel to, so read action on every 202 instead of assuming one per channel.

A redirect

Wave works this way: the payer approves in the Wave app, not on a prompt. So do the crypto channels in live, where the provider's page shows the address and the amount to send.

Response · 202 Accepted, with a redirect
{
"code": 202,
"status": "Accepted",
"message": "Open Wave to confirm your payment.",
"action": "redirect",
"transaction": {
"id": "trx_CSUGajfv9xh0XQ5wu2lx",
"amount": 5000,
"currency": "XOF",
"status": "processing",
"channel": "ci.wave"
},
"action_url": "https://pay.wave.com/c/cos-1c2rs2wsg10q8"
}

Send the payer to action_url: a redirect in a browser, a link or a button in an app. When they are done, the provider brings them back to the callback of the payment, if you set one. The outcome still arrives by webhook, and you still verify the payment before you fulfil.

A code, or another detail

With confirm_otp the provider has sent the payer a code. Collect it on your own screen, then send it from your server to POST /payments/{id}/confirm, with your secret key, the otp_id of the answer and the code.

Confirm the code the payer received
curl https://api.wajub.com/payments/trx_CSUGajfv9xh0XQ5wu2lx/confirm \
  -H "Authorization: sk.xxxxxxxxxxxxxxxx…" \
  -H "Idempotency-Key: confirm-order-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "otp_id": "prc_2b4l4EObTOnOa501JXNi",
    "code": "482913"
  }'

The answer is the charge's. 200 when the payment is paid. 202 when the provider asks for one more step: action names it, with what it needs, as on the charge. 402 when the provider refused what was sent: error_code says why (invalid_otp, or max_attempts_exceeded once the three tries of the step are used up) and attempts_remaining counts the tries left. With none left, charge the payment again. A 422 with otp_expired means no step of this payment has that otp_id. Send an Idempotency-Key: a retried call then answers what the first one did, without sending the code to the provider twice.

The four other steps take the same call, with their own fields. Each …_instruction field of the charge's answer is the provider's own wording of what it asks for.

actionBody of POST /payments/{id}/confirm
confirm_otpotp_id, code
confirm_pinpin_id, pin
confirm_phonephone_id, phone
confirm_birthdaybirthday_id, birthday as YYYY-MM-DD
confirm_addressaddress_id, address, city, state, zip_code

Each step allows three tries, whether it is confirmed from your integration or from the payment page.

The provider's own window

With client_session the provider collects the payment itself. When client_session.hosted_url is present, send the payer to it as you would for a redirect. Once they are back, call POST /payments/{id}/confirm with client_session_id set to client_session.id: Wajub checks the result with the provider at once, without waiting for its webhook, and answers as the charge does, 202 while the provider has nothing final yet. A provider that only offers a widget inside the page, with no hosted_url, is meant for Embedded Components, which open it for you.

Letting Wajub find the operator

With channel set to mobile, or to cm.mobile for Cameroon, Wajub reads the operator from the number and charges on it: +23767… goes to cm.mtn. These two are the only aggregates: another country is charged on its operator's own channel, such as ci.orange. When the number cannot be attributed, a ported one for instance, Wajub uses data.operator if you sent it with cm.mobile, 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.

Some channels are named after a wallet, not after an operator. Wave (ci.wave, sn.wave), Express Union (cm.eu), Djamo (ci.djamo, sn.djamo) and Capitec Pay (za.capitec) are held whatever the payer's operator, so any number is accepted on them. The other wallets belong to one operator, and take a number of that operator: ke.mpesa a Safaricom number, tz.mpesa and mz.mpesa a Vodacom one, tg.tmoney a Togocel one, gh.vodafone a Telecel one. An aggregate sends such a number to the wallet. Phone numbers lists every wallet and the operator behind it.

On a payment created with accept, an aggregate is judged by the operator the number resolves to: cm.mobile with an MTN number is accepted wherever cm.mtn is.

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. A sandbox card charge takes four fields in data, each answered with a 422 on its name when it is missing or malformed:

FieldWhat it holds
card.numberA test card. The number decides the outcome
card.holderThe name on the card. card_holder is accepted too
expThe expiry as MM/YY or MM/YYYY, a year from this one to thirty years ahead
cvcThree or four digits

The two card. keys are names with a dot in them, not a nested object: "data": {"card.number": "4242424242424242", "card.holder": "Amina Diallo", "exp": "12/30", "cvc": "123"}.

In sandbox

The last six digits of the phone number decide the outcome, as on the hosted page. …000000 succeeds; …000001 is declined at once as live declines: a 402 with error_code: insufficient_funds, and the payment stays pending. The full table is in Test scenarios.

Sandbox charges no provider, so it never answers redirect, confirm_otp or client_session: those steps come from live providers. It answers Mobile Money and bank channels with no action, a 3-D Secure test card with confirm_3ds, and a crypto channel with confirm and crypto.deposit, the address and the amount to send.

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 both, on Mobile Money channels in live: Split payments describes it.

What did you think of this content?