Sessions & security
Which key goes where, how a session token is made, what it may do, and where the checkout may be embedded.
Everything on the browser side of Wajub runs on one short lived string: the session token. This page covers where it comes from, what it replaces, and why the key that creates it must never leave your server.
Three credentials, three places
The prefix of a Wajub key tells you where the key is allowed to live. Live keys start with sk. or
pk., sandbox keys with sk_test. or pk_test..
| Credential | Looks like | Lives in | Can do |
|---|---|---|---|
| Publishable key | pk. or pk_test. | Browser, your HTML | Create a session, nothing else |
| Secret key | sk. or sk_test. | Server, environment variable | Everything on the API |
| Session token | authorization_token | Browser, as sessionId | Pay one payment, once |
The session token is what makes the other two unnecessary in the browser. It is scoped to a single payment, it carries no account access, and it dies with that payment.
A secret key in a browser is refused and reported
A secret key must never be used from a browser. Wajub.js refuses one with a
secret_key_in_browser error, and the API refuses one with 403 and emails the key's owner.
Neither is a safety net you should ever reach.
The flow that is correct
Two steps, in this order, every time.
- Your server calls
POST /paymentswith the secret key and gets back anauthorization_token. - Your browser receives that token, and nothing else, and passes it as
sessionId.
curl https://api.wajub.com/payments \
-H "Authorization: sk_test.xxxxxxxxxxxxxxxx…" \
-H "Content-Type: application/json" \
-d '{
"amount": 35000,
"currency": "XOF",
"reference": "ORDER-123",
"customer": { "email": "buyer@example.com" }
}'The response carries three things worth keeping.
| Field | What it is for |
|---|---|
authorization_token | The browser's sessionId. Send this one down |
authorization_url | The hosted page, for a redirect checkout |
transaction.id | Your reconciliation key when the webhook lands |
const { sessionId } = await fetch('/api/checkout/session', { method: 'POST' })
.then((r) => r.json());
await mount('#checkout', { sessionId });Before the order exists
Payment fields shown while the buyer is still on the checkout come before the order. Create their
session with deferred: true: the payment is only prepared, stays out of your payments and sends
no webhook until your server activates it with the final total and the customer.
Before the order exists walks
through the three calls.
Creating a session from the browser
Wajub(publishableKey).createPayment() creates the session from the page, with a publishable key,
sandbox or live. It is the right tool for a prototype or a demo page with a fixed price. It is the
wrong tool for a shop, because the amount is then decided by code the customer controls.
const client = Wajub('pk_test.xxxxxxxxxxxxxxxx…');
const { sessionId } = await client.createPayment({
amount: 35000,
currency: 'XOF',
customer: { email: 'buyer@example.com' },
});
await client.mount('#checkout');The price is part of the trust boundary
A publishable key cannot read your account, but it can open a payment for any amount the page asks for. Anything a customer could profit from changing belongs on your server.
What the token may do
The session token authorises exactly one payment.
- It mounts the checkout, opens the overlay, or drives the field components.
- It reads the session preview through
fetchSession(sessionId). - On a prepared payment, it shows the ways to pay and charges nothing until your server activates the payment. Activation keeps the token.
- It stays valid while the payment is not terminal, and stops shortly after
succeeded,failed,expiredorcancelled. - It survives a remount, so a component that unmounts and comes back reuses the same token.
- It lives as long as the payment: 24 hours by default, or the
expires.inminutes you set onPOST /payments(5 to 43 200). Past that the payment expires,onExpiredfires on the checkout, and a payment field rejects withsession_expired. - Payment attempts on one session are rate limited. Past the limit, the attempt is refused with
rate_limiteduntil a short wait has passed.
Mounting a token that has stopped calls onLoadError with session_terminal_expired, and a token
Wajub does not know calls it with session_not_found. The embed itself stays empty, which is why
that callback is worth wiring even on a page you think cannot reach it. Until the token stops, a
checkout mounted on a finished payment reports the outcome instead, through onSuccess, onError,
onExpired or onCancel.
The callback URL
callback is a redirect destination, so it only matters for the redirect flow. Pass a plain HTTPS
base URL with no query string of your own. Wajub appends its own.
| Query param | Values |
|---|---|
status | succeeded, cancelled, failed, expired |
reference | Wajub's id of the payment, the one GET /payments/{id} takes, when available |
trxref | Your own reference, when available |
Every return carries reference and trxref, after a failed payment too.
https://shop.example.com/order/complete?status=succeeded&reference=trx_CSUGajfv9xh0XQ5wu2lx&trxref=ORDER-123status=succeeded is a hint, not a receipt
It is a query string on a URL the payer can edit. Ship the order when payment.succeeded
arrives on your endpoint, or when a server side
GET /payments/{id} says so.
The hosted embed never leaves your page, so callback does nothing there. Use onSuccess for the
interface and the webhook for the truth. Payment fields are the exception: a payment that continues
on the provider's own page, a crypto payment for example, takes your page there, and the payer comes
back to callback, with the parameters above or, depending on the method, without them.
When the payment continues on the provider's page
says what that page must do.
Where the checkout may be embedded
Settings → Checkout embedding holds the sites allowed to embed your checkout: up to 20 origins,
each written https://domain, with an optional port and no path. The page is on every plan, Pay as
you go included, and the list applies in live and in sandbox. Once you fill it, the browser shows
your checkout and your payment fields only inside a page served from one of those origins.
On any other origin the frame stays blank, and neither onReady nor onLoadError fires. Add every
origin that mounts the checkout or a field, staging included, before you fill the list.
Declare your origins before you go live
Declare the sites allowed to embed your checkout in Settings → Checkout embedding before you take live payments.
Multi domain shops
A shop on several domains needs no option in the code: the checkout and the fields work on the page that mounts them. Add each domain to Settings → Checkout embedding.
Before you go live
| Rule | Why |
|---|---|
HTTPS on your site and on callback | Payment iframes and redirects refuse plain HTTP |
sk. only in server environment variables | Never in a bundle, never in a PUBLIC_ variable |
Only authorization_token reaches the browser | It is the one credential with nothing to lose |
| Mount through the SDK | The checkout only runs when the SDK mounts it |
| Confirm with a webhook | onSuccess is a browser event, and browsers close |