Skip to content

Quickstart

Your first payment end to end, from API key to verified webhook, in about five minutes.

This guide takes you from an empty account to a verified payment with a working webhook. You'll need a Wajub account (free) and a few minutes.

1. Create your account and get your keys

Create an account on the Dashboard. A new account holds no key, so once logged in, open Settings → Developer → API Keys and create a private key while the Dashboard shows the sandbox. It comes out as sk_test.….

KeyEnvironmentWhere it belongs
pk_test.…SandboxPublic. Safe in a browser, initializes payments client-side.
sk_test.…SandboxPrivate. Server-side only, no real money moves.
pk.…ProductionPublic. Same client-side role, on real funds.
sk.…ProductionPrivate. Server-side only, moves real money.

Production keys only become available once your account is verified, and restricted rk keys are covered in Environments & API keys.

Everything in this guide runs on your server, so everything here uses the private sandbox key. The public key exists for the one case where code runs in a browser, initializing a payment from the page itself. Never use a private key from a browser: when Wajub detects one, it refuses the request and emails you, so a leaked key does not stay silent.

2. Install the SDK for your language

The SDKs wrap the same HTTP API with typed resources, an Idempotency-Key on every mutating call, a per-request timeout and webhook signature verification. If you would rather call the API directly, every example below has a cURL tab.

npm install @wajub/node

Ruby, Java, C#, Kotlin, Flutter and React Native have their own SDKs and their own guides. See SDKs & Libraries for the full list and the configuration reference.

3. Create a payment

A payment starts with one request. You send the amount, the currency, who is paying, and the page you want that person sent back to. Wajub creates the payment, keeps it pending and answers with a hosted page where the customer picks their operator and confirms. Nobody is charged yet.

Describe the payer inside the customer object. The API also accepts a top-level email, and both end up on the same customer record, but the object is the structure that 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.xxxxxxxxxxxxxxxx…" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 5000,
    "currency": "XAF",
    "customer": { "email": "[email protected]" },
    "callback": "https://shop.example.com/order/complete"
  }'

Wajub answers 201 Created with the payment, its id, and the page to send the customer to:

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",
"amount": 5000,
"amount_paid": 0,
"currency": "XAF",
"status": "pending",
"customer": {
"id": "cus_test_sAaim5apjocIgtlhzJY3wQ8s",
},
"callback": "https://shop.example.com/order/complete",
"sandbox": true,
"created_at": "2026-09-11T10:24:00+00:00"
}
}

Store transaction.id against your order, then redirect the customer to authorization_url.

Amounts are in the major unit, never in centimes

"amount": 5000 means 5 000 XAF, not 50 XAF. Wajub never uses subunits. Send 500 for 500 NGN and 10.50 for 10.50 USD. See Key Concepts → Amounts.

4. Pay it with a sandbox number

Open authorization_url in your browser. In test mode the payment page accepts fake Mobile Money numbers whose last 6 digits decide the outcome. The same suffixes work for every operator and every country:

SuffixResult
000000Payment succeeded
000001Insufficient funds
000002Failure, operator declined
000003Timeout
000004Pending → cancelled
000009Payment succeeds, a later refund on it fails

Pick a number for the operator you want to exercise:

Country and operatorPrefixExample, success
Cameroon, MTNCameroon / MTN+23767+237670000000
Cameroon, OrangeCameroon / Orange+23769+237690000000
Côte d'Ivoire, MTNCôte d'Ivoire / MTN+22505+225050000000
Côte d'Ivoire, OrangeCôte d'Ivoire / Orange+22507+225070000000
Côte d'Ivoire, WaveCôte d'Ivoire / Wave+22503+225030000000
Senegal, OrangeSenegal / Orange+22177+221770000000
Ghana, MTNGhana / MTN+23324+233240000000
Nigeria, MTNNigeria / MTN+2348+2348000000000
Kenya, M-PesaKenya / M-Pesa+25470+254700000000
Uganda, MTNUganda / MTN+25677+256770000000

The sandbox payment page asks for no PIN: the test number alone decides the outcome.

5. Verify the payment server-side

The customer lands back on your callback URL, and that return proves nothing. Anyone can open that URL by hand, and a customer who closes the tab never opens it at all. Before you ship an order, ask the API what really happened, from your server, with the id you stored in step 3.

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

