Skip to content

Key Concepts

Payment lifecycle, environments, session tokens, keys and the orchestration model.

Before writing code, take a few minutes to understand the core concepts. They'll make every API call and webhook event immediately clear.

Payment lifecycle

A payment is a conversation between three parties: your server, Wajub, and the person paying. Every state change comes from one of them acting, so the fastest way to learn the lifecycle is to watch who does what, in order.

The path a successful payment takes

Six movements, and only two of them are yours. You open the payment, you redirect, then you wait. Wajub picks a provider, collects the money and tells you how it ended.

When it does not succeed

The same three parties, the same order, but the payment leaves the happy path. Each exit produces its own event, so your endpoint learns about it exactly the way it learns about a success.

Four states end the attempt: succeeded, failed, cancelled and expired. Nothing the customer does moves a payment out of them. pending, processing and partial are still in flight, so treat them as open, never as a failure. Refunds are the one thing that keeps moving after the end: a succeeded payment can still become refunded or partially_refunded days later.

StateMeaningNext action
pendingInitialized, waiting for the customer to act.Redirect the customer to the payment page.
processingThe provider is processing the charge.Wait. The payment.* webhook tells you how it ends.
succeededFunds received. You can fulfill the order.Deliver the product or service.
failedDeclined, timed out, or never reached a provider.Show an error, offer to retry.
cancelledCancelled by the customer or merchant.No action needed.
expiredThe payment window closed before completion.Re-initiate if needed.
partialPart of the amount settled, the rest is still moving.Keep the order open. It can still reach succeeded.
refundedA succeeded payment was fully refunded.Handle the return.
partially_refundedA partial refund was applied.Track the remaining amount.

How payments move through states

  1. You call POST /payments, creates a transaction in pending.
  2. The customer completes the payment on the hosted page (pay.wajub.com).
  3. Wajub processes the charge with the best available provider (see Orchestration).
  4. Wajub sends a payment.succeeded webhook to your server.
  5. You verify the status via GET /payments/{id} and fulfill.

Step 4 is where the state machine reaches you. Every transition Wajub records emits an event, and an event delivered to your endpoint looks like this. The type is in event, and data carries the payment exactly as GET /payments/{id} would return it:

payment.succeeded
{
"id": "evt_test_aio5DpN577tNU2vOxdmu",
"event": "payment.succeeded",
"data": {
"id": "trx_test_CSUGajfv9xh0XQ5wu2lx",
"amount": 5000,
"amount_paid": 5000,
"currency": "XAF",
"status": "succeeded",
"channel": "cm.mtn",
"customer": {
"id": "cus_test_sAaim5apjocIgtlhzJY3",
"email": "amina@example.com"
},
"sandbox": true,
"created_at": "2026-09-11T10:24:00Z"
},
"livemode": false,
"pending_webhooks": 1,
"api_version": "2026-09-01",
"request": {
"id": null,
"idempotency_key": null
},
"created": "2026-09-11T10:26:13Z"
}

Sandbox vs Live

Wajub provides two completely isolated environments. A single base URL serves both. The environment is determined by the API key you use.

SandboxLive
Keyspk_test.… / sk_test.…pk.… / sk.…
MoneyNo real fund movementReal payments
Test dataTest phone numbersReal customer data
ScopePayments, refunds, transfers, customers, webhooksEverything, including links, invoices, tax and Shield

API keys

Three key types, each with a specific purpose:

Public key (pk.… / pk_test.…)client-safeoptional
Initialize payments from the browser or mobile app. Cannot access sensitive operations.
Private key (sk.… / sk_test.…)server onlyoptional
Full API access, payments, transfers, refunds, balance. Must never leave your backend.
Restricted key (rk.… / rk_test.…)server onlyoptional
Scoped to specific resources (e.g. payment.read). Ideal for third-party integrations or least-privilege services.

Session tokens

The hosted checkout page (pay.wajub.com) runs in the customer's browser, so it cannot hold an API key. It uses a session token instead, returned by POST /payments as authorization_token and already embedded in the authorization_url you redirect to. The token is scoped to one payment, it can do nothing but pay that payment, and it is safe to hand to a browser.

