Abia State Government Services Portal
Home · Developers

Endpoint reference

Every endpoint calls the same service the console calls (FR-9.1). Identifier-bearing calls are POST so a national identity number never reaches a URL or an access log.

How to read this

  • Paths are relative to https://services.abiastate.gov.ng/api/v1.
  • * marks a required field. Fields are listed with the data object they sit in; everything in the response table lives under data.
  • Scope is what the key must carry. Idempotent means the call requires an Idempotency-Key header.
  • Response fields are the keys of data. Most answers nest the thing they are about under one key — data.party, data.document, data.contract, data.voucher — so read the table's first column literally.
  • The reason codes under each endpoint are the ones it decides for itself. Transport refusals — auth, scope, rate, idempotency, validation — can come back from any endpoint and are listed on the errors page.
MethodPathDoes
GET /ping Prove a key and read its limits
POST /parties/verify Resolve a taxpayer and read their compliance position
POST /parties/validate Ask whether an applicant may proceed, on a fee-less process
POST /parties Register a party the state register does not hold
GET /parties/{ref} Read a party you already hold a reference for
POST /transactions Submit a case and run the compliance gates
GET /transactions/{ref} Read a transaction and its current gate position
POST /transactions/{ref}/payments Cite a payment against a transaction
POST /transactions/{ref}/assessments Re-run the gates and read everything that is owed
POST /transactions/{ref}/document Issue the final document
POST /documents/verify Look a document up by its long number
GET /documents/{no} Read a final document from the register
GET /documents/{no}/evidence The evidence bundle behind a document
GET /public/documents/{no} Verify a document number without a key
POST /public/documents/verify Verify a long document number without a key
POST /contractors Onboard a contractor
GET /contractors/{ref} Read a contractor
POST /contracts/lookup Call a contract up by its full award number
POST /contracts Request a contract award and run the award gate
GET /contracts/{ref} Read a contract by its contract reference
POST /contracts/{ref}/variations Raise a variation on the increment
POST /contracts/{ref}/pvs Raise a payment voucher against a contract
POST /pvs Raise a voucher against an award or payment authority number
POST /pvs/{pv_no}/validate Validate a draft voucher
GET /pvs/{pv_no} Read a payment voucher
POST /payments/advice Advise a collection against a verification
GET /webhooks List your subscriptions
POST /webhooks Subscribe to events
GET /webhooks/{id} Read one subscription
POST /webhooks/{id}/enable Resume a paused or disabled subscription
POST /webhooks/{id}/redeliver Send one failed delivery again
DELETE /webhooks/{id} Stop a subscription
GET /sandbox/identities Read the seeded identities
POST /sandbox/pay Settle a sandbox bill
POST /sandbox/reset Reseed the sandbox
GET /queue/{ref} Follow a request accepted in degraded mode
GET /dw/extracts List the recent data-warehouse extracts
GET /dw/extracts/{date} Read one day's extract manifest
GET /dw/extracts/{date}/{name} Download one extract file

Connectivity

Prove a key before anything real.

GET /ping

Prove a key and read its limits

The first call an integrator makes. It proves the credential, shows which environment the key belongs to, which scopes it carries and what it is allowed per minute — and it returns a verification id, so the envelope is seen before anything real happens.

  • No scope required: any active key may call it.
Response data
FieldTypeNotes
client *stringClient reference (CLI-YYYY-XXXXXX).
mda *stringThe revenue head the key is scoped to. Every call is filtered by it.
env *string (live | sandbox)Decided by the key, never by the caller.
scopes *array of stringThe scopes this client holds.
signed *booleanWhether this request carried a verified signature.
rate_per_minute *integerRequests per minute allowed for this client.
server_time *stringServer clock, ISO-8601. Use it to correct skew before signing.
version *stringAPI version serving this path.
Example data (inside the envelope)
{
    "client": "CLI-2026-004417",
    "mda": "20008001",
    "env": "sandbox",
    "scopes": [
        "verify",
        "transact",
        "documents"
    ],
    "signed": false,
    "rate_per_minute": 120,
    "server_time": "2026-09-14T09:12:44+01:00",
    "version": "v1"
}

Parties

Who the taxpayer is and whether they are compliant.

POST /parties/verify scope: verify

Resolve a taxpayer and read their compliance position

Resolves ONE identifier to a party on the state register and returns the clearance position of the entity and, for a company, of every active director (BR-10); for a business name, of the proprietor (BR-11). This is a POST and not a GET because the identifier may be a national identity number, and those must never appear in a URL, a proxy log or an access log.

  • Send ONE identifier. If more than one is present the first of abssin, nin, rc, tin, party_ref is the one used — so do not send two and expect them to be cross-checked.
  • A party that resolves but is NOT compliant is not an error: ok=true, data.compliant=false, and data.reason_codes names every published code that applies, with data.remediation_url pointing at the page that fixes the first of them. The refusal with a reason code happens where a decision is actually being asked for — a transaction, an award or a voucher.
  • The NIN is never returned, not even masked; data.party.has_nin only says whether the register holds one. An individual's phone and email are masked unless the calling agency created or last touched the record (a corporate contact is public and is never masked).
  • The clearance position is always read for the CURRENT year. There is no year parameter on this endpoint; a voucher or a transaction is where a specific fiscal year is judged.
Request body
FieldTypeNotes
abssinstringThe state identification number (state_id is accepted as a synonym).
ninstringNational identity number. Never logged, never echoed.
rcstringCAC registration number of a company or business name. The field is `rc`, not `rc_no`.
tinstringTaxpayer identification number.
party_refstringA party reference the platform issued earlier (BPR-YYYY-XXXXXX).
kindstring (abssin | nin | rc | tin | party_ref)The explicit form: name the kind and put the identifier in `value`.
valuestringThe identifier, when `kind` is given.
Response data
FieldTypeNotes
party *objectThe resolved party.
party.party_ref *stringBPR-YYYY-XXXXXX.
party.kind *string (individual | business_name | llc | other)Entity type; it decides which rules apply.
party.display_name *stringRegistered name.
party.abssinstring, nullableState identification number, where the party holds one.
party.rc_numberstring, nullableCAC number where held.
party.tinstring, nullableTIN where held.
party.lga_idinteger, nullableLocal government area of the registered address.
party.status *stringRegister status of the party.
party.has_nin *booleanWhether the register holds a NIN. The number itself never leaves.
party.cac_verified_atstring, nullableWhen the RC number was last verified upstream.
party.contact *objectPhone and email; masked for an individual outside the owning agency.
party.contact.phonestring, nullableContact phone, masked when redacted is true.
party.contact.emailstring, nullableContact email, masked when redacted is true.
party.contact.redacted *booleanTrue when the values above are masked.
party.tax_clearanceobjectThe entity clearance position. Absent when the registry could not be read.
party.tax_clearance.held *booleanWhether a certificate is held at all.
party.tax_clearance.referencestring, nullableCertificate reference.
party.tax_clearance.yearinteger, nullableYear of assessment the certificate covers.
party.tax_clearance.valid_untilstring, nullableExpiry date, YYYY-MM-DD.
party.outstandingobjectWhat the party owes across the platform.
party.outstanding.count *integerUnpaid bills.
party.outstanding.total *numberTotal outstanding.
party.outstanding.available *booleanFalse when the figure could not be computed; treat the total as unknown, not as zero.
party.directorsarray of objectDirectors, or the proprietor of a business name.
party.directors[].party_ref *stringThe director as a party in their own right.
party.directors[].name *stringDirector name.
party.directors[].role *stringdirector, proprietor or signatory.
party.directors[].tcc_held *booleanWhether this subject holds a certificate.
party.directors[].tcc_refstring, nullableTheir certificate reference.
party.directors[].valid_untilstring, nullableExpiry of their certificate.
compliant *booleanTrue only when reason_codes is empty — the entity AND every checked subject is clear.
reason_codes *array of stringEvery published code that stands against this party. Empty when compliant.
remediation_urlstring, nullableA link the taxpayer can open and act on. Null when compliant.
Example request body
{
    "abssin": "SBX0000102"
}
Example data (inside the envelope)
{
    "party": {
        "party_ref": "BPR-2026-000914",
        "kind": "llc",
        "display_name": "Sandbox Blocked Ventures Limited",
        "abssin": "SBX0000102",
        "rc_number": "RC9900102",
        "tin": null,
        "lga_id": null,
        "status": "active",
        "has_nin": false,
        "cac_verified_at": null,
        "contact": {
            "phone": null,
            "email": "sandbox@abiaone.test",
            "redacted": false
        },
        "tax_clearance": {
            "held": true,
            "reference": "TCC/2026/004418",
            "year": 2026,
            "valid_until": "2026-12-31"
        },
        "outstanding": {
            "count": 0,
            "total": 0,
            "available": true
        },
        "directors": [
            {
                "party_ref": "BPR-2026-000915",
                "name": "Ngozi Sandbox Director",
                "role": "director",
                "tcc_held": true,
                "tcc_ref": "TCC/2026/004419",
                "valid_until": "2026-12-31"
            },
            {
                "party_ref": "BPR-2026-000916",
                "name": "Uche Sandbox Nocert",
                "role": "director",
                "tcc_held": false,
                "tcc_ref": null,
                "valid_until": null
            }
        ]
    },
    "compliant": false,
    "reason_codes": [
        "DIRECTOR_NONCOMPLIANT"
    ],
    "remediation_url": "https://services.abiastate.gov.ng/me/tcc"
}
POST /parties/validate scope: verify