The response carries the payment as Wajub knows it, whatever your browser was told:

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

Fulfill the order only when status is succeeded, and only after checking that amount and currency match what you charged for. A payment that is still pending is not a failure, it is a customer who has not finished yet, so leave the order open and let the webhook in step 6 tell you when it lands.

Track the transaction in Konsole

Open Konsole → API Logs to see the exact request/response Wajub recorded. Once you are live, Routing Log shows which provider was chosen and why. It does not exist in the sandbox.

6. Set up a webhook endpoint

Polling the API after every redirect only covers the customers who come back. Webhooks cover the rest: a payment confirmed ten minutes later on the operator's side, a refund issued from the Dashboard, a dispute opened by the customer. Set one up now, before you build anything on top.

Register the endpoint

In Konsole → Webhooks → Endpoints (or Settings → Developer → Webhooks in the Dashboard):

  1. Click Add Endpoint.
  2. Enter your public URL, for example https://yourapp.com/webhooks/wajub.
  3. Select the events to receive. Start with every payment event, using Select all beside them. A subscription matches exact names only, so typing payment.* would receive nothing.
  4. Save, then copy the Signing Secret (whsec_…) into your environment as WAJUB_WEBHOOK_SECRET.

Give the SDK the signing secret

The client you already built in step 3 also verifies webhooks, once it knows the signing secret. Add it to the same constructor, next to your API key:

import { Wajub } from '@wajub/node';

export const wajub = new Wajub({
  apiKey: process.env.WAJUB_API_KEY,
  webhookSecret: process.env.WAJUB_WEBHOOK_SECRET,
});

Write the endpoint

Four rules make a webhook endpoint correct, and the SDK only handles the first one for you.

Verify the signature, in every environment. An endpoint that skips verification accepts anything anyone posts to it, including a forged payment.succeeded. constructEvent recomputes the HMAC over X-Wajub-Timestamp and the raw body, compares it with X-Wajub-Signature, rejects anything older than five minutes, and only then parses the JSON. It needs the raw body, so disable JSON body parsing on this route.

Acknowledge before you work. Wajub waits 10 seconds for your 2xx. Past that the delivery counts as failed and gets retried, so return first and do the slow work afterwards, in a queue or a goroutine.

Deduplicate on the delivery id. A failed delivery is retried with a growing wait, for up to 72 hours, and every attempt carries the same X-Wajub-Delivery-Id. That id is what keeps a retry from shipping the same order twice.

Then act on the event. The type is in event.event, and event.data holds the object the event is about, with the same shape the API returns for it. For payment.succeeded, event.data.id is the payment id you stored in step 3.

app.post('/webhooks/wajub', express.raw({ type: '*/*' }), async (req, res) => {
  const event = wajub.webhooks.constructEvent(
    req.body,
    req.headers['x-wajub-signature'],
    req.headers['x-wajub-timestamp'],
  );

  res.sendStatus(200);

  const deliveryId = req.headers['x-wajub-delivery-id'];

  if (await alreadyHandled(deliveryId)) return;

  await markHandled(deliveryId);

  if (event.event === 'payment.succeeded') {
    await fulfillOrder(event.data.id);
  }
});

Receive events on your machine

You do not need a public URL, a deployment or a registered endpoint to start. The CLI opens an outbound connection to Wajub and posts every sandbox event to a local address:

npm install -g @wajub/cli
wajub listen --forward-to http://localhost:3000/webhooks/wajub

Nothing on your machine is exposed to the internet. wajub listen prints the secret forwarded events are signed with, and that is the secret your local WAJUB_WEBHOOK_SECRET needs while you develop. It stays the same between runs, so set it once (wajub listen --print-secret prints it alone); it is not a registered endpoint's secret, each endpoint keeps its own. See the CLI listen command for all options.

Complete webhook guide

Signature verification, retry policy, event catalog, and idempotency are all covered in the Webhooks section.

What's next?

Integration checklist

0/4
  • Sign up on the Wajub Dashboard and get your sandbox API keys.

    View guide →
  • Initialize a sandbox transaction with your public key.

    View guide →
  • Subscribe to payment.succeeded and payment.failed events.

    View guide →
  • Replace test keys with live keys and deploy to production.

    View guide →

What did you think of this content?