SDK Quickstart
A key, an install line, one call, and a payment page you can open.
Get a key, install one package and make one call: what comes back is a Wajub payment page you can open, and a payment you then confirm on your server.
1. Get a key
Keys live in the Dashboard, under Settings, then Developers, then API keys. Every account has two sets from the first day, and the prefix is what tells them apart.
| Prefix | Where it belongs | What it can do |
|---|---|---|
sk_test. | Your server, sandbox | Everything, on test money |
sk. | Your server, live | Everything, on real money |
pk_test. / pk. | The browser | Read a session, nothing else |
rk_test. / rk. | A script or a CI job | Only the scopes you grant it |
Server SDKs take the secret key.
A secret key never reaches a browser
A secret key must never be used from a browser. When Wajub detects one, the API refuses it and emails the key's owner. If a key has ever been in client-side code, roll it. The browser gets a session token instead, which is covered on Sessions and security.
2. Store it
Python, Ruby, Go, Java and C# read WAJUB_API_KEY from the environment, and every server SDK reads
WAJUB_WEBHOOK_SECRET, so a .env file is usually the whole configuration step.
WAJUB_API_KEY=sk_test.xxxxxxxxxxxxxxxx…
WAJUB_WEBHOOK_SECRET=whsec_test_xxxxxxxxxxxxxxxxNode.js and PHP are the exceptions for the key: they do not read WAJUB_API_KEY, so you pass it
explicitly, as the examples below do.
3. Install
One package per language, from the registry you already use.
| Language | Install | Needs at least |
|---|---|---|
| Node.js | npm install @wajub/node | Node.js 18 |
| Python | pip install "wajub>=1.2.1" | Python 3.10 |
| PHP | composer require wajub/wajub-php | PHP 8.4 |
| Go | go get github.com/wajubhq/wajub-go | Go 1.22 |
| Ruby | gem install wajub | Ruby 3.1 |
| Java | com.wajub:wajub-java:1.2.1 | Java 17 |
| C# | dotnet add package Wajub | .NET 8 |
The API version is pinned on your account, not in the SDK
Upgrading an SDK never changes the API version you get: your account is pinned at creation. To
try another version on one request, send the X-Wajub-Version header yourself. Details on
Versioning.
4. Create a payment
This is the call. An amount, a currency, a way to reach the customer, and a URL to come back to.
curl https://api.wajub.com/payments \
-H "Authorization: $WAJUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 25000,
"currency": "XAF",
"email": "amina@example.com",
"description": "Order 4172",
"reference": "order-4172",
"callback": "https://shop.example.com/complete"
}'The amount is in the major unit, not in cents
25000 with XAF is twenty-five thousand francs, not two hundred and fifty. Wajub takes the
currency's ordinary unit everywhere, so a decimal currency is written as a decimal:
"amount": 12.50 with GHS. Every limit is expressed the same way, and XAF runs from 25 to
2 000 000.
Only amount and currency are truly required. Beyond those you need one way to identify the
payer, which is email, phone or customer_id, any one of the three. callback is what makes
the redirect flow work, and reference is your own order number coming back on every webhook.
5. Read what came back
You get one object with the transaction fields and the two authorization fields together.
| Field | What you do with it |
|---|---|
authorization_url | Send the customer here, or open it yourself to test |
authorization_token | The session token, if you embed the checkout instead of redirecting |
id | Your handle on the payment, for retrieve and for refunds. trx_test_ in sandbox, trx_ live |
reference | Yours, echoed back on the object and on every webhook |
status | pending on creation. It is not the final word |
Open authorization_url in a browser now. That page is the checkout, already carrying your
account branding, and the sandbox accepts the
test cards and numbers.
6. Confirm it server side
The redirect brings the customer back to your callback URL with a status parameter. Use it to
pick the page you show them, never to release the goods.
curl https://api.wajub.com/payments/trx_test_8kQ2mW9vB4nL6hR1cY3d \
-H "Authorization: $WAJUB_API_KEY"The browser is not a reliable reporter. A customer can close the tab on a successful payment, and a Mobile Money confirmation can land minutes after the redirect. The webhook is the event that is guaranteed to arrive, so fulfilment belongs there and this call is the fallback.
When the call fails
Every SDK raises a typed error rather than returning a status. The class is the decision: retry, fix the request, or tell the payer something.
| HTTP | Class | What it means |
|---|---|---|
| 401 | AuthenticationError | Wrong key, or a live key against sandbox data |
| 403 | PermissionError | The key is valid but lacks the scope |
| 404 | NotFoundError | No such payment, customer or account |
| 400, 422 | InvalidRequestError | Malformed request. errors names the field |
| 429 | RateLimitError | Too many requests. retry_after carries the wait |
| other | WajubError | The base class, also the one to catch broadly |
| none | ApiConnectionError | Network or timeout, no response at all |
Three spellings differ from that list. PHP suffixes its subclasses with Exception, so the row
above reads InvalidRequestException, while the base stays WajubError. Java suffixes every class
with Exception, the base included: WajubException, InvalidRequestException,
ApiConnectionException, all unchecked. Go capitalizes the initialism, so its connection class is
APIConnectionError.
Node.js names these differently
@wajub/node prefixes every class with Wajub, folds 403 into WajubAuthenticationError and 404 into WajubInvalidRequestError, and
adds two of its own: WajubPaymentError for 402 and WajubApiError as the catch-all. Its
connection class is WajubConnectionError. Catch WajubError and you cover all of them.
Whatever the class, four fields are always there: the message, a code, the HTTP status, and an
errors map keyed by field name. That last one is what you show back on a form.
import { WajubError, WajubInvalidRequestError, WajubRateLimitError } from '@wajub/node';
try {
await wajub.payments.create({ amount, currency: 'XAF', email });
} catch (error) {
if (error instanceof WajubInvalidRequestError) {
return res.status(422).json({ fields: error.errors });
}
if (error instanceof WajubRateLimitError) {
return res.status(503).set('Retry-After', String(error.retryAfter ?? 5)).end();
}
if (error instanceof WajubError) {
logger.error({ code: error.code, status: error.httpStatus });
}
throw error;
}In PHP the code is on errorCode, not getCode
Read the error code from $e->errorCode, next to $e->httpStatus, $e->errors and $e->raw.
getCode() does not carry it.
Where to go next
Related pages
- Quickstart, end to endPay in sandbox, verify on your server, and receive the webhook.
- WebhooksThe event that actually confirms a payment.
- Test scenariosCards and numbers that produce each outcome.
- Display the checkoutEmbed the page instead of redirecting to it.
- Integration pathsRedirect, embed or your own form. Which and why.
- IdempotencyWhat an idempotency key protects, and what it does not.
- Choose your SDKRuntime floors and the constraints that rule one out.