Sessions & security
Which key goes where, how a session token is made, and what it may do.
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
A Wajub key is a prefix, a dot, then 96 characters. The prefix alone tells you where the key is allowed to live.
| 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
Any request carrying sk. or sk_test. with an Origin or Referer header is answered 403
by the API, and the key's owner receives an alert by email. The SDK refuses it before that, with
a secret_key_in_browser error. 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.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…" \
-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 });Creating a session from the browser
Wajub(publishableKey).createPayment() exists and works, and 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.mT9xW2kQ7vB4nL6hR1cY8dF3jS5aG0eU2pA…');
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, and the runtime is what enforces it.
- It mounts the checkout, opens the overlay, or drives the field components.
- It reads the session preview through
fetchSession(sessionId), which is the same token used as a bearer credential against the checkout origin. - 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.
A terminal token produces onLoadError rather than an empty embed, which is why that callback is
worth wiring even on a page you think cannot reach it.
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 | complete, cancelled, failed, expired |
reference | The Wajub transaction reference, when available |
trxref | Your own reference, when available |
https://shop.example.com/order/complete?status=complete&reference=trx_CSUGajfv9xh0XQ5wu2lx&trxref=ORDER-123status=complete 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.
An embed never leaves your page, so callback does nothing there. Use onSuccess for the
interface and the webhook for the truth.
Multi domain shops
The SDK sends the origin of the current page to the checkout, so a single domain needs no configuration. When your checkout runs on a second domain, name the canonical one explicitly.
await mount('#checkout', { sessionId, embedOrigin: 'https://shop.example.com' });
const factory = await components(sessionId, {
componentOrigin: 'https://shop.example.com',
});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 | A hand built checkout URL is answered with an embed error |
| Confirm with a webhook | onSuccess is a browser event, and browsers close |