Ask whether an applicant may proceed, on a fee-less process

The expression-of-interest check (FR-10.3): a real transaction on a fee-less process, judged by the same gate engine an officer would use, so an applicant's journey cannot show a different answer from the counter's. It answers one question — may this ABSSIN proceed — and names every gate that says otherwise.

  • This runs the gates, so it creates a transaction and returns its reference. It is not a read; do not call it in a loop.
  • Two verification ids are in play: the envelope's own, and gate_verification_id for the gate run. A later dispute cites the second.
  • A blocked applicant comes back ok=false with the first blocking code — and data is still present, carrying the transaction reference and the full blocking list.
Request body
FieldTypeNotes
abssin *stringThe applicant's state identification number (state_id is accepted as a synonym).
processstringThe process, as {AGENCY_CODE}/{PROCESS_CODE}. Defaults to HSG/EOI.
Response data
FieldTypeNotes
may_proceed *booleanTrue when every applicable gate is clear.
process *stringThe process the answer was judged against.
transaction_refstring, nullableThe transaction the gate run created.
gate_verification_idstring, nullableThe verification id the gate run itself recorded.
blocking *array of objectOne entry per gate that is not clear. Empty when may_proceed is true.
blocking[].gate *stringTCC, CGT, STAMP, FEE or BR.
blocking[].reason_codestring, nullableThe published code for this gate.
blocking[].subjectstring, nullableWho the gate is about, when it is not the entity.
blocking[].amount_duenumber, nullableWhat is payable to clear it.
blocking[].messagestring, nullableThe catalogue message with the subject filled in.
messagestring, nullableA sentence for the applicant's screen.
Example request body
{
    "abssin": "SBX0000101",
    "process": "HSG/EOI"
}
Example data (inside the envelope)
{
    "may_proceed": true,
    "process": "HSG/EOI",
    "transaction_ref": "BTX-2026-004870",
    "gate_verification_id": "VRF-2026-7C1A9E4D2B08",
    "blocking": [],
    "message": null
}
POST /parties scope: transact idempotent

Register a party the state register does not hold

Mints a state identification number on the spot so a transaction is never blocked by the absence of an identity (FR-1.6). A company or a business name is registered outright. An individual is registered in the SANDBOX only: in live an ABSSIN is issued only against a photograph captured live, which a server-to-server call cannot perform, so the call is refused with PARTY_UNREGISTERED and a link to the public registration journey.

  • The answer is the same body /parties/verify returns — party, compliant, reason_codes, remediation_url — so one parser handles both.
  • An RC number already on the register is a REFUSAL (VALIDATION_ERROR, field rc_number) that carries the existing party in data.party. Continue with that party; do not retry.
  • Degraded mode: while the taxpayer register or the ABSSIN registration service are unavailable, a well-formed request is answered QUEUED_DEGRADED with HTTP 202 instead — no identity is claimed or minted meanwhile. The error details and data carry queue_ref (QUE-YYYY-XXXXXX) and queue_url; follow it with GET /queue/{ref} or the queue.processed webhook. Queued is not approved: when service resumes the request goes through exactly the checks a live call does, and may still be refused. A malformed request is refused at once, as it would be live; the same Idempotency-Key with a different body is IDEMPOTENCY_KEY_REUSED.
  • Only a company or business name is ever queued. An individual's registration rests on a liveness capture valid only in the moment it is taken, so it is never held: while upstream is down it is refused, and should be retried later.
  • The queue never stores an identity number. A director named only by NIN is queued by the ABSSIN the platform already holds for that person; if it holds none, the call answers a retryable INTERNAL_ERROR — retry later, or name the director by abssin.
Request body
FieldTypeNotes
kind *string (individual | business_name | llc)Entity type to register.
namestringCorporate: name as registered with the CAC. Required for business_name and llc.
rc_numberstringCorporate: CAC registration number. Required for business_name and llc.
phonestringCorporate: contact phone. Required.
emailstringCorporate: contact email. Required.
lga_idintegerCorporate: local government area of the registered address. Required.
addressstringCorporate: registered address. Required.
tinstringTIN where the entity holds one.
directorsarray of objectCorporate: the directors, or the proprietor of a business name, who are the subjects of the clearance check (BR-10/BR-11).
first_namestringIndividual (sandbox only): given name. Required.
middle_namestringIndividual (sandbox only): middle name.
surnamestringIndividual (sandbox only): surname. Required.
Response data
FieldTypeNotes
party *objectThe registered party, exactly as /parties/verify shapes it — party_ref, kind, display_name, abssin, rc_number, tin, contact and, where read, tax_clearance.
compliant *booleanThe clearance position of the party just registered.
reason_codes *array of stringCodes standing against it — a fresh registration usually has none but TCC_MISSING.
remediation_urlstring, nullableWhere the party regularises.
Example request body
{
    "kind": "llc",
    "name": "Umuahia Civil Works Limited",
    "rc_number": "RC1284410",
    "phone": "08030000002",
    "email": "accounts@ucw.example.ng",
    "lga_id": 12,
    "address": "4 Bende Road, Umuahia"
}
GET /parties/{ref} scope: verify

Read a party you already hold a reference for

The same record /parties/verify returns, addressed by the reference the platform issued. Safe in a URL because a party reference is not an identity number — that is precisely why the platform hands you one.

  • Store the party_ref against your own applicant record and use this endpoint afterwards; there is then no reason for a national identity number to be in your system at all.
  • A reference belonging to the other environment reads as PARTY_NOT_FOUND: sandbox and live must not be able to prove each other's rows exist.
Path
{ref}
Party reference, BPR-YYYY-XXXXXX.
Response data
FieldTypeNotes
party *objectAs /parties/verify.
compliant *booleanClearance position for the current year.
reason_codes *array of stringCodes standing against the party.
remediation_urlstring, nullableWhere they fix it.

Transactions

A case through the compliance gates, from submission to issuance.

POST /transactions scope: transact idempotent

Submit a case and run the compliance gates

The machine twin of an officer capturing a case in the agency console. The platform resolves the party, pins the published version of the process, runs every configured gate and answers with the position. A blocked case names the gate, the subject and the remediation link; an eligible case is ready for a document.

  • The transaction is created whether or not it clears — you need the transaction_ref to pay and come back.
  • When a gate blocks, the answer is ok=false with the first blocking reason code and its remediation link, AND data still carries the whole transaction view. Read data even on a refusal.
  • declared_value is required for a value-bearing process; a value above the basis of the clearance held raises an automatic additional assessment (BR-08) rather than a silent pass.
  • The process must belong to the calling agency. Both a bare code (COFO) and the labelled form (LND/COFO) are accepted — anything before the last slash is discarded.
  • Degraded mode: while the taxpayer register or the collection rails are unavailable, a well-formed request is answered QUEUED_DEGRADED with HTTP 202 instead — no case is opened and no gate runs until service resumes, so there is no transaction_ref yet; the processed queue entry carries it. The error details and data carry queue_ref (QUE-YYYY-XXXXXX) and queue_url; follow it with GET /queue/{ref} or the queue.processed webhook. Queued is not approved: when service resumes the request goes through exactly the checks a live call does, and may still be refused. A malformed request is refused at once, as it would be live; the same Idempotency-Key with a different body is IDEMPOTENCY_KEY_REUSED.
  • The queue never stores an identity number. A party named by NIN is queued by the party reference the platform already holds for that person; if it holds none, the call answers a retryable INTERNAL_ERROR — retry later, or name the party by abssin or party_ref.