Its lifetime follows the payment. The payment window is 24 hours by default, adjustable from 5 minutes to 30 days with expires.in. Once the payment reaches an end state the token keeps working for a short grace period, 5 minutes after a success so the payer can reload their receipt, and 60 seconds after a failure, a cancellation or an expiry so a checkout tab still open can discover the outcome. After that it is dead.

You only handle the token yourself when you embed checkout in your own page rather than redirecting. Your server creates the payment as usual:

Server, create the payment
import { Wajub } from '@wajub/node';

const wajub = new Wajub({ apiKey: process.env.WAJUB_API_KEY! });

const payment = await wajub.payments.create({
  amount: 5000,
  currency: 'XAF',
  customer: { email: 'amina@example.com' },
});

// Send payment.authorization_token to your frontend.

Your frontend passes that token to the browser SDK, which is a different package and never sees your API key:

Browser, mount the checkout
import { mount } from '@wajub/js';

await mount('#checkout', { sessionId: authorizationToken });

See Wajub Components for the full embedded checkout.

Orchestration model

When you process a payment, Wajub doesn't just forward it. It orchestrates:

  1. Routing, Selects the best provider for the channel, country and amount.
  2. Execution, Calls the provider's API.
  3. Fallback, If the primary provider fails, retries with the next one.
  4. Reconciliation, Matches provider callbacks with the original transaction.

All of this is transparent to you. You just create a payment and listen for the webhook.

Watch orchestration in real time

Open Konsole → Event Stream to see the routing decision and provider responses as they happen.

Resource identifiers

Every resource id is a prefix, then test_ when the object lives in the sandbox, then 20 to 24 random characters depending on the resource. An id therefore tells you two things at a glance: what it is, and which environment produced it. Store ids as text with room for 40 characters.

PrefixResourceLiveSandbox
trx_Payment transactiontrx_CSUGajfv9xh0XQ5wu2lxtrx_test_CSUGajfv9xh0XQ5wu2lx
po_Transfer (payout)po_225KMKULRQqYvlhrgUOEpo_test_225KMKULRQqYvlhrgUOE
rfd_Refundrfd_9xh0XQ5wu2lxCSUGajfvrfd_test_9xh0XQ5wu2lxCSUGajfv
ben_Beneficiaryben_LRQqYvlhrgUOE225KMKUben_test_LRQqYvlhrgUOE225KMKU
dsp_Disputedsp_hrgUOE225KMKULRQqYvlhdsp_test_hrgUOE225KMKULRQqYvlh
pm_Payment methodpm_sAaim5apjocIgtlhzJY3xpm_test_sAaim5apjocIgtlhzJY3x
cus_Customercus_sAaim5apjocIgtlhzJY3wQ8scus_test_sAaim5apjocIgtlhzJY3wQ8s
evt_Webhook eventevt_aio5DpN577tNU2vOxdmuZGhTevt_test_aio5DpN577tNU2vOxdmuZGhT

Three resources follow their own rule. An invoice is always inv_ with no environment infix. A payment link has no prefix at all, its id is nine characters such as kQ7vB4nL6, because it is also the public URL people receive. A dispute message is a UUID.

Use the prefix as a guard rail

A test_ id reaching your production database is a configuration bug, not a payment. Checking the prefix before you fulfill an order catches a mixed-up API key long before your accountant does.

Amounts

Unlike many payment providers, Wajub expresses amounts in the major unit of the currency (e.g. 5000 = 5 000 XAF, not 500 000 centimes). CFA franc currencies have no common subunit, so this avoids confusion.

"Payment" vs "transaction"

Wajub uses both terms, and they refer to the same object:

  • Payment. The business concept. You create a payment, a customer pays, you fulfill.
  • Transaction. The internal representation. The API response uses "transaction": {...} as the root key, and the id field is prefixed trx_.

In practice: you call POST /payments, receive transaction.id = "trx_xxx", and listen for payment.succeeded webhooks. The terms are interchangeable in this documentation.

Next steps

What did you think of this content?