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.
This call belongs on your server
Send it with your secret key, from your backend. Never use an sk.… key from a browser: Wajub
rejects it with a 403. If your payment screen lives in a browser, use
Embedded Components instead.
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
https://api.wajub.com/payments/{id}channelstringrequiredGET /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.phonestringoptionaldata.operatorstringoptionalcm.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.ibanstringoptionaldata.account_number is accepted instead.data.wallet_addressstringoptionaldata.save_payment_methodbooleanoptionalfalse.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:
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.
| Code | What it means | What to do |
|---|---|---|
200 | The channel confirmed at once. The payment is succeeded | Verify, then fulfil |
202 | Waiting for the payer, or for a next step named in action | Show message, complete the step in action if there is one, wait for the webhook |
400 | The payment can no longer be charged: it already succeeded, expired or was cancelled | Create a new payment if needed |
402 | The operator or the card issuer declined. The payment is back to pending. Also error_code: merchant_not_verified while your account's verification is pending | Let the payer try again, see below. For merchant_not_verified, finish your verification |
403 | Refused by a restriction on your account, or by Shield. A Shield block carries error_code: shield_blocked and its BLK-… reference in block_reference | Do not retry the same attempt |
404 | No payment with this id, in this environment | Check the id and the key |
409 | A 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 yet | Wait for the outcome of the attempt in progress, by webhook or by retrieving the payment. Charge again only if it is back to pending |
422 | The 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 request | Fix 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.
action | What the payer does | What the answer carries |
|---|---|---|
redirect | Approves the payment on the provider's page or in its app | action_url, where to send the payer |
confirm_otp | Types the code the provider sent them | otp_id, and otp_masked_contact when the provider says where the code went |
confirm_pin, confirm_phone, confirm_birthday, confirm_address | Gives the provider one more detail | pin_id and pin_instruction, or the same pair named after phone, birthday or address |
client_session | Pays in a window the provider runs itself | client_session, with its id and, when the provider has a page of its own, hosted_url |
confirm | Follows the instruction in message | Nothing more in live. In sandbox, a crypto channel adds crypto.deposit |
confirm_3ds | Authenticates with the card issuer | Nothing 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.
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.
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.
action | Body of POST /payments/{id}/confirm |
|---|---|
confirm_otp | otp_id, code |
confirm_pin | pin_id, pin |
confirm_phone | phone_id, phone |
confirm_birthday | birthday_id, birthday as YYYY-MM-DD |
confirm_address | address_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:
| Field | What it holds |
|---|---|
card.number | A test card. The number decides the outcome |
card.holder | The name on the card. card_holder is accepted too |
exp | The expiry as MM/YY or MM/YYYY, a year from this one to thirty years ahead |
cvc | Three 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.
Related pages