Request body
FieldTypeNotes
process *stringThe configured process. process_code is accepted as a synonym.
party *objectOne identifier, exactly as /parties/verify accepts. The same fields at the top level of the body also work.
party.abssinstringState identification number.
party.ninstringNational identity number.
party.rcstringCAC number.
party.tinstringTaxpayer identification number.
party.party_refstringA party reference already issued.
declared_valuenumberTransaction value in naira. Required by value-bearing processes.
instrumentstringStamp-duty instrument key where the process has one.
external_refstringYour own case reference. Returned on every answer.
payloadobjectThe process fields, keyed by field key. Validated against the published version.
Response data
FieldTypeNotes
transaction_ref *stringBTX-YYYY-XXXXXX. Cite it on every later call.
status *string (draft | submitted | blocked | eligible | issuing | issued | void | cancelled)blocked = at least one applicable gate is not terminal (BR-01).
mda *stringThe agency the case belongs to.
processstring, nullableThe process code.
process_namestring, nullableWhat the process issues.
partyobject, nullableThe resolved party.
party.party_ref *stringBPR-YYYY-XXXXXX.
party.display_name *stringRegistered name.
party.kind *stringEntity type.
party.abssinstring, nullableState identification number.
declared_valuenumber, nullableAs submitted.
instrumentstring, nullableAs submitted.
external_refstring, nullableYour own reference, echoed.
gates *array of objectEvery gate the process applies.
gates[].gate *stringTCC, CGT, STAMP, FEE or BR — the head this gate guards.
gates[].status *string (CLEARED | ASSESSED | SHORTFALL | PENDING | NOT_APPLICABLE)Upper case, as the register holds it. CLEARED and NOT_APPLICABLE are terminal; everything else stands in the way.
gates[].subjectstring, nullableWho the gate is about, when it is not the entity.
gates[].rolestring (entity | business_name | director | proprietor), nullableWhose position this gate is about.
gates[].reason_codestring, nullableThe published code for a gate that is not cleared.
gates[].computed_amountnumber, nullableWhat the rules say is due on this head.
gates[].paid_amountnumber, nullableWhat has been matched against it.
gates[].na_reasonstring, nullableWhy the gate does not apply.
gates[].evaluated_atstring, nullableWhen it was last judged.
blocking *array of objectThe gates standing in the way, with a readable message each.
blocking[].gate *stringThe gate.
blocking[].statusstring, nullableIts status.
blocking[].reason_codestring, nullableA published reason code.
blocking[].subjectstring, nullableWho is failing — the entity or the director by name.
blocking[].rolestring, nullableTheir role.
blocking[].amount_duenumber, nullableWhat is payable to clear it.
blocking[].assessment_refstring, nullableThe assessment raised for it.
blocking[].messagestring, nullableRenderable message with the subject named.
assessments *array of objectBills raised by the gates.
assessments[].reference *stringThe assessment reference.
assessments[].head *stringRevenue head assessed.
assessments[].basis *stringThe frozen sentence explaining how the figure was reached.
assessments[].computed *numberWhat the rules computed.
assessments[].paid_before *numberWhat had already been paid on this head.
assessments[].amount_due *numberThe balance payable.
assessments[].state *string (raised | director_review | chairman_review | approved | rejected | billed | paid | cancelled)Where the assessment sits in the chain.
assessments[].bill_refstring, nullableThe bill reference in the collection ledger.
assessments[].pay_urlstring, nullableA link the taxpayer can pay from. Always null in the sandbox, which has no gateway — use POST /sandbox/pay.
assessments[].paid_refstring, nullableThe payment reference that settled it.
documentobject, nullablePresent once issued: document_number, short_number, status, tcc_ref, issued_at, verification_id, verify_url.
remediation_urlstring, nullableAdded when the case is blocked: a single-use page the taxpayer can open.
created_atstring, nullableWhen the case was opened.
Example request body
{
    "process": "LND/COFO",
    "party": {
        "abssin": "SBX0000102"
    },
    "declared_value": 25000000,
    "external_ref": "LND/2026/00821",
    "payload": {
        "instrument": "deed_of_assignment",
        "property_address": "Plot U/1147, Umuahia North"
    }
}
Example data (inside the envelope)
{
    "transaction_ref": "BTX-2026-004871",
    "status": "blocked",
    "mda": "20008001",
    "process": "COFO",
    "process_name": "Certificate of Occupancy (sandbox)",
    "party": {
        "party_ref": "BPR-2026-000914",
        "display_name": "Sandbox Blocked Ventures Limited",
        "kind": "llc",
        "abssin": "SBX0000102"
    },
    "declared_value": 25000000,
    "instrument": "deed_of_assignment",
    "external_ref": "LND/2026/00821",
    "gates": [
        {
            "gate": "TCC",
            "status": "PENDING",
            "subject": "Uche Sandbox Nocert",
            "role": "director",
            "reason_code": "DIRECTOR_NONCOMPLIANT",
            "computed_amount": null,
            "paid_amount": null,
            "na_reason": null,
            "evaluated_at": "2026-09-14T09:12:44+01:00"
        },
        {
            "gate": "CGT",
            "status": "ASSESSED",
            "subject": null,
            "role": "entity",
            "reason_code": "CGT_UNPAID",
            "computed_amount": 2500000,
            "paid_amount": 0,
            "na_reason": null,
            "evaluated_at": "2026-09-14T09:12:44+01:00"
        }
    ],
    "blocking": [
        {
            "gate": "TCC",
            "status": "PENDING",
            "reason_code": "DIRECTOR_NONCOMPLIANT",
            "subject": "Uche Sandbox Nocert",
            "role": "director",
            "amount_due": null,
            "assessment_ref": null,
            "message": "Director Uche Sandbox Nocert has no current tax clearance certificate."
        }
    ],
    "assessments": [
        {
            "reference": "ASM-2026-011204",
            "head": "CGT",
            "basis": "10% of 25,000,000.00 declared value, rate card 2026-01-01",
            "computed": 2500000,
            "paid_before": 0,
            "amount_due": 2500000,
            "state": "raised",
            "bill_ref": "SBX-BAS-000418",
            "pay_url": null,
            "paid_ref": null
        }
    ],
    "document": null,
    "remediation_url": "https://services.abiastate.gov.ng/remediate/9f3c1d7a4b2e"
}
GET /transactions/{ref} scope: transact

Read a transaction and its current gate position

The same body as the submission, as the register holds it now. Poll this after your taxpayer pays if you have not subscribed to transaction.gate_changed — settlement is discovered by a sweep, so a gate can clear minutes after the money moves.

  • A transaction belonging to another agency is a not-found, not a forbidden: an integrator may not enumerate another agency's cases.
Path
{ref}
The transaction reference, BTX-YYYY-XXXXXX.
Response data
FieldTypeNotes
transaction_ref *stringThe transaction reference.
status *stringCurrent status.
gates *array of objectAs on submission.
blocking *array of objectAs on submission.
assessments *array of objectAs on submission.
documentobject, nullablePresent once issued.
Reason codes
POST /transactions/{ref}/payments scope: pay idempotent

Cite a payment against a transaction

Tells the platform that a reference in the central collection ledger belongs to this transaction and this head. Nothing is taken on trust: the reference must exist, be settled, belong to this taxpayer, sit under the right revenue head and not already have been spent on another case.

  • Gates are re-run immediately, so data.transaction already shows whether the payment cleared the gate.
  • A refusal still returns data, carrying matched=false and the ledger's own verdict in data.validation.
  • Degraded mode: while the taxpayer register or the collection rails are unavailable, a well-formed request is answered QUEUED_DEGRADED with HTTP 202 instead — the reference is neither matched nor refused until the ledger can be read. The error details and data carry queue_ref (QUE-YYYY-XXXXXX) and queue_url; follow it with GET /queue/{ref} or the queue.processed webhook. Queued is not approved: when service resumes the request goes through exactly the checks a live call does, and may still be refused. A malformed request is refused at once, as it would be live; the same Idempotency-Key with a different body is IDEMPOTENCY_KEY_REUSED.
Path
{ref}
The transaction reference.
Request body
FieldTypeNotes
reference *stringThe payment reference from the collection ledger or the receipt. At most 80 characters.
head *string (CGT | STAMP | FEE | TCC)Which gate the payment is offered against.
amountnumberWhat you believe was paid. A disagreement with the ledger is refused, not reconciled.
Response data
FieldTypeNotes
matched *booleanWhether the evidence was accepted.
head *stringThe head it was applied to.
reference *stringThe reference as cited.
messagestring, nullableWhat the evidence service made of it.
transaction *objectThe whole transaction view after re-running the gates — status, gates, blocking, assessments.
Example request body
{
    "reference": "ABP-2026-77120043",
    "head": "CGT",
    "amount": 2500000
}
POST /transactions/{ref}/assessments scope: transact idempotent

Re-run the gates and read everything that is owed

Re-evaluates the case against the rules as they stand now and hands back what the party owes on every head, with the bill reference and the payment link for each (FR-4.6, FR-11.2). The platform computes every figure from the effective rate card and freezes the basis: a caller never supplies an amount, because a figure supplied by the payer is not an assessment.

  • It is a POST because it re-evaluates: a certificate may have been issued, a bill settled or a rate corrected since the last look.
  • The body is empty. There is no per-head parameter — the gates decide which heads apply.
  • An amount above the automatic ceiling is routed to an assessment officer and the gate reads ASSESSMENT_PENDING_APPROVAL — retryable, not a failure.
Path
{ref}
The transaction reference.
Response data
FieldTypeNotes
transaction_ref *stringThe transaction.
status *stringStatus after the re-run.
gates *array of objectThe recomputed gate list.
assessments *array of objectEvery assessment, each with its frozen basis, bill reference and payment link.
blocking *array of objectWhat still stands in the way.
POST /transactions/{ref}/document scope: documents idempotent

Issue the final document

Mints the final document number for an eligible transaction (FR-5). The number embeds the tax clearance it was issued against, so the document can never be separated from the compliance that earned it. Refused while any applicable gate is open; asking twice returns the same number, never a second one.

  • The body is empty: the issuing system is taken from the key, not from the request.
  • Degraded mode: while the taxpayer register or the collection rails are unavailable, a well-formed request is answered QUEUED_DEGRADED with HTTP 202 instead — no number is minted meanwhile, and none may be written on a document. The error details and data carry queue_ref (QUE-YYYY-XXXXXX) and queue_url; follow it with GET /queue/{ref} or the queue.processed webhook. Queued is not approved: when service resumes the request goes through exactly the checks a live call does, and may still be refused. A malformed request is refused at once, as it would be live; the same Idempotency-Key with a different body is IDEMPOTENCY_KEY_REUSED.
  • A refusal carries data.blocking — the gates that are still open — so one call tells you both that it did not issue and what to do about it.
