Your POS microservice authenticates with a sk-nija-partner- key bound to ONE
distributor and mints / activates vouchers on demand. Mint is idempotent on
(distributor, saleRef); the plaintext PIN is returned once on a fresh mint
and is never logged or re-derivable. The full machine-readable contract is the
OpenAPI 3.1 spec.
Base URL & auth
| Environment | Base URL |
|---|---|
| Production | https://nija.africa |
| Staging | https://staging.nija.africa |
Every request carries your partner key:
Authorization: Bearer sk-nija-partner-XXXXXXXXXXXXXXXXXXXXXXXX
1 · Mint a voucher
Supply a denomination by denominationId, a named preset
{currency, faceValue} (ZAR 20 / 50 / 100), or an open amount inside the
per-currency band (ZAR floor R20, ceiling R2000). A fresh mint returns
201 with the PIN; a retry of the same saleRef returns
200 with duplicate: true and no new PIN.
curl -sS https://nija.africa/api/v1/partner/vouchers/mint \
-H "Authorization: Bearer $NIJA_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{"saleRef":"POS-2026-0001","currency":"ZAR","faceValue":50}'
# 201 Created
# {"serial":"NJ-SGXEZKSJ4E","pin":"0908422618766694","duplicate":false,
# "denomination":{"id":2,"currency":"ZAR","face_value":"50.000000"}}
2 · Activate on sale
Flip the voucher to active once the sale is confirmed (by serial or PIN):
curl -sS https://nija.africa/api/v1/partner/vouchers/activate \
-H "Authorization: Bearer $NIJA_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{"serial":"NJ-SGXEZKSJ4E"}'
# 200 OK → {"ok":true}
3 · Cancel an abandoned sale
Void a generated or active voucher when the sale falls away (a till
timeout, a cancelled transaction). Reference it by serial or by your own
saleRef. Idempotent: a repeat returns already_cancelled. Needs the
voucher:activate scope. A cancelled sale drops out of your settlement basis.
curl -sS https://nija.africa/api/v1/partner/vouchers/cancel \
-H "Authorization: Bearer $NIJA_PARTNER_KEY" \
-H "Content-Type: application/json" \
-d '{"saleRef":"POS-2026-0001"}'
# 200 OK → {"ok":true,"outcome":"cancelled","serial":"NJ-SGXEZKSJ4E"}
# A redeemed / expired voucher, or one in a settled statement → 409 (use a credit note).
4 · Look a sale up
Read one voucher's lifecycle state, by serial or by the saleRef on the customer's
receipt. The saleRef form is what answers "what happened to this sale?" after a
mint whose response you never received: the serial is exactly what a POS loses on a timeout.
Neither read ever returns PIN material, and both are scoped to your own distributor.
curl -sS -H "Authorization: Bearer $NIJA_PARTNER_KEY" \
https://nija.africa/api/v1/partner/sales/POS-2026-0001
# 200 OK
# {"serial":"NJ-SGXEZKSJ4E","status":"active","denomination":{"currency":"ZAR","face_value":"50.000000"},
# "activated_at":"2026-09-01T10:22:31.000Z","redeemed_at":null,"cancelled_at":null,
# "cancellable":true,"expires_at":"2029-08-31T21:59:59.000Z","created_at":"2026-09-01T10:21:04.000Z"}
# By serial instead:
# GET /api/v1/partner/vouchers/NJ-SGXEZKSJ4E/status
cancellable answers whether a void would still be accepted on lifecycle grounds,
without your having to attempt one. It does not cover the settled-period rule, so a
true can still meet a 409 at cancel time.
5 · Reconcile your period
Your own sold / redeemed / expired figures, the commission deducted and the net due to you, over a window. The distributor is taken from your key, never from a parameter, and the numbers come from the same engine that produces our settlement statement, so the two cannot disagree. Money is a decimal string: parse it with a decimal type, never a float.
curl -sS -H "Authorization: Bearer $NIJA_PARTNER_KEY" \
"https://nija.africa/api/v1/partner/settlement?from=2026-08-01&to=2026-08-31"
# 200 OK
# {"distributor_id":1,"period":{"from":"2026-08-01","to":"2026-08-31"},"currency":"ZAR",
# "sold_count":412,"sold_local":"24150.000000","redeemed_count":388,"redeemed_local":"22700.000000",
# "expired_count":3,"commission_local":"1932.000000","net_due_local":"22218.000000"}
6 · Settlement webhook
Period settlement (the commission Nija deducts at your agreed rate, plus outstanding float) is
reconciled by Nija, which then settles you the net. Your platform POSTs a signed callback
to /api/webhooks/stellr, authenticated by HMAC-SHA512 over the raw body plus a
timestamp replay guard:
| Header | Value |
|---|---|
x-stellr-sig | hex( HMAC_SHA512(rawBody, STELLR_SETTLEMENT_SECRET) ) |
x-stellr-timestamp | unix seconds (must be within 300s) |
BODY='{"event":"settlement","saleRef":"POS-2026-0001"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha512 -hmac "$STELLR_SETTLEMENT_SECRET" -hex | sed 's/^.* //')
curl -sS https://nija.africa/api/webhooks/stellr \
-H "Content-Type: application/json" \
-H "x-stellr-sig: $SIG" \
-H "x-stellr-timestamp: $(date +%s)" \
-d "$BODY"
# 200 OK → {"ok":true,"saleRef":"POS-2026-0001"}
# A bad or stale signature → 401 (rejected).
saleRef (a retried sale yields ONE
voucher). saleRef is case-sensitive: references differing only in letter case are two
different sales. The PIN is shown once: store it on the receipt, never log it. Below-floor or
over-ceiling amounts are rejected 400. Settlement is non-repudiable (HMAC + replay
guard).
Errors
| Status | Meaning |
|---|---|
400 | Bad request (missing saleRef, below-min / over-ceiling faceValue, malformed settlement window). |
401 | Missing / invalid / disabled / expired partner key (or bad webhook signature). |
403 | Key lacks the required scope. |
404 | No such voucher for your distributor. Unknown and someone else's answer alike. |
409 | Voucher redeemed / expired / in a settled statement, or a settlement window spanning two currencies. |
429 | Rate limited (default 300 req / 60s per caller). Wait Retry-After seconds and retry the same call. |
/api/v1/partner/* call is metered, so a busy till will
meet one. The response carries Retry-After in whole seconds. Sleep it and retry
rather than looping. No operation has a side effect on a refusal, and mint stays idempotent on
saleRef, so a retry is always safe.