NIJA. Partners

Partner Voucher API — Quickstart

Mint & activate Nija stored-value vouchers from your POS. Settled net of commission

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

EnvironmentBase URL
Productionhttps://nija.africa
Staginghttps://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:

HeaderValue
x-stellr-sighex( HMAC_SHA512(rawBody, STELLR_SETTLEMENT_SECRET) )
x-stellr-timestampunix 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).
Money-class rules. Mint is idempotent per 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

StatusMeaning
400Bad request (missing saleRef, below-min / over-ceiling faceValue, malformed settlement window).
401Missing / invalid / disabled / expired partner key (or bad webhook signature).
403Key lacks the required scope.
404No such voucher for your distributor. Unknown and someone else's answer alike.
409Voucher redeemed / expired / in a settled statement, or a settlement window spanning two currencies.
429Rate limited (default 300 req / 60s per caller). Wait Retry-After seconds and retry the same call.
Handle the 429. Every /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.