Path
{ref}
The transaction reference.
Response data
FieldTypeNotes
document *objectThe minted document.
document.document_number *stringThe long form: AB/{MDA}/{PROC}/{YYYY}/{SEQ}/{TCC-REF}.
document.short_number *stringThe addressable short form printed on the document.
document.status *string (valid | void)Status on the register.
document.tcc_ref *stringThe clearance the number embeds.
document.tcc_valid_untilstring, nullableExpiry of that clearance.
document.issued_at *stringIssue timestamp, ISO-8601.
document.verification_id *stringThe ledger id for the issuance itself.
document.verify_url *stringThe public verification page — the same target as the QR code.
document.void_reasonstring, nullableWhy it was voided, when it has been.
Example data (inside the envelope)
{
    "document": {
        "document_number": "AB/LND/COFO/2026/4F2K7C/TCC-2026-004418",
        "short_number": "AB-LND26-4F2K7C",
        "status": "valid",
        "tcc_ref": "TCC/2026/004418",
        "tcc_valid_until": "2026-12-31",
        "issued_at": "2026-09-14T10:02:11+01:00",
        "verification_id": "VRF-2026-9F3C1D7A4B2E",
        "verify_url": "https://services.abiastate.gov.ng/verify/document/AB-LND26-4F2K7C",
        "void_reason": null
    }
}

Documents

Final documents, their evidence, and public verification.

POST /documents/verify scope: documents

Look a document up by its long number

The long document number contains forward slashes and a route parameter never spans one, so the full number is carried in the body instead. The answer is the same record GET /documents/{no} returns.

  • Nothing is minted, so no Idempotency-Key is required.
Request body
FieldTypeNotes
number *stringThe long or the short number, exactly as printed. document_number is accepted as a synonym.
Response data
FieldTypeNotes
document *objectAs GET /documents/{no}.
transaction_refstring, nullableIssuing agency only.
evidence_urlstring, nullableIssuing agency only: the evidence bundle for this number.
Example request body
{
    "number": "AB/LND/COFO/2026/4F2K7C/TCC-2026-004418"
}
GET /documents/{no} scope: documents

Read a final document from the register

The owning agency sees the whole record. Any other key sees the limited public set — issuer, process, reduced party name, clearance reference and status — because a document number is a verification instrument, not a licence to read another agency's case file.

  • {no} is the SHORT number. The long form contains "/" and a route parameter never spans one, so post the long number to /documents/verify instead.
  • A VOID document still answers, with its whole record in data — but as a refusal carrying DOCUMENT_VOID, because "this number is void" is not a favourable answer.
Path
{no}
The short document number.
Response data
FieldTypeNotes
document *objectThe record.
document.document_number *stringLong form.
document.short_number *stringShort form.
document.status *string (valid | void)A void document still answers — it answers "void".
document.mda *stringIssuing agency, by revenue head.
document.process *stringProcess code.
document.process_namestring, nullableProcess name.
document.document_name *stringWhat was issued, e.g. Certificate of Occupancy.
document.party_display *stringIssued to. An individual is reduced to a masked name on every channel.
document.entity_type *stringEntity type of the holder.
document.tcc_ref *stringThe clearance the number embeds.
document.tcc_valid_untilstring, nullableExpiry of that clearance.
document.issued_at *stringIssue timestamp.
document.verification_id *stringThe issuance ledger id.
document.verify_url *stringThe public verification page.
document.sandbox *booleanTrue for a rehearsal document. It is never a state document.
document.void_reasonstring, nullableWhy it was voided.
document.void_atstring, nullableWhen it was voided.
document.party_namestring, nullableIssuing agency only: the unmasked name.
document.abssinstring, nullableIssuing agency only.
document.issued_bystring, nullableIssuing agency only: who minted it.
document.issued_to_systemstring, nullableIssuing agency only: the channel it was minted through.
transaction_refstring, nullableIssuing agency only.
evidence_urlstring, nullableIssuing agency only.
GET /documents/{no}/evidence scope: documents

The evidence bundle behind a document

Every gate as it stood at issuance — status, the frozen basis, the rule version, the assessments raised and the payments matched, and the clearance certificate cited (FR-14.4). This is what an auditor reads a year later, and it is available to the owning agency only.

  • The bundle is versioned and hashed. Store the sha256 with your copy: it is how a printed bundle is proved identical to the one the platform holds.
Path
{no}
The short document number.
Response data
FieldTypeNotes
document_number *stringThe document, in long form.
version *integerBundle version.
sha256 *stringHash of the bundle as returned.
evidence *objectThe bundle itself: the gates with their basis and rule version, the assessments with frozen inputs, the payments matched, and the clearance cited.
GET /public/documents/{no} no key required

Verify a document number without a key

The machine twin of the public verification page. No credential, no personal data: the issuer, what was issued, a reduced name, the clearance reference and whether the document is genuine and still valid. Throttled per address, and every lookup is written to the verification ledger with the caller's address.

  • Use this to verify a document presented to you; use GET /documents/{no} to read one you issued. For a number printed in its long form, post it to /public/documents/verify.
  • A number that is not on the register answers ok=false with DOCUMENT_NOT_FOUND and no data. A number that is VOID answers ok=false with DOCUMENT_VOID and the record in data — being told it is void is the point.
  • request.client, request.mda and request.env are null on this route: there is no key, so there is nothing to name.
Path
{no}
The short document number as printed.
Response data
FieldTypeNotes
document *objectThe public field set: short_number, document_number, status, mda, process_name, document_name, party_display, tcc_ref, issued_at, verify_url, sandbox and, where void, void_reason.
POST /public/documents/verify no key required

Verify a long document number without a key

The unauthenticated twin of /documents/verify, for a number printed in its long form. Same limited answer as the public GET: issuer, process, reduced name, clearance reference and status. Throttled per address.

Request body
FieldTypeNotes
number *stringThe number as printed, long or short. document_number is accepted as a synonym.
Response data
FieldTypeNotes
document *objectThe public fields, as GET /public/documents/{no}.
Example request body
{
    "number": "AB-LND26-4F2K7C"
}

Contractors

Contractor onboarding for award and payment (FR-6).

POST /contractors scope: transact idempotent

Onboard a contractor

Registers a contractor for award and payment (FR-6.1). The entity and every director are checked against the clearance registry and that answer is frozen into the record with its verification ids, so a later dispute can see what was known on the day. Bank details are encrypted at rest and only the last four digits are ever returned.

  • The record lands pending_verification. A second officer verifies it in the procurement console, and the officer who onboarded may never be the officer who verifies (SEC-8) — which is why there is no verify endpoint here.
  • Changing bank details bumps a version; a voucher pins the version it was approved against.
  • Identifier and bank fields sit at the TOP LEVEL of the body. There is no nested party or bank object.
  • This endpoint is part of the procurement surface. When that surface is switched off on a deployment every call here answers API_DISABLED.
Request body
FieldTypeNotes
abssinstringOne identifier is required: abssin, nin, rc_number, tin or party_ref.
rc_numberstringCAC registration number. Doubles as the identifier when no other is sent.
tinstringTIN.
party_refstringA party reference already issued.
identifier_kindstring (abssin | nin | rc | tin | party_ref)The explicit form: name the kind here and put the value in `identifier`.
identifierstringThe identifier, when identifier_kind is given.
legal_name *stringName as registered, at most 200 characters.
entity_type *string (individual | business_name | llc | other)Decides which deductions apply later (BR-15, BR-16). Defaults to llc.
vat_registeredbooleanVAT is withheld only from a registered limited company (BR-16).
vat_reg_nostringVAT registration number.
classes_of_workarray of stringWhat this contractor is registered to do.
bank_code *stringBank code.
account_no *stringAccount number. Encrypted on receipt, never logged and never returned — not even to the agency that sent it.
account_name *stringAccount name as held by the bank.
Response data
FieldTypeNotes
contractor *objectThe record as held.
contractor.contractor_ref *stringCTR-YYYY-XXXXXX.
contractor.legal_name *stringAs registered.
contractor.entity_type *stringAs registered.
contractor.abssinstring, nullableState identification number.
contractor.rc_numberstring, nullableCAC number.
contractor.tinstring, nullableTIN.
contractor.vat_registered *booleanAs registered.
contractor.vat_reg_nostring, nullableAs registered.
contractor.classes_of_work *array of stringAs registered.
contractor.status *string (draft | pending_verification | active | suspended)Only an active contractor may be awarded.
contractor.bank *objectNever the account number.
contractor.bank.bank_codestring, nullableAs supplied.
contractor.bank.account_namestring, nullableAs supplied.
contractor.bank.account_last4string, nullableThe last four digits, and nothing more.
contractor.bank.version *integerBumped on every bank change; a voucher pins the version it was approved against.
contractor.bank.verified_atstring, nullableWhen the account was verified.
contractor.directors *array of objectThe snapshot frozen at onboarding.
contractor.directors[].name *stringDirector name.
contractor.directors[].role *stringdirector, proprietor or signatory.
contractor.directors[].tcc_statusstring, nullableWhat the registry said on the day.
contractor.directors[].tcc_refstring, nullableTheir certificate reference.
contractor.directors[].verification_idstring, nullableThe ledger id for that check.
contractor.verified_atstring, nullableWhen a second officer verified the record.
contractor.created_atstring, nullableWhen it was onboarded.
complianceobject, nullableThe frozen clearance answer.
compliance.favourable *booleanWhether entity and directors all cleared on the day.
compliance.reason_codestring, nullableThe code, when they did not.
compliance.verification_idstring, nullableThe ledger id for the entity check.
compliance.tcc_refstring, nullableThe entity certificate cited.
compliance.directors *array of objectPer director: name, role, tcc_status, tcc_ref, valid_until.
message *stringWhat happens next, in a sentence.
Example request body
{
    "rc_number": "RC9900101",
    "legal_name": "Sandbox Compliant Holdings Limited",
    "entity_type": "llc",
    "vat_registered": true,
    "classes_of_work": [
        "civil"
    ],
    "bank_code": "058",
    "account_no": "0123456789",
    "account_name": "Sandbox Compliant Holdings Limited"
}
GET /contractors/{ref} scope: transact

