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 |
A key is a prefix, a dot, then 96 characters. Server SDKs take the secret one.
A secret key never reaches a browser
The API blocks any sk. request that arrives with a browser Origin and emails the key 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
Every server SDK reads WAJUB_API_KEY and WAJUB_WEBHOOK_SECRET from the environment on its own,
so a .env file is usually the whole configuration step.
WAJUB_API_KEY=sk_test.kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0eU3i…
WAJUB_WEBHOOK_SECRET=whsec_test_4f8c2b91d7e6a0f35c1dB7nY4hC6dF9jNode.js is the one exception. It has no environment fallback, so you pass the key explicitly, as the example below does.
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 | 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.1.1 | Java 17 |
| C# | dotnet add package Wajub | .NET 8 |
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 and jitter. Node.js does three |
| 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 |
Two spellings differ from that list. PHP suffixes its subclasses with Exception, so the row above
reads InvalidRequestException, while the base stays WajubError. Go capitalizes the initialism,
so its connection class is APIConnectionError.
Node.js names these differently
Six of the seven SDKs use the table above verbatim. @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.
Ruby, Java and C#
The three of them do exactly what the five above do, with the same eighteen resources and the same call shape. Their pages carry the idiomatic version of every example here.
The rest of the first call
Where to go next
Related pages
- WebhooksThe event that actually confirms a payment.
- Test scenariosCards and numbers that produce each outcome.
- Hosted 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.