Webhooks
Subscribe once and the platform tells you when a gate moves, a payment lands, a document issues or a voucher changes state — signed, retried, and never delivered to a private address.
Subscribe
One call registers an endpoint and returns the signing secret — once. Store it before you close the response.
curl -s https://services.abiastate.gov.ng/api/v1/webhooks \
-H 'Authorization: Bearer ak_sbx_4K21PQ7M.0tS3-…' \
-H 'Idempotency-Key: hooks-2026-09-14' \
-H 'Content-Type: application/json' \
-d '{
"url": "https://lands.example.gov.ng/hooks/abia",
"events": ["transaction.gate_changed", "document.issued"]
}' The URL must be https, on a public host, and must not redirect. A loopback address, a private range, a link-local address or a name that resolves to one is refused outright — a webhook that can be pointed at the inside of a network is a server-side request forgery with a nice name.
What a delivery looks like
POST /hooks/abia HTTP/1.1
Host: lands.example.gov.ng
Content-Type: application/json
User-Agent: ABSG-Webhooks/1.0
X-Abia-Event: document.issued
X-Abia-Event-Id: EVT-2026-9K41XA
X-Abia-Delivery-Id: 4417
X-Abia-Signature: t=1789123964,v1=9c1b…
{
"id": "EVT-2026-9K41XA",
"event": "document.issued",
"created_at": "2026-09-14T10:02:11+01:00",
"env": "sandbox",
"mda": "20008001",
"data": {
"document_number": "AB/LND/COFO/2026/4F2K7C/TCC-2026-004418",
"short_number": "AB-LND26-4F2K7C",
"transaction_ref": "BTX-2026-004871",
"tcc_ref": "TCC/2026/004418",
"issued_at": "2026-09-14T10:02:11+01:00",
"verification_id": "VRF-2026-9F3C1D7A4B2E"
}
} - Answer 2xx as soon as you have stored it. Anything else is a failure and will be retried.
- Answer within 10 seconds. Do the work afterwards, not before you reply.
- At least once, not exactly once. Deduplicate on X-Abia-Delivery-Id — one delivery to one endpoint. X-Abia-Event-Id, which is the body's id, names the event itself, and several endpoints may each be told about it.
- Order is not guaranteed. Treat the payload as the current fact and ignore an event older than what you hold.
- A webhook is a nudge, not a record. Confirm anything that matters by reading the endpoint it names.
- The fields under data differ by event and are additive. Read the ones you need and ignore the rest; a client that rejects an unknown field breaks on an improvement.
Verify the signature
The signature covers the timestamp and the exact body bytes. Compare in constant time, and reject a timestamp far from your own clock — otherwise a captured delivery can be replayed at leisure.
<?php
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_ABIA_SIGNATURE'] ?? ''; // t=…,v1=…
parse_str(str_replace(',', '&', $header), $parts);
$ts = (int)($parts['t'] ?? 0);
$sig = (string)($parts['v1'] ?? '');
if (abs(time() - $ts) > 300) {
http_response_code(400); // stale: refuse, do not process
exit;
}
$expected = hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit;
}
http_response_code(200); // acknowledge FIRST, then do the workRetries
A failed delivery is retried on a widening schedule, up to 8 attempts, and then marked dead.
| Attempt | When |
|---|---|
| 1 | Immediately. |
| 2 | 1 minute later. |
| 3 | 5 minutes. |
| 4 | 15 minutes. |
| 5 | 1 hour. |
| 6 | 6 hours. |
| 7 and 8 | 24 hours apart, then the delivery is dead and will not be sent again. |
Fifty consecutive failures pause the subscription and notify BIR. Fix the endpoint, then resume it yourself with POST /webhooks/{id}/enable — the platform will not quietly keep hammering a host that has stopped answering. Deliveries that died meanwhile are resent one at a time with POST /webhooks/{id}/redeliver, naming the delivery_id that GET /webhooks/{id} reports.
Events
| Event | Sent when |
|---|---|
| party.registered | A party was registered and given an ABSSIN. |
| transaction.gate_changed | A gate on a transaction moved; the payload carries the new blocking set. |
| assessment.raised | An assessment was raised, with its bill reference and payment link. |
| assessment.approved | An assessment above the automatic ceiling was approved by an officer. |
| payment.confirmed | A cited payment was matched and settled in the central ledger. |
| document.issued | A final document number was minted. |
| document.voided | A final document was voided, with the reason. |
| contract.awarded | A contract passed its gates and was awarded; the award number is the document number. |
| pv.status_changed | A payment voucher moved through the approval chain — raised, validated, ticked, approved, warranted. |
| pv.paid | A payment voucher was paid, with its payment code and the verification id behind it (FR-11.5). |
| queue.processed | A request accepted in degraded mode was finally processed. |
| api.deprecation_notice | An endpoint your client uses has a sunset date. |
| sandbox.reset | The sandbox was reseeded; your test references are gone. |
Subscribe only to what you handle. An event you did not ask for is never sent, and an unknown name in a subscription is a validation error rather than a silent no-op.