Read a contractor

The record as held, including the frozen director snapshot and the bank details reduced to their last four digits.

  • The account number is not in this answer and will not be added to it: an API that can echo an account number is an API that can be used to harvest them.
Path
{ref}
Contractor reference, CTR-YYYY-XXXXXX.
Response data
FieldTypeNotes
contractor *objectAs POST /contractors returns it.

Contracts

Award requests, the award gate and the contracts register.

POST /contracts/lookup scope: transact

Call a contract up by its full award number

The award number is a final document number and contains forward slashes, so the long form travels in the body. This is the call the voucher screen makes: it returns the award date, the sum, approved variations, paid to date, retention and the stamp and capital gains position — everything needed to raise a payment, and no bank details.

  • Nothing is minted, so no Idempotency-Key is required.
Request body
FieldTypeNotes
final_doc_nostringThe award number, long or short. award_number and number are accepted as synonyms.
contract_refstringThe contract reference, CON-YYYY-XXXXXX, when you hold that instead. One of the two is required.
Response data
FieldTypeNotes
contract *objectAs GET /contracts/{ref}.
Example request body
{
    "final_doc_no": "AB-DPO26-9K41XA"
}
POST /contracts scope: transact idempotent

Request a contract award and run the award gate

Creates the contract and runs the award gate with the contract sum as the declared value. Stamp duty is due ON AWARD (BR-12), so the answer carries the stamp bill and what is outstanding on it; capital gains is recorded cleared with a reference or not applicable with a named reason (BR-23). The award itself is taken by a Due Process officer in the console once the gates are terminal — there is no force path, and no API path to an unearned award.

  • The award number IS the final document number. There is no separate certificate to issue.
  • Blocking gates are the EXPECTED answer here, not a refusal: ok=true with may_be_awarded=false and the outstanding stamp duty named. BR-12 means the duty has not been paid yet by definition.
Request body
FieldTypeNotes
contractor_ref *stringAn active contractor, CTR-YYYY-XXXXXX.
title *stringWhat the contract is for, at most 255 characters.
descriptionstringScope in words.
contract_type *string (construction | supply | consultancy | services | rent | other)Doubles as the withholding service class (BR-15). Defaults to construction.
contract_sum *numberAward value in naira, greater than zero.
vat_treatmentstring (inclusive | exclusive | none)How VAT sits in the sum; it decides the withholding base later. Defaults to exclusive.
retention_pctnumberRetention percentage held from each payment, 0 to 100.
procurement_methodstringOpen competitive, selective, direct or emergency.
due_process_refstringThe Bureau reference for the procurement.
start_datestringYYYY-MM-DD.
end_datestringYYYY-MM-DD.
Response data
FieldTypeNotes
contract *objectThe contract as raised — the same shape GET /contracts/{ref} returns.
may_be_awarded *booleanTrue when every award gate is terminal and a Due Process officer can sign.
blocking *array of objectGate, reason code, subject, amount due and a readable message for each.
reason_codes *array of stringThe distinct codes standing in the way.
remediation_urlstring, nullableWhere the contractor regularises.
message *stringWhat happens next, in a sentence.
Example request body
{
    "contractor_ref": "CTR-2026-000318",
    "title": "Reconstruction of Bende Road, Phase 2",
    "contract_type": "construction",
    "contract_sum": 184500000,
    "vat_treatment": "exclusive",
    "retention_pct": 5
}
GET /contracts/{ref} scope: transact

Read a contract by its contract reference

Everything a paying agency needs before raising a voucher: award date, sum, approved variations, paid to date, retention held and released, the stamp and capital gains position, and the contractor it was awarded to.

  • {ref} is the CONTRACT reference, CON-YYYY-XXXXXX — the award number is not accepted in a path, in either form. Post the award number to /contracts/lookup instead.
Path
{ref}
Contract reference, CON-YYYY-XXXXXX.
Response data
FieldTypeNotes
contract *objectThe contract.
contract.contract_ref *stringCON-YYYY-XXXXXX.
contract.award_numberstring, nullableThe final document number. Null until awarded.
contract.status *stringrequested, draft, gated, awarded, active, completed, terminated or void.
contract.mda *stringThe awarding agency.
contract.contractorobject, nullablecontractor_ref, legal_name, entity_type, abssin and status. No bank details.
contract.title *stringWhat the contract is for.
contract.contract_type *stringThe withholding service class.
contract.vat_treatment *stringHow VAT sits in the sum.
contract.due_process_refstring, nullableBureau reference.
contract.procurement_methodstring, nullableHow it was procured.
contract.contract_sum *numberAward value.
contract.approved_variations *numberTotal approved variations, each stamped on its increment.
contract.contract_value *numberSum plus approved variations — the ceiling cumulative voucher gross is checked against.
contract.gross_paid *numberSum of paid vouchers.
contract.unpaid_balance *numbercontract_value less gross_paid.
contract.retention *objectpct, held, released.
contract.award_datestring, nullableYYYY-MM-DD.
contract.start_datestring, nullableYYYY-MM-DD.
contract.end_datestring, nullableYYYY-MM-DD.
contract.stamp_duty *objectstatus (pending, assessed, paid, not_applicable), amount, bill_ref, payment_ref.
contract.cgt *objectstatus (pending, cleared, not_applicable), na_reason, reference.
contract.tax_clearance *objectreference and verification_id of the clearance the award was judged against.
contract.gate_transaction_refstring, nullableThe transaction the award gate ran on.
contract.created_atstring, nullableWhen it was raised.
POST /contracts/{ref}/variations scope: transact idempotent

Raise a variation on the increment

Records an increment to the contract. The increment goes through the same award gate as the original sum, so stamp duty is assessed on the increase — a variation is not a way to enlarge a contract behind the duty that was paid on it.

  • The increment joins the contract only when a second officer approves the variation in the console (SEC-8), and its own stamp duty is gated first — so a variation is never instantly spendable.
  • Cumulative voucher gross is checked against the sum plus APPROVED variations, so a variation must be recorded and approved before the voucher that relies on it.
Path
{ref}
Contract reference, CON-YYYY-XXXXXX.
Request body
FieldTypeNotes
amount *numberThe increment in naira, greater than zero.
reason *stringWhy the variation is being raised, at most 500 characters. Kept with the contract.
Response data
FieldTypeNotes
contract *objectThe contract as it stands after the variation was recorded.
variationobject, nullableThe variation.
variation.variation_no *integerIts number on this contract.
variation.amount *numberAs recorded.
variation.reasonstring, nullableAs given.
variation.status *string (draft | gated | approved | rejected)Where the variation sits. This call never returns approved: a second officer does that in the console.
variation.stamp_status *stringThe duty position on the increment.
variation.stamp_bill_refstring, nullableThe bill for that duty.
gates_clear *booleanWhether the gate run on the increment came back eligible.
blocking *array of objectWhat stands in the way of the increment.
message *stringWhat happens next, in a sentence.
Example request body
{
    "amount": 18450000,
    "reason": "Additional drainage, approved by EXCO reference EXCO/2026/114"
}

Vouchers

Payment vouchers and deduction at source (FR-7).

POST /contracts/{ref}/pvs scope: pv idempotent

Raise a payment voucher against a contract

The machine twin of the agency voucher screen. There is no payee, no bank account and no net figure in the request: the contract supplies the first two and the deduction engine computes the third. The voucher is created against the contract's award number and the full deduction breakdown comes back with the basis sentence for every line.

  • net_amount is null until every deductible line has been ticked (BR-17). That is not a redaction: the column is NULL, because a net that has not been agreed does not exist. net_available says which it is.
  • A contract that has not been AWARDED carries no award number and is refused with CONTRACT_NOT_FOUND: no voucher exists without a final document number (BR-14).
  • Send payee_name, bank_code, account_no, net_amount or withheld_total and the call is refused with VALIDATION_ERROR naming the field. They are not ignored — an integrator who believes they set the net has a bug they need to see.
  • The clearance is re-checked for the voucher's fiscal year by POST /pvs/{pv_no}/validate, which is the next call to make.
  • Degraded mode: while the taxpayer register or the collection rails are unavailable, a well-formed request is answered QUEUED_DEGRADED with HTTP 202 instead — no voucher is raised meanwhile; the period is fixed at the moment of the request. The error details and data carry queue_ref (QUE-YYYY-XXXXXX) and queue_url; follow it with GET /queue/{ref} or the queue.processed webhook. Queued is not approved: when service resumes the request goes through exactly the checks a live call does, and may still be refused. A malformed request is refused at once, as it would be live; the same Idempotency-Key with a different body is IDEMPOTENCY_KEY_REUSED.
