SDK Quickstart
A key, an install line, one call, and a payment page you can open.
The shortest path to a working payment is four steps. You create a key in the Dashboard, you put it in an environment variable, you install one package, and you make one call. What comes back is a URL. Open it and you are looking at a real Wajub payment page.
Everything else in this section builds on that call.
1. Get a key
Keys live in the Dashboard, under Settings, then Developer, 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 on their own, and every
server SDK falls back to 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 |
Python: install 1.2.1 or later
wajub 1.1.0 and 1.1.1 install nothing importable: the wheel was published without the module.
1.2.1 is the first release that carries it, which is why the line above asks for it by version.
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
The SDKs flatten the API envelope, so 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.
What the SDK is doing for you
The same behaviour in all seven, and it is the reason to use a package rather than a raw HTTP call.
| Behaviour | Detail |
|---|---|
| Idempotency | An Idempotency-Key is generated for every POST and PUT, so a replay never double charges |
| Retries | Two automatic retries on 429 and 5xx, with exponential backoff. Node.js does three. Node.js, Python, PHP and Go add jitter, Ruby, Java and C# do not |
| Never a blind replay | A POST is retried only because it carries an idempotency key |
| Timeouts | 30 seconds per request by default |
| Errors | A typed exception per class of failure, not a status code to switch on |
| Pagination | list() returns a page you can iterate, and it fetches the next one for you |
The API version is pinned on your account, not in the SDK
No SDK sends an X-Wajub-Version header. Your account is pinned at creation, new accounts get
the current version, and you override it per request by sending that header yourself. Details on
Versioning.
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
The other six follow the table above, with the spellings just described. @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
Wajub\Exception\WajubError passes only the message up to Exception, so getCode() returns
0 on every Wajub failure. The real value is the readonly property $e->errorCode, next to
$e->httpStatus, $e->errors and $e->raw.
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 the generated key protects, and what it does not.
- Choose your SDKRuntime floors and the constraints that rule one out.