Abia State Government Services Portal
Home · Developers

Authentication & limits

A key is a server credential: no cookie, no session, no CSRF, HTTPS only. Everything on this page is enforced before a controller is reached.

The credential

Authorization: Bearer ak_sbx_4K21PQ7M.0tS3xh_9Yb4Q7pLmE1nRfWc2vJ8dKz6aTgUxHqNsB0
  • The key id is the public half — ak_live_… or ak_sbx_… — and identifies the client in every log.
  • The secret is 256 random bits, base64url. It is shown once, when the key is issued, and stored only as a peppered hash: nobody at BIR can read it back to you.
  • The two are joined by a single dot. Send them in the Authorization header and nowhere else — never a query string, never a form field.
  • HTTPS only. A plaintext call in production is refused with TLS_REQUIRED before the key is even looked at.
  • No cookie is set and no session is started for /api/ paths, so CSRF does not apply and there is nothing for a browser to replay.
  • No CORS headers are sent. The caller is a server; a browser origin calling this API is a misuse, not a configuration problem.

Scopes

A key carries the scopes its agency asked for. A call outside them is refused with SCOPE_MISSING naming the scope it needed — ask for it rather than working around it.

ScopeGrants
verifyResolve a party and read a compliance position.
transactCreate and progress transactions, parties, contractors and contracts.
payCite payments and advise collections.
documentsIssue and read final documents and their evidence.
pvRaise and read payment vouchers.
webhooksManage webhook subscriptions.
dwPull the anonymised data-warehouse extracts. Live keys only.

A client is also bound to one agency. Every call is filtered by it: another agency's transaction, document or contract reads as not found, because an integrator may not enumerate work that is not theirs. An optional IP allowlist narrows it further — calls from anywhere else are refused IP_NOT_ALLOWED.

Request signing (optional, per client)

A client that handles money can be configured to sign every state-changing call. The signature binds timestamp, method, path and body together, so a captured request cannot be replayed, redirected to another endpoint or altered in flight.

Headers and the string that is signed
X-Timestamp: 1789123964
X-Signature: v1=<hex hmac_sha256>

signing string = ts + "\n" + METHOD + "\n" + path + "\n" + sha256(raw body)
signature      = "v1=" + hex(hmac_sha256(secret, signing string))
Signing in PHP
<?php
$ts   = time();
$body = json_encode($payload, JSON_UNESCAPED_SLASHES);
$sig  = 'v1=' . hash_hmac(
    'sha256',
    $ts . "\n" . 'POST' . "\n" . '/api/v1/transactions' . "\n" . hash('sha256', $body),
    $secret
);
  • The path is the request path exactly as sent, without the query string.
  • The body is the raw bytes you transmit. Sign what you send, not what you meant to send.
  • Clock skew beyond ±300 seconds is refused with SIGNATURE_SKEW, and the refusal carries the server's own time so your client can correct itself.
  • A timestamp, body and key that have already been seen are refused as a replay.

Rate limits

Limits are per client, not per address, and they apply to reads as well as writes. The default is 120 requests a minute; a daily quota can be set alongside it. Ask for more if your volumes need more — a limit that forces an integrator to spread work across keys is a limit that has stopped measuring anything.

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117

HTTP 429 + Retry-After: 60
{ "ok": false, "error": { "code": "RATE_LIMITED", "retryable": true } }

Back off on Retry-After. Do not retry in a tight loop: the per-address limiter in front of the key limiter exists to blunt key guessing, and a hammering client is indistinguishable from one.

Idempotency

Every call that can MINT something — a transaction, a party, a document number, a contract, a voucher, a subscription — requires an Idempotency-Key header: up to 64 characters of A–Z a–z 0–9 _ - : ., unique to the operation you are performing. Use your own case reference — it is the one value you can reproduce after a timeout.

A lookup that is a POST only so an identifier stays out of the URL — /parties/verify, /documents/verify, /contracts/lookup — creates nothing a retry could duplicate and takes no key. The endpoint reference marks which is which.

You sendThe platform does
A new keyClaims it and processes the call. The claim is an insert against a unique index, so two concurrent retries can never both proceed.
The same key, the same bodyReplays the stored answer byte for byte, with Idempotent-Replayed: true. Nothing happens twice.
The same key, a different bodyRefused: IDEMPOTENCY_KEY_REUSED. A key means one intention.
The same key while the first is still runningIDEMPOTENCY_IN_PROGRESS with Retry-After. Wait and ask again.
The same key more than 24 hours laterIDEMPOTENCY_EXPIRED: the answer was completed and its body has been purged. Fetch the resource instead of retrying.
No key at allIDEMPOTENCY_KEY_REQUIRED. There is no unguarded write path.

A timeout is not a failure. Retry with the same key: either the first attempt landed and you get its answer, or it did not and the retry does the work.

Rotation and revocation

  • Rotating issues a new key and leaves the old one valid for a seven-day overlap. Deploy the new secret, watch your logs, and let the old one expire.
  • Revoking takes effect on the next request. There is no cache to wait out and no grace period — that is the point of a revocation.
  • Suspending a client stops every key it holds at once, with CLIENT_SUSPENDED.
  • If a secret is exposed, say so and have it revoked. A rotated key costs an afternoon; a leaked one that keeps working costs a register.