Path
{ref}
Contract reference, CON-YYYY-XXXXXX.
Request body
FieldTypeNotes
pv_type *string (contract | part | retention | adjustment)Which kind of payment this is. Defaults to contract.
gross_amount *numberGross payable before deductions, greater than zero. Cumulative gross may not exceed the sum plus approved variations.
descriptionstringWhat is being paid for, at most 500 characters.
invoice_refstringThe contractor's invoice reference.
periodstringYYYY-MM the payment belongs to. Defaults to the current period.
Response data
FieldTypeNotes
voucher *objectThe voucher.
voucher.pv_no *stringPV-{TYPE}-{YYYY}-{MDA}-{XXXXXX}.
voucher.pv_type *stringAs raised.
voucher.status *stringDRAFT, VALIDATED, TICKED, FC_APPROVED, WARRANTED, AG_APPROVED, SCHEDULED, RELEASED, PAID, RETURNED or REJECTED.
voucher.mda *stringThe paying agency.
voucher.award_number *stringThe award or authority the voucher is drawn on. No voucher exists without one (BR-14).
voucher.payee *stringTaken from the award, never from the request.
voucher.entity_type *stringThe payee's entity type; it decides which heads apply.
voucher.descriptionstring, nullableAs raised.
voucher.invoice_refstring, nullableAs raised.
voucher.gross_amount *numberAs raised.
voucher.withheld_total *numberSum of deductible lines.
voucher.net_amountnumber, nullableNull until TICKED.
voucher.net_available *booleanFalse while net_amount is null — the net has not been agreed, so it does not exist.
voucher.pending_ticks *integerDeductible lines still awaiting a tick.
voucher.deductions *array of objectOne line per head, in statutory order.
voucher.deductions[].line_no *integerOrder on the voucher.
voucher.deductions[].head *string (PAYE | WHT | VAT | STAMP | SECURITY | ADMIN | DEV_LEVY | CGT | ADV_RECOVERY | RETENTION)The head withheld.
voucher.deductions[].authority *string (ABIA_BIR | NRS | LGA | PENSION | NHF | UNION | TREASURY)Who the money is for.
voucher.deductions[].rev_codestring, nullableThe revenue code it is posted to.
voucher.deductions[].revenue_itemstring, nullableThat code in words.
voucher.deductions[].rate_valuenumber, nullableThe rate applied.
voucher.deductions[].rate_kind *stringpercent, flat, band or none.
voucher.deductions[].basis *stringThe frozen sentence: which rate, on what base, and why.
voucher.deductions[].base_amount *numberThe base the rate was applied to.
voucher.deductions[].amount *numberThe computed figure, rounded half-up to two decimals independently of every other line.
voucher.deductions[].applicability *string (deductible | settled_at_award | not_applicable)settled_at_award = already paid on award, never withheld twice.
voucher.deductions[].na_reasonstring, nullableWhy the head does not apply.
voucher.deductions[].status *string (pending | withheld | settled)withheld = the treasury deducts it; settled = the payee already paid it.
voucher.deductions[].settled_refstring, nullableThe payment that settled it, when it was settled outside this voucher.
voucher.deductions[].provisional *booleanTrue while the rate card awaits confirmation by the Directorate.
voucher.fiscal_yearinteger, nullableThe year the clearance must cover.
voucher.periodstring, nullableYYYY-MM.
voucher.payment_codestring, nullableMinted at treasury approval; the one string that ties voucher, schedule line, covering letter, bank narration and credit note together (FR-7.16).
voucher.paid_atstring, nullableWhen the money left.
voucher.return_reasonstring, nullableWhy it was returned to the maker.
voucher.reject_reasonstring, nullableWhy it was rejected.
voucher.created_atstring, nullableWhen it was raised.
message *stringWhat happens next, in a sentence.
Example request body
{
    "pv_type": "part",
    "gross_amount": 42000000,
    "description": "Interim valuation 2",
    "invoice_ref": "INV-0442"
}
Example data (inside the envelope)
{
    "voucher": {
        "pv_no": "PV-PAR-2026-11010001-000318",
        "pv_type": "part",
        "status": "DRAFT",
        "mda": "11010001",
        "award_number": "AB/DPO/AWRD/2026/9K41XA/TCC-2026-004418",
        "payee": "Sandbox Compliant Holdings Limited",
        "entity_type": "llc",
        "description": "Interim valuation 2",
        "invoice_ref": "INV-0442",
        "gross_amount": 42000000,
        "withheld_total": 4515000,
        "net_amount": null,
        "net_available": false,
        "pending_ticks": 4,
        "deductions": [
            {
                "line_no": 1,
                "head": "STAMP",
                "authority": "ABIA_BIR",
                "rev_code": null,
                "revenue_item": null,
                "rate_value": null,
                "rate_kind": "none",
                "basis": "Stamp duty settled on award under payment ABP-2026-77003318; not withheld again (BR-12)",
                "base_amount": 0,
                "amount": 0,
                "applicability": "settled_at_award",
                "na_reason": null,
                "status": "settled",
                "settled_ref": "ABP-2026-77003318",
                "provisional": false
            },
            {
                "line_no": 2,
                "head": "WHT",
                "authority": "NRS",
                "rev_code": null,
                "revenue_item": null,
                "rate_value": 2.5,
                "rate_kind": "percent",
                "basis": "2.5% withholding on construction by a limited company, on gross exclusive of VAT (BR-15)",
                "base_amount": 42000000,
                "amount": 1050000,
                "applicability": "deductible",
                "na_reason": null,
                "status": "pending",
                "settled_ref": null,
                "provisional": true
            }
        ],
        "fiscal_year": 2026,
        "period": "2026-09",
        "payment_code": null,
        "paid_at": null
    },
    "message": "Raised. Validate it to re-check the clearance, then the deductions are ticked in the console."
}
POST /pvs scope: pv idempotent

Raise a voucher against an award or payment authority number

The same act with the number in the body rather than the contract in the path. It takes an award number directly, and it is the only way to raise a voucher against a PAYMENT AUTHORITY — imprest, an advance, a non-contract payment. An authority is the final-document equivalent for those, so BR-14 holds here too.

  • Cumulative gross is checked against the authority's ceiling and its validity dates.
  • Degraded mode: while the taxpayer register or the collection rails are unavailable, a well-formed request is answered QUEUED_DEGRADED with HTTP 202 instead — no voucher is raised meanwhile; the period is fixed at the moment of the request. The error details and data carry queue_ref (QUE-YYYY-XXXXXX) and queue_url; follow it with GET /queue/{ref} or the queue.processed webhook. Queued is not approved: when service resumes the request goes through exactly the checks a live call does, and may still be refused. A malformed request is refused at once, as it would be live; the same Idempotency-Key with a different body is IDEMPOTENCY_KEY_REUSED.
Request body
FieldTypeNotes
final_doc_no *stringThe award or authority number the voucher is drawn on. award_number is accepted as a synonym.
pv_type *string (contract | part | retention | adjustment | imprest | non_contract | advance)Which kind of payment this is.
gross_amount *numberGross payable before deductions.
descriptionstringWhat is being paid for.
invoice_refstringSupplier invoice reference.
periodstringYYYY-MM the payment belongs to.
Response data
FieldTypeNotes
voucher *objectAs POST /contracts/{ref}/pvs returns it.
message *stringWhat happens next.
Example request body
{
    "final_doc_no": "AB-AGT26-2M8P4D",
    "pv_type": "non_contract",
    "gross_amount": 1250000,
    "description": "Quarterly office supplies"
}
POST /pvs/{pv_no}/validate scope: pv idempotent

Validate a draft voucher

Re-checks the clearance for the voucher's fiscal year — the entity and every director (BR-03/04) — recomputes the deduction lines and moves the voucher from DRAFT to VALIDATED. A clearance that is not current leaves it in DRAFT with the reason and a remediation link, so the voucher is never lost, only waiting.

  • A stale clearance is a REFUSAL that still carries data: data.voucher is the voucher as it stands (still DRAFT) and data.blocking names the subject.
  • Everything after this point — ticking, controller approval, warrant, treasury approval — is done by named officers in the consoles, because each step is a different person and SEC-8 will not let one credential be all of them.
Path
{pv_no}
The voucher number.
Response data
FieldTypeNotes
voucher *objectThe voucher after the check — status VALIDATED, or still DRAFT when the clearance is stale. net_amount is still null: the net is revealed at TICKED, not here.
message *stringWhat happened, in a sentence.
GET /pvs/{pv_no} scope: pv

