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.
| State | Meaning | Next action |
|---|---|---|
pending | Initialized, waiting for the customer to act. | Redirect the customer to the payment page. |
processing | The provider is processing the charge. | Wait. The payment.* webhook tells you how it ends. |
succeeded | Funds received. You can fulfill the order. | Deliver the product or service. |
failed | Declined, timed out, or never reached a provider. | Show an error, offer to retry. |
cancelled | Cancelled by the customer or merchant. | No action needed. |
expired | The payment window closed before completion. | Re-initiate if needed. |
partial | Part of the amount settled, the rest is still moving. | Keep the order open. It can still reach succeeded. |
refunded | A succeeded payment was fully refunded. | Handle the return. |
partially_refunded | A partial refund was applied. | Track the remaining amount. |
How payments move through states
- You call
POST /payments, creates a transaction inpending. - The customer completes the payment on the hosted page (
pay.wajub.com). - Wajub processes the charge with the best available provider (see Orchestration).
- Wajub sends a
payment.succeededwebhook to your server. - 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:
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.
| Sandbox | Live | |
|---|---|---|
| Keys | pk_test.… / sk_test.… | pk.… / sk.… |
| Money | No real fund movement | Real payments |
| Test data | Test phone numbers | Real customer data |
| Scope | Payments, refunds, transfers, customers, webhooks | Everything, including links, invoices, tax and Shield |
Never mix environments
A sandbox key cannot access live data, and vice versa. Store keys in separate environment variables per environment.
API keys
Three key types, each with a specific purpose:
Public key (pk.… / pk_test.…)client-safeoptionalPrivate key (sk.… / sk_test.…)server onlyoptionalRestricted key (rk.… / rk_test.…)server onlyoptionalPrivate keys are blocked in browsers
Wajub rejects any request made with a private key from a browser or mobile app (detected via
Origin/Referer headers). This is a safety net. Store sk.… in environment variables. Never in
client-side code.
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:
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:
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:
- Routing, Selects the best provider for the channel, country and amount.
- Execution, Calls the provider's API.
- Fallback, If the primary provider fails, retries with the next one.
- 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.
| Prefix | Resource | Live | Sandbox |
|---|---|---|---|
trx_ | Payment transaction | trx_CSUGajfv9xh0XQ5wu2lx | trx_test_CSUGajfv9xh0XQ5wu2lx |
po_ | Transfer (payout) | po_225KMKULRQqYvlhrgUOE | po_test_225KMKULRQqYvlhrgUOE |
rfd_ | Refund | rfd_9xh0XQ5wu2lxCSUGajfv | rfd_test_9xh0XQ5wu2lxCSUGajfv |
ben_ | Beneficiary | ben_LRQqYvlhrgUOE225KMKU | ben_test_LRQqYvlhrgUOE225KMKU |
dsp_ | Dispute | dsp_hrgUOE225KMKULRQqYvlh | dsp_test_hrgUOE225KMKULRQqYvlh |
pm_ | Payment method | pm_sAaim5apjocIgtlhzJY3x | pm_test_sAaim5apjocIgtlhzJY3x |
cus_ | Customer | cus_sAaim5apjocIgtlhzJY3wQ8s | cus_test_sAaim5apjocIgtlhzJY3wQ8s |
evt_ | Webhook event | evt_aio5DpN577tNU2vOxdmuZGhT | evt_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 theidfield is prefixedtrx_.
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
- Quickstart, code your first payment.
- Orchestration, how routing and fallback work.
- Webhooks, receive
payment.succeededreliably.