Read a payment voucher

The voucher with its deduction breakdown and its position in the approval chain. Once paid it carries the payment code — the one string that ties voucher, schedule line, covering letter, bank narration and credit note together (FR-7.16).

  • net_amount stays null until the voucher reaches TICKED, on this endpoint as everywhere else.
Path
{pv_no}
The voucher number.
Response data
FieldTypeNotes
voucher *objectAs POST /contracts/{ref}/pvs returns it, with each line's current tick state and the payment code once it is paid.

Payments

Telling the platform money was collected against an answer it gave.

POST /payments/advice scope: pay idempotent

Advise a collection against a verification

An agency system telling the platform that money was collected for an answer the platform gave. The verification id is mandatory and must exist (SEC-5, FR-11.5): a payment that cites no verification is refused, because a collection with no verification behind it is exactly the leak this platform exists to close.

  • The verification must be one this client was answered with, in this environment. Someone else's verification is as good as none.
  • The transaction is taken from the verification. Send transaction_ref only when the verification does not name one.
  • The reference is then checked against the central ledger: unknown, unsettled, wrong amount, wrong payer or already spent are all refusals, each with its own code.
  • Degraded mode: while the taxpayer register or the collection rails are unavailable, a well-formed request is answered QUEUED_DEGRADED with HTTP 202 instead — the verification citation is checked at once, but the reference is neither matched nor refused until the ledger can be read. The error details and data carry queue_ref (QUE-YYYY-XXXXXX) and queue_url; follow it with GET /queue/{ref} or the queue.processed webhook. Queued is not approved: when service resumes the request goes through exactly the checks a live call does, and may still be refused. A malformed request is refused at once, as it would be live; the same Idempotency-Key with a different body is IDEMPOTENCY_KEY_REUSED.
Request body
FieldTypeNotes
verification_id *stringThe id from the answer this collection followed.
reference *stringThe reference in the collection ledger, at most 80 characters.
headstring (CGT | STAMP | FEE | TCC)The head the money was collected for. Defaults to FEE.
amountnumberAmount collected. A disagreement with the ledger is refused, not reconciled.
transaction_refstringThe case the money belongs to, when the verification does not name one.
Response data
FieldTypeNotes
matched *booleanWhether the evidence was accepted.
head *stringThe head it was applied to.
reference *stringAs cited.
messagestring, nullableWhat the evidence service made of it.
transaction *objectThe transaction view after re-running the gates.
Example request body
{
    "verification_id": "VRF-2026-9F3C1D7A4B2E",
    "reference": "ABP-2026-77120043",
    "amount": 2500000,
    "head": "CGT"
}

Webhooks

Subscriptions to platform events.

GET /webhooks scope: webhooks

List your subscriptions

Every endpoint registered by this client, with its health. A subscription paused after repeated failures shows here with its failure count before it shows anywhere else.

Response data
FieldTypeNotes
webhooks *array of objectThe subscriptions.
webhooks[].id *integerSubscription id.
webhooks[].url *stringDelivery URL.
webhooks[].events *array of stringEvents subscribed.
webhooks[].status *string (active | paused | disabled)paused = too many consecutive failures.
webhooks[].consecutive_failures *integerFailures since the last success.
webhooks[].paused_after *integerThe failure count at which the platform pauses the endpoint.
webhooks[].last_success_atstring, nullableLast accepted delivery.
webhooks[].created_atstring, nullableWhen it was registered.
events *array of stringEvery event name this platform will accept in a subscription.
queue_due *integerDeliveries waiting to go out across the platform.
signature_note *stringThe signing string, restated so a client can be written from this response alone.
Reason codes
POST /webhooks scope: webhooks idempotent

Subscribe to events

Registers an HTTPS endpoint and returns the signing secret ONCE. Store it: it is encrypted at rest and cannot be shown again. The URL must be https, must resolve to a public address, and must not redirect — a loopback or private host is refused outright.

  • Subscribe to the events you handle. An event you did not ask for is never sent, and an unknown event name is a validation error rather than a silent no-op.
  • One client may hold ten endpoints. Delete one before registering an eleventh.
Request body
FieldTypeNotes
url *stringHTTPS endpoint on a public host, at most 255 characters, no credentials in the URL, port 443 or above 1024.
events *array of stringEvent names from the published list.
Response data
FieldTypeNotes
webhook *objectThe subscription as registered: id, url, events, status, consecutive_failures, paused_after, created_at.
secret *stringThe signing secret. Shown once, never again.
notice *stringA reminder to store the secret before closing the response.
Example request body
{
    "url": "https://lands.example.gov.ng/hooks/abia",
    "events": [
        "transaction.gate_changed",
        "document.issued"
    ]
}
GET /webhooks/{id} scope: webhooks

Read one subscription

The subscription with its recent delivery history: what was sent, what your endpoint answered, and when the next attempt is due. The first place to look when events have stopped arriving.

  • The secret is never returned. If you have lost it, delete the subscription and register it again.
Path
{id}
Subscription id.
Response data
FieldTypeNotes
webhook *objectThe subscription, as in the list.
deliveries *array of objectThe fifty most recent deliveries: id, event, status, attempts, the last HTTP code and when the next attempt is due.
Reason codes
POST /webhooks/{id}/enable scope: webhooks idempotent

Resume a paused or disabled subscription

Clears the failure count and puts the subscription back to active. Use it once your endpoint is answering again — the platform pauses rather than deletes, so nothing has to be reconfigured. The URL is re-checked against the SSRF rules first: a name that now resolves to a private address is refused.

  • Resuming does not resend what was already marked dead; use the redeliver endpoint for that.
Path
{id}
Subscription id.
Response data
FieldTypeNotes
webhook *objectThe subscription, now active with consecutive_failures reset to zero.
message *stringWhat happens next.
POST /webhooks/{id}/redeliver scope: webhooks idempotent

Send one failed delivery again

Puts a single failed or dead delivery back at the front of the queue, so an outage on your side does not cost you the event it spanned. It goes out with its original delivery id, so your deduplication still holds.

  • One delivery per call: name it. Read GET /webhooks/{id} for the ids of the deliveries that failed.
  • A delivery that is not failed or dead is refused with VALIDATION_ERROR naming its current status — a delivered event is not resent.
Path
{id}
Subscription id.
Request body
FieldTypeNotes
delivery_id *integerThe delivery to send again. It must belong to this subscription.
Response data
FieldTypeNotes
delivery_id *integerThe delivery requeued.
status *stringpending.
message *stringWhen it goes out.
Example request body
{
    "delivery_id": 4417
}
DELETE /webhooks/{id} scope: webhooks idempotent

Stop a subscription

Stops delivery. The subscription is DISABLED rather than erased, so its delivery history — which is the evidence in a "we never received it" conversation — survives, and POST /webhooks/{id}/enable can bring it back without reconfiguring anything.

  • Events already queued for it are kept but never sent while it is disabled.
Path
{id}
Subscription id.
Response data
FieldTypeNotes
webhook *objectThe subscription, now status disabled.
message *stringWhat was done.
Reason codes

Sandbox

Sandbox-only helpers. A live key is refused.

GET /sandbox/identities

Read the seeded identities

The same cast this portal publishes, from the sandbox itself — so a test harness can read it at run time instead of hard-coding what a page said.

Response data
FieldTypeNotes
identities *array of objectThe seeded parties.
identities[].abssinstring, nullableThe state identification number, or null for the unregistered person.
identities[].ninstring, nullableSet only on the identity that has no ABSSIN.
identities[].kind *stringindividual, business_name or llc.
identities[].name *stringRegistered name.
identities[].rc_numberstring, nullableCAC number, for the corporate identities.
identities[].tcc *string (current | expired | none)What certificate this identity holds.
identities[].directors *array of stringThe ABSSINs standing behind a corporate identity.
identities[].note *stringWhat the identity is for.
process *stringThe sandbox process, as {AGENCY_CODE}/{PROCESS_CODE}.
note *stringA reminder that a reset restores these identifiers and never renumbers them.
Reason codes
POST /sandbox/pay idempotent

Settle a sandbox bill

Marks a sandbox bill settled so the gate that is waiting on it can clear. There is no payment gateway in the sandbox and nothing here reaches the collection ledger: this is the stand-in for a taxpayer paying, and a live key is refused.

  • It settles the assessment in full. There is no partial-payment parameter — to see a shortfall, work a case whose declared value exceeds the basis of the clearance held.
  • The reference to cite afterwards comes back as data.paid_ref.
Request body
FieldTypeNotes
reference *stringThe bill reference the assessment carries, or the assessment reference itself. bill_ref and assessment_ref are accepted as synonyms.
Response data
FieldTypeNotes
reference *stringAs cited.
state *stringThe assessment chain state after settlement.
transaction_refstring, nullableThe case the bill belonged to.
headstring, nullableThe head settled.
amountnumber, nullableWhat was recorded as paid.
paid_refstring, nullableThe sandbox payment reference. Cite this on /transactions/{ref}/payments.
messagestring, nullableWhat happened, in a sentence.
Example request body
{
    "reference": "SBX-BAS-000418"
}
POST /sandbox/reset idempotent

Reseed the sandbox

Removes the transactions, documents, contracts and vouchers the fixtures took part in and rebuilds the cast. The published ABSSINs themselves survive — they are documented, and your stored test data points at them. Three per hour, per client; it also runs nightly on its own.

  • The body is empty. There is no confirmation flag: the Idempotency-Key is what stops a retry resetting twice, and the throttle is what stops a loop.
  • Every transaction, document, contract and voucher you were holding is gone afterwards. That is the point: a test that depends on yesterday's data is not a test.
Response data
FieldTypeNotes
purged *objectWhat was removed, counted by kind.
seeded *objectWhat was rebuilt: parties, certificates, directorships, rates, thresholds, the process, and any warnings.
identities *array of objectThe cast now available, as GET /sandbox/identities returns it.

Queue

Requests accepted in degraded mode, and what became of them.

GET /queue/{ref}

Follow a request accepted in degraded mode

When a dependency the platform does not own is down, a submission, a document request, a payment citation, a voucher raise or a corporate registration is answered 202 QUEUED_DEGRADED with a queue reference instead of being judged. Queued is not approved: the request carries no verdict, and when service resumes it is processed through exactly the checks a live call goes through. This call says where it stands and, once it has been processed, what the answer was.

  • Any active key may call it, but a client sees only the requests IT queued, in its own environment. Another client's reference, a counter's, or one that does not exist is NOT_FOUND alike.
  • status: queued (waiting), draining (being processed now), done (the service answered — outcome.ok says whether it was accepted or refused), failed (it could not be processed at all; submit it again) or abandoned.
  • Do not issue, pay or act on a queued request. A document number, voucher number or party reference exists only once outcome carries one.
  • Prefer the queue.processed webhook to tight polling: the queue is drained every minute once the dependency has recovered. The webhook is sent for live requests only; in the sandbox, poll.
Path
{ref}
The queue reference from the 202 answer, QUE-YYYY-XXXXXX.
Response data
FieldTypeNotes
queue_ref *stringQUE-YYYY-XXXXXX.
kind *string (transaction_submit | document_request | payment_evidence | pv_raise | party_register)What was queued.
status *string (queued | draining | done | failed | abandoned)Where it stands.
message *stringThe position in a sentence, safe to show an officer. While queued it says not to issue the document.
subject_refstring, nullableThe case, award number or RC number the request is about; for a queued submission, the transaction reference once it has been opened.
queued_atstring, nullableWhen it was accepted.
next_attempt_atstring, nullableWhile queued: the earliest it will be looked at again.
completed_atstring, nullableWhen it reached done, failed or abandoned.
attempts *integerProcessing attempts that could not complete. Waiting for a dependency uses none.
outcomeobject, nullableNull while queued or draining; afterwards, the service's answer.
outcome.okbooleanTrue when the request was accepted.
outcome.outcomestringaccepted, refused, failed or abandoned.
outcome.reason_codestring, nullableThe published code when it was not accepted.
outcome.messagestringWhat the service said.
outcome.txn_refstringThe transaction, for a submission, document request or payment.
outcome.statusstringThe transaction status after a submission.
outcome.blockingarray of objectGates still blocking, when there are any.
outcome.doc_nostringThe final document number, when one was issued.
outcome.doc_no_shortstringIts short form.
outcome.validationstringThe ledger's verdict on a payment reference.
outcome.pv_nostringThe voucher number, when one was raised.
outcome.party_refstring, nullableThe party, for a registration.
queue_url *stringThis resource.
Reason codes
Example data (inside the envelope)
{
    "queue_ref": "QUE-2026-7K2M9P",
    "kind": "document_request",
    "status": "done",
    "message": "Processed.",
    "subject_ref": "BTX-2026-004871",
    "queued_at": "2026-09-14 09:12:44",
    "next_attempt_at": null,
    "completed_at": "2026-09-14 09:41:02",
    "attempts": 0,
    "outcome": {
        "ok": true,
        "outcome": "accepted",
        "txn_ref": "BTX-2026-004871",
        "doc_no": "AB/LND/COFO/2026/4F2K7C/TCC-2026-004418",
        "doc_no_short": "AB-LND26-4F2K7C",
        "message": "Issued: AB/LND/COFO/2026/4F2K7C/TCC-2026-004418"
    },
    "queue_url": "https://services.abiastate.gov.ng/api/v1/queue/QUE-2026-7K2M9P"
}

Warehouse

The nightly anonymised data-warehouse extracts. Live keys with the dw scope only.

GET /dw/extracts scope: dw

List the recent data-warehouse extracts

One extract per day: a gzipped JSON-lines file per dataset and a manifest naming each file with its row count and sha256. Each day is a delta — the rows created or changed inside that day — so a rebuilt day reproduces itself and a warehouse can replay a gap. No file carries a national identity number, bank verification number, TIN, phone or email.

  • Live keys only. A sandbox key is refused with SCOPE_MISSING even when it holds dw: the warehouse is fed from live data.
  • Newest first. ?limit= takes 1 to 60 days; the default is 30, which is also the retention.
  • available is true when the day is ready AND its files are still on disk; only then is manifest_url set.
  • complete=false marks a day extracted before it ended. It will be rebuilt, so collect it again.
Response data
FieldTypeNotes
env *string (live)Always live.
retention_days *integerHow long an extract is kept before it is deleted.
datasets *array of stringEvery dataset an extract carries, in file order.
extracts *array of objectOne entry per day.
extracts[].date *stringYYYY-MM-DD.
extracts[].status *string (building | ready | failed | expired)Where the day's build stands.
extracts[].available *booleanWhether it can be collected now.
extracts[].completeboolean, nullableFalse when the day had not ended when it was extracted.
extracts[].schema_versioninteger, nullableBumped when a dataset's shape changes in a way a consumer must notice.
extracts[].file_count *integerFiles in the extract.
extracts[].total_rows *integerRows across every file.
extracts[].total_bytes *integerCompressed bytes across every file.
extracts[].built_atstring, nullableWhen the build finished.
extracts[].expires_atstring, nullableWhen retention deletes it.
extracts[].manifest_urlstring, nullableGET /dw/extracts/{date}, when available.
Reason codes
Example data (inside the envelope)
{
    "env": "live",
    "retention_days": 30,
    "datasets": [
        "parties",
        "tcc",
        "transactions"
    ],
    "extracts": [
        {
            "date": "2026-09-13",
            "status": "ready",
            "available": true,
            "complete": true,
            "schema_version": 1,
            "file_count": 12,
            "total_rows": 18422,
            "total_bytes": 1204116,
            "built_at": "2026-09-14 01:15:09",
            "expires_at": "2026-10-14 01:15:09",
            "manifest_url": "https://services.abiastate.gov.ng/api/v1/dw/extracts/2026-09-13"
        }
    ]
}
GET /dw/extracts/{date} scope: dw

Read one day's extract manifest

The manifest exactly as the extract wrote it — window, completeness, privacy statement and every file — with a download URL added to each file. The manifest is written last, so its presence is what says the day's set is whole.

  • Verify each file against its sha256 after download. The platform checks it before sending as well, and never sends a file that does not match.
  • Money and other decimal columns are JSON strings inside the files, so no precision is lost in transit.
  • A day that was never built, has expired, or is being rebuilt right now is NOT_FOUND. Try again later for a rebuild.
Path
{date}
The extract day, YYYY-MM-DD.
Response data
FieldTypeNotes
date *stringThe day asked for.
manifest *objectThe manifest.
manifest.schema_version *integerDataset shape version.
manifest.env *stringlive.
manifest.extract_date *stringYYYY-MM-DD.
manifest.window *objectfrom, to and the basis of the delta.
manifest.complete *booleanFalse when extracted before the day ended.
manifest.generated_at *stringWhen the extract was written.
manifest.privacy *stringWhat the files never carry.
manifest.numbers *stringHow decimals are encoded.
manifest.file_count *integerFiles listed below.
manifest.total_rows *integerRows across every file.
manifest.total_bytes *integerCompressed bytes across every file.
manifest.files *array of objectOne entry per dataset file.
manifest.files[].name *string{dataset}.jsonl.gz.
manifest.files[].rows *integerJSON lines in the file.
manifest.files[].bytes *integerCompressed size.
manifest.files[].sha256 *stringHex sha256 of the compressed file.
manifest.files[].url *stringGET /dw/extracts/{date}/{name}.
GET /dw/extracts/{date}/{name} scope: dw

Download one extract file

The file itself, as application/gzip — not a JSON envelope. The platform reads the file, checks its sha256 against the manifest and sends exactly the bytes it checked. Refusals are still the usual JSON envelope.

  • The name must be one the manifest lists for that date; anything else is NOT_FOUND.
  • X-Content-SHA256 carries the verified hash and X-Extract-Rows the line count. Cache-Control is private, no-store: do not put a caching proxy in front of this call.
  • A file that fails its integrity check is not sent: the answer is a retryable INTERNAL_ERROR and the day is rebuilt by the Board.
  • Files larger than 48 MB are not served over the API and are refused with VALIDATION_ERROR; collect those by SFTP.
Path
{date}
The extract day, YYYY-MM-DD.
{name}
A file name from that day's manifest.