Nigeria ๐ณ๐ฌ
E-invoicing in Nigeria runs through the Nigeria Revenue Service (NRS), formerly the Federal Inland Revenue Service (FIRS), on its e-invoicing platform MBS (Merchant Buyer Solution, also written NRSMBS; some NRS hosts still carry the older FIRS MBS name). Nigeria uses a clearance model: an invoice exists only once NRS has validated and signed it. NRS then exchanges the signed invoice between the supplier's and the buyer's access points.
This page is the API documentation for the platform's Nigeria e-invoicing endpoints: every endpoint, what it does with NRS, and its request and response shapes. For one invoice the platform does the following:
- Issue. Validate the invoice with NRS, have NRS sign it (NRS registers the IRN), build the QR code with the supplier business's own NRS keys when the business has them, and record the invoice. This is one synchronous call.
- Transmit. Ask NRS to deliver the signed invoice to the buyer's access point. Only the supplier's own access point can transmit, so an invoice whose supplier is on another access point is not transmitted from here. Consumer (B2C) invoices are not transmitted.
- Track. Read the invoice's status from NRS until NRS reports it delivered and acknowledged, and report payment status changes to NRS.
- Receive. Take NRS's notifications (the webhook) and NRS's queue of waiting invoices (pull) for the businesses the platform serves, check each invoice with NRS, fetch it, and acknowledge receipt to NRS.
The endpoints on this page are served only by a GoRoute service that has its Nigeria pack switched on. Check yours
before you build anything on them. Any valid API key can make this call; it needs no special permission ($API and
$KEY are explained in Authentication and permissions):
curl -i -H "X-API-Key: $KEY" "$API/api/v1/jurisdictions/NG/settings"
- 200 means the Nigeria pack is on for your service, and this page applies to it.
- 404 with
error_codeJURISDICTION_NOT_AVAILABLEmeans it is not. The calls on this page are not available on that service as described here, whatever your key or permissions.
{
"error": "not_found",
"error_code": "JURISDICTION_NOT_AVAILABLE",
"message": "No jurisdiction pack 'NG' is available on this platform.",
"request_id": "req_4f5a6b7c8d9e40a1b2c3d4e5f6a7b8c9"
}
The same check for any country is described on Jurisdiction Settings. What this call returns when the pack is on is in Where the settings live.
"Acme Supplies Ltd" (TIN 12345678-0001, Service ID A1B2C3D4), "Bright Retail Ltd", "Delta Components Ltd",
"Example Provider Ltd", and every UUID, key and IRN below are made up for this page. They are not NRS
registrations. Your own values come from the NRS portal and from GET /api/v1/nigeria/businesses.
Overviewโ
| Aspect | Details |
|---|---|
| Tax authority | Nigeria Revenue Service (NRS), formerly the Federal Inland Revenue Service (FIRS) |
| E-invoicing platform | NRS MBS: einvoice.nrs.gov.ng |
| Model | Clearance. NRS validates and signs every invoice before it is issued and registers its IRN; NRS then delivers the signed invoice from the supplier's access point to the buyer's |
| Service-provider roles | System Integrator: connects taxpayers' systems to MBS and onboards their businesses. Access Point: transmits and receives invoices through MBS. The platform performs both roles, each with its own NRS key (Roles and keys) |
| Identifiers | TIN (Taxpayer Identification Number). NRS also gives each business a business_id (UUID) and an 8-character Service ID |
| Invoice reference | IRN: {invoice number}-{Service ID}-{YYYYMMDD}, for example INV1001-A1B2C3D4-20261012 |
| Document format | NRS invoice JSON (UBL-aligned field names). The platform also keeps a UBL XML rendering of every invoice for preview |
| Invoice types | NRS's own list: 381 Commercial Invoice, 380 Credit Note, 384 Debit Note, 396 Invoice Request (details) |
| Invoice kinds | invoice_kind: B2B, B2C or B2G |
| QR code | NRS's rule: mandatory. The IRN and the supplier business's own NRS certificate, encrypted with NRS's public key. Anyone can read it back through NRS verify_stamp. The platform builds it from the business's NRS keys. A service provider's own account business that NRS issued no keys to is signed without one (The QR code) |
| Environments | Sandbox and production |
| Peppol statistics | NRS exchanges are not Peppol traffic: the platform leaves every NRS invoice, sent or received, out of the OpenPeppol Transaction Statistics Report (TSR) and End User Statistics Report (EUSR) |
| NRS documentation | einvoice.nrs.gov.ng/docs |
How an invoice movesโ
SUPPLIER SIDE: the supplier's system calls POST /api/v1/nigeria/invoices
Platform NRS MBS
0 where the supplier is โ AP key โถ invoice/transmit/lookup/tin/{TIN}
1 validate โ signing key (*) โโโโถ invoice/validate
2 sign โ signing key (*) โโโโโโโโถ invoice/sign (IRN registered)
3 QR from own keys, checked โโโโโถ integration/verify_stamp (only when the business has QR keys)
4 record the invoice and its legs
โ answers 200 { ok, irn, qr, qr_png_base64, legs, ... }
nrs_transmit leg โ AP key โโโโโโโถ invoice/transmit/{IRN} (only for a supplier on this access point)
nrs_status โ taxpayer or AP key โถ invoice/confirm/{IRN} (polled)
(*) the organisation's taxpayer key; for one of a service provider's own account
businesses, that account's own SI or AP key
BUYER SIDE: NRS calls POST /api/v1/public/nrs/webhook (answered 200)
Platform NRS MBS
re-read the IRN โ AP key โโโโโโโโถ invoice/transmit/lookup/{IRN}
pull every 5 minutes โ AP key โโโถ invoice/transmit/pull
nrs_receive leg:
fetch โ AP key, else buyer's taxpayer key โถ invoice/download/{IRN}
check the QR, keep a UBL preview
acknowledge โ AP key โโโโโโโโโโถ invoice/transmit/{IRN} (PATCH)
Steps 0 to 4 happen inside one API call. Step 0 runs only for an invoice to be transmitted, and its answer never
stops the invoice being issued (Issuing an invoice). Everything after the call runs as
legs: background jobs that call NRS, retry on failure and record their outcome on the invoice. A sent invoice has
an nrs_transmit leg (unless it is not transmitted, or its supplier is on another access point) and an nrs_status
leg; a received invoice has an nrs_receive leg.
Roles and keysโ
NRS issues three kinds of API key, each an x-api-key and x-api-secret pair. Every NRS call accepts one kind
only. Called with the wrong kind, NRS answers 403 with handler: entity_actions, which the platform reports as
C5_NG_WRONG_KEY_ROLE.
| Key | Usually held by | The platform uses it to | Inherited from a parent organisation |
|---|---|---|---|
| Taxpayer key | Each taxpayer organisation, for its own invoices | Validate, sign, download and confirm the organisation's own invoices; report payment status; read NRS's reference lists | Never |
| System Integrator (SI) key | The service provider | List the businesses of the NRS account and onboard new ones (NRS entities and businesses); validate and sign as the service provider's own SI account businesses | Yes, except to sign as an account business |
| Access Point (AP) key | The service provider | Transmit invoices; look up the supplier's access point before transmitting; look up an IRN's two parties, or a TIN; pull waiting invoices; download invoices addressed to a buyer on its access point; acknowledge received invoices; confirm any IRN; NRS's self-health-check; validate and sign as the service provider's own AP account businesses | Yes, except to sign as an account business |
Inheritance. Organisations on the platform form a hierarchy: a service provider (the operator) and the client
organisations under it. For the SI and AP keys the platform uses the organisation's own key first; if it has none,
it uses the nearest parent organisation's key (at most 12 levels up), always with the NRS base URL of the
organisation that holds the key. A taxpayer key and a business's QR keys are never inherited: each taxpayer stores
its own. A key that signs as one of a service provider's own account businesses (Businesses) must be
the organisation's own SI or AP key, never one it inherits. GET /api/v1/nigeria/settings names the organisation an
SI or AP key is inherited from (si_ap_inherited_from).
Storage. All of an organisation's NRS secrets (the three key pairs and every business's QR keys) are held as one
record in the platform's credential store, encrypted with AWS KMS envelope encryption: a new 256-bit data key per
record, AES-256-GCM, with the organisation and the record bound in as the encryption context. Saves take a lock per
organisation, so two concurrent saves never lose a key. When the credential store is not configured on a platform,
saving a key is refused with 503 C5_NG_CREDENTIAL_STORE_UNAVAILABLE; there is no weaker fallback.
No endpoint returns a key, a secret or a certificate. GET /api/v1/nigeria/settings reports which keys are present
(has_taxpayer_key, has_si_key, has_ap_key) and shows the taxpayer key masked to its first and last four
characters; a business's QR keys appear only as has_keys. Keys are not written to the organisation's settings, to
logs or to error messages. Keys saved before this store existed are read from the older encrypted settings and moved
into the store at the organisation's next settings save, business refresh or onboarding.
Authentication and permissionsโ
Every call carries the organisation's platform API key. This is not an NRS key: the NRS keys stay in the credential store and are never sent per call.
X-API-Key: pk_live_your_api_key_here
Content-Type: application/json
The examples use $API for your platform's API base URL, which is the portal host followed by /peppol-api (for
example https://einvoicing.example.com/peppol-api), and $KEY for the platform API key. The live OpenAPI
description is at $API/openapi.json. The Nigeria endpoints are served only on a platform where Nigeria
e-invoicing is enabled.
| Permission column below | Who has it |
|---|---|
| authenticated | Any valid API key or signed-in user of the organisation |
invoices:send | An API key with the send or admin scope, or a user whose role grants invoices:send |
settings:manage | An API key with the admin scope, or a user whose role grants settings:manage |
A caller without the permission gets 403:
{
"error": "forbidden",
"error_code": "INSUFFICIENT_PERMISSION",
"message": "This action requires the 'invoices:send' permission.",
"request_id": "req_3b1f0c8e2d7a4c5f9e6b1a2d3c4e5f60"
}
Endpointsโ
| Method | Path | Permission | Purpose |
|---|---|---|---|
GET | /api/v1/nigeria/settings | authenticated | The organisation's NRS configuration: which keys are stored (never their values), whose keys are inherited, the default business, the webhook URL to register with NRS |
PUT | /api/v1/nigeria/settings | settings:manage | Store the taxpayer, SI and AP keys; set the NRS base URL, the default business and auto-acknowledge (the role: a super admin only); re-discover the businesses |
GET | /api/v1/nigeria/businesses | authenticated | The organisation's NRS businesses; ?refresh=true re-discovers them from NRS |
POST | /api/v1/nigeria/businesses | settings:manage | Onboard a taxpayer business with NRS (SI key) |
PUT | /api/v1/nigeria/businesses/{business_id}/keys | settings:manage | Store a business's QR keys (public_key and certificate from its crypto_keys.txt) |
GET | /api/v1/nigeria/resources/{name} | authenticated | NRS reference lists (states, LGAs, invoice types, ...), cached 24 hours |
POST | /api/v1/nigeria/health-check | settings:manage | NRS's self-health-check with the AP key; NRS then posts a test notification to the webhook |
GET | /api/v1/nigeria/lookup/tin/{tin} | invoices:send | Ask NRS whether a buyer's TIN is reachable through an access point (AP key) |
POST | /api/v1/nigeria/invoices | invoices:send | Issue an invoice: validate, sign, QR and record it, then transmit it and track its status |
GET | /api/v1/nigeria/invoices | authenticated | Invoices issued through NRS, newest first, paged |
GET | /api/v1/nigeria/invoices/{transaction_id} | authenticated | One issued invoice with its QR, legs and NRS status; ?refresh=true reads NRS first |
POST | /api/v1/nigeria/invoices/{transaction_id}/transmit | invoices:send | Transmit now (also for an invoice issued with transmit: false) |
PATCH | /api/v1/nigeria/invoices/{transaction_id}/payment | invoices:send | Report the payment status to NRS |
POST | /api/v1/nigeria/validate | invoices:send | Validate with NRS without issuing; with sign: true, issue without transmitting |
POST | /api/v1/nigeria/qr | invoices:send | Build and check the QR code for an IRN with the business's own keys |
POST | /api/v1/nigeria/decrypt | invoices:send | Download a signed invoice from NRS and decrypt it (taxpayer key, then the organisation's own AP key) |
GET | /api/v1/nigeria/received | authenticated | The inbox: invoices received through NRS, newest first, paged |
GET | /api/v1/nigeria/received/{transaction_id} | authenticated | One received invoice with its content and parties (read-only) |
POST | /api/v1/nigeria/received/{transaction_id}/acknowledge | invoices:send | Acknowledge receipt to NRS (AP key) |
POST | /api/v1/nigeria/received/pull | invoices:send | Pull waiting invoices from NRS now (AP key) |
POST | /api/v1/public/nrs/webhook | none (called by NRS) | NRS's notifications; always answers 200 |
On a white-label operator's portal host the same webhook is also served at https://{portal host}/nrs/webhook
(The webhook). The tenant part of the settings can also be written with the platform's generic
PUT /api/v1/jurisdictions/NG/settings (Where the settings live).
Errorsโ
The platform answers in three ways when a call does not succeed.
1. Refused by the platform (a missing key or setting, an invalid input, a conflict, NRS unreachable or
refusing a call other than validate or sign). The HTTP status is 4xx or 5xx and the body names a reason code:
{
"code": "C5_NG_BILLING_REFERENCE_REQUIRED",
"error": "C5_NG_BILLING_REFERENCE_REQUIRED",
"message": "A credit note (380) or debit note (384) must reference the invoice it corrects: add billing_reference with that invoice's IRN and issue date.",
"invoice_type_code": "380",
"issues": [
"Invoice type 380 (Credit Note) needs billing_reference: [{irn, issue_date}] of the invoice it corrects."
],
"request_id": "req_3b1f0c8e2d7a4c5f9e6b1a2d3c4e5f60"
}
code and error carry the same reason code; message is a sentence to show the user; some refusals add fields
(irn, transaction_id, business_id, missing, needs_keys, leg_status, issues, tin, invoice_kind,
supplier_app_reference).
request_id is also returned in the X-Request-ID response header.
| HTTP | Meaning |
|---|---|
400 | A key, a business or a setting is missing, or the input is refused (reason code in the body) |
403 | The caller lacks the permission (above) |
404 | No such invoice in this organisation ("message": "Transaction not found"), or an unknown resource list |
409 | A conflict: the IRN was issued already; the invoice is not owed a transmission, was transmitted already, or its transmission has ended; the supplier's business is on another access point, which alone can transmit the invoice (C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT); the business belongs to another organisation; receiving is switched off; or the organisation's NRS settings cannot be read |
422 | An invalid value: invoice_kind other than B2B, B2C, B2G (C5_NG_INVOICE_KIND_INVALID), a malformed TIN to look up, or a role change by anyone but a super admin (C5_NG_ROLE_PLATFORM_OWNED); or a request body without the documented shape (standard validation error with detail[]) |
502 | NRS failed the call: unreachable, or 5xx or 429, after the retries (C5_NG_NRS_UNREACHABLE); a sign whose outcome NRS cannot confirm (C5_NG_SIGN_OUTCOME_UNKNOWN); any other NRS error outside validate and sign (C5_NG_NRS_ERROR); an NRS answer that cannot be read (C5_NG_NRS_BAD_RESPONSE) |
503 | C5_NG_CREDENTIAL_STORE_UNAVAILABLE: the encrypted credential store is not configured on this platform, or the organisation's stored keys cannot be read when a write or a change of NRS host needs them |
2. Refused by NRS. When NRS itself refuses an invoice (validate or sign), a payment status or an
acknowledgement, the HTTP status is 200 with "ok": false. issues[] holds NRS's field-level messages made
readable (NRS's error.details, not its generic public_message), and nrs_error holds NRS's error as it came
back. See Refusals for a full example.
3. Business-key refusals. PUT /api/v1/nigeria/businesses/{business_id}/keys reports a refused key as 200 with
"ok": false and a message.
No route answers 500 because of what NRS answered. An NRS error outside validation and signing (for example to
a status read, the health check, the pull or a reference list) is a 502 that carries NRS's own error:
{
"code": "C5_NG_NRS_ERROR",
"error": "C5_NG_NRS_ERROR",
"message": "NRS refused the request; its handler, message and details say why.",
"nrs_status": 400,
"handler": "invoice_actions",
"public_message": "bad request",
"details": "invoice.irn: not found for this business",
"nrs_error_id": "3c2b1a09-8f7e-4d6c-9b5a-4e3d2c1b0a99",
"request_id": "req_6e5d4c3b2a1940f8e7d6c5b4a3928170"
}
Reason codesโ
| Reason code | When |
|---|---|
C5_NG_TAXPAYER_CREDENTIALS_REQUIRED | The organisation has no taxpayer key: needed to validate and sign as a taxpayer-account business. Also answered when a call that can fall back to another key finds none (download, confirm, reference lists) |
C5_NG_SI_CREDENTIALS_REQUIRED | No SI key on the organisation or any parent (needed to onboard a business); or, to validate and sign as one of the service provider's own SI account businesses, the organisation has no SI key of its own (an inherited one does not count) |
C5_NG_AP_CREDENTIALS_REQUIRED | No AP key on the organisation or any parent (needed to transmit, pull, acknowledge); a leg waiting for it resumes once the key is stored. Also: to validate and sign as one of the service provider's own AP account businesses, the organisation has no AP key of its own (an inherited one does not count) |
C5_NG_CREDENTIAL_STORE_UNAVAILABLE | The encrypted credential store is not configured on this platform |
C5_NG_WRONG_KEY_ROLE | NRS refused the key for this call: a key of another kind is stored in that field |
C5_NG_UNSAFE_BASE_URL | The NRS base URL points at a host the platform will not connect to |
C5_NG_NRS_ERROR | NRS answered an error to a call other than validate or sign; nrs_status, handler, public_message and details carry NRS's error |
C5_NG_NRS_BAD_RESPONSE | NRS answered with something that could not be read (for example a created business without an id) |
C5_NG_KEYS_REQUIRED_FOR_NEW_BASE_URL | A new base URL was sent without every stored key re-entered, key and secret (missing lists them) |
C5_NG_ROLE_PLATFORM_OWNED | A role change from anyone but a super admin (422); nothing in the request was written |
C5_NG_DEFAULT_HSN_CODE_INVALID | A default_hsn_code sent to the settings that is not in 0000.00 format (422); nothing in the request was written |
C5_NG_DEFAULT_PRODUCT_CATEGORY_INVALID | A default_product_category longer than 255 characters (422); nothing in the request was written |
C5_NG_NOT_ENROLLED | The organisation's NRS settings cannot be read (save them again), or receiving is switched off on this platform; from the generic paths, an organisation in Nigeria whose NRS settings were never saved |
C5_NG_BUSINESS_REQUIRED | No business named and no default business set |
C5_NG_BUSINESS_NOT_FOUND | The business named is not one of the organisation's NRS businesses, or it has no usable Service ID; also an onboarding request without a name or TIN |
C5_NG_DEMO_BUSINESS | The business named is NRS's demo business, which never issues invoices, signs QR codes or becomes the default |
C5_NG_BUSINESS_HELD_BY_ANOTHER_ORG | Onboarding found the TIN's business already registered, and another organisation under the same service provider lists it; or the service provider tried to issue as one of its account businesses that another organisation under it lists (409) |
C5_NG_BUSINESS_KEYS_REQUIRED | A taxpayer-account business has no QR keys stored, so it cannot issue (also POST /api/v1/nigeria/qr for any business without keys); or QR keys were sent to the settings instead of to the business |
C5_NG_NO_BUSINESS_KEYS | Not a refusal: the invoice was issued as one of the service provider's own account businesses, which has no NRS keys, so it is signed without a QR code ("qr": "not_available"). NRS issues keys only to approved businesses |
C5_NG_SAMPLE_CERTIFICATE | The certificate is NRS's published sample, not the business's own |
C5_NG_BILLING_REFERENCE_REQUIRED | A credit note (380) or debit note (384) without billing_reference |
C5_NG_CUSTOMER_TIN_REQUIRED | accounting_customer_party.tin is missing (required for B2C too); also a TIN to look up that is not letters, digits and hyphens (422) |
C5_NG_INVOICE_KIND_INVALID | invoice_kind is not B2B, B2C or B2G (422) |
C5_NG_PAYMENT_DUE_DATE_REQUIRED | A payment_means entry has no payment_due_date and the invoice has no due_date to give it. NRS's validate accepts such an invoice, but its sign refuses it with a generic "try again later" |
C5_NG_INVOICE_NUMBER_INVALID | The invoice number is missing, or is not letters, digits and single inner hyphens of at most 64 characters |
C5_NG_IRN_MISMATCH | An IRN or Service ID was sent that is not the business's own, and there is no invoice number to rebuild it from |
C5_NG_DUPLICATE_IRN | The IRN was already issued through the platform, or NRS reports it signed already and does not confirm holding it for this key; also "transmitted already" |
C5_NG_GENERIC_MAPPING | From the generic paths: the invoice lacks what NRS requires (an HSN code on a line, with no organisation default either; an email, an address field, the buyer's TIN, ...). Answered 422 with issues[]; NRS is not called |
C5_NG_VALIDATION_REJECTED | NRS refused the invoice at validation (see issues[]); also an invalid payment status request |
C5_NG_SIGN_REJECTED | NRS refused to sign the invoice (see issues[]) |
C5_NG_NRS_UNREACHABLE | NRS could not be reached (or answered 5xx / 429) after the retries. While issuing, it means nothing was issued |
C5_NG_SIGN_OUTCOME_UNKNOWN | NRS stopped answering while signing and could not confirm whether it signed; nothing was recorded |
C5_NG_TRANSMIT_NOT_OWED | The invoice is B2C, or transmission is switched off on this platform |
C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT | Only the supplier's access point can transmit the invoice, and the supplier's business is on another one. No transmission is made from here: the nrs_transmit leg is not created, or ends skipped; Transmit now answers 409. NRS holds the signed invoice. The reason names the supplier's access point |
C5_NG_TRANSMIT_NOT_AUTHORISED | NRS answered 403 to the transmission (leg reason) |
C5_NG_TRANSMIT_FAILED | NRS refused the transmission (the leg ends failed on the third refusal), or reported it failed and it is being transmitted again (leg reason) |
C5_NG_AWAITING_DELIVERY | NRS has not yet reported the invoice delivered and acknowledged; the status leg checks again (leg reason, not a failure) |
C5_NG_STATUS_FAILED | The status could not be read from NRS after three refusals (leg reason) |
C5_NG_STATUS_TIMEOUT | NRS did not report the invoice delivered within 30 days (leg reason) |
C5_NG_NOT_ACKNOWLEDGED_BY_BUYER | Delivered, but not acknowledged by the buyer within 30 days (leg reason) |
C5_NG_RECEIVE_FAILED | A received invoice could not be fetched after three refusals (leg reason) |
C5_NG_CONTENT_PENDING | A received invoice whose content could not be fetched yet, with the access point's key or the buyer organisation's taxpayer key; it is fetched again on its own for 7 days |
C5_NG_RECEIVED_UNASSIGNED | A received invoice that matches none of the operator's organisations |
C5_NG_LEGACY_RECORD | An invoice signed before transmission tracking existed: nothing is transmitted or tracked for it |
Every leg shows its reason code with a one-sentence reason beside it.
Setting upโ
Before the first invoice, three things are in place:
| What | Where it comes from | Stored with |
|---|---|---|
NRS keys: taxpayer, SI, AP (x-api-key + x-api-secret each) | NRS portal, API Integration. Generating a new key there replaces the old pair | PUT /api/v1/nigeria/settings |
The business: business_id, Service ID, TIN | Assigned by NRS when the business is created | Discovered from NRS, or created with POST /api/v1/nigeria/businesses |
The business's QR keys: public_key and certificate from its crypto_keys.txt | NRS portal: System Integrator, API Integration, Approved Business list, Get Cryptographic Keys on that business's row | PUT /api/v1/nigeria/businesses/{business_id}/keys |
POST /api/v1/nigeria/businesses registers the business with NRS. It does not put the business on the portal's
Approved Business list, and only that list issues the business's crypto_keys.txt. Until NRS approves the
business there are no QR keys to store, so it cannot issue invoices yet.
NRS's QR code page says to download crypto_keys.txt
from My Account, Manage Cryptographic Keys. The current portal has no such menu; use the Approved Business row.
Settingsโ
PUT /api/v1/nigeria/settings (settings:manage) stores the organisation's NRS keys and settings. Only what the
request names changes; secrets are never echoed back. Fields that are not listed below (a business list, for
example) are ignored.
| Field | Notes |
|---|---|
taxpayer_api_key, taxpayer_api_secret | The organisation's own taxpayer key. The older names api_key and api_secret set the same key |
si_api_key, si_api_secret | System Integrator key |
ap_api_key, ap_api_secret | Access Point key |
base_url | NRS base URL; default the sandbox. Platform-owned, and changed only through this route's checks. A host that resolves to a private, loopback or link-local address is refused (C5_NG_UNSAFE_BASE_URL). Changing it requires every key the organisation has stored, key and secret, in the same request: half a pair does not count, because the stored half would travel with it (400 C5_NG_KEYS_REQUIRED_FOR_NEW_BASE_URL, with missing naming the keys). If the stored keys cannot be read, a change of host is refused with 503 C5_NG_CREDENTIAL_STORE_UNAVAILABLE. A stored key is never sent to a host it was not entered for |
default_business_id | The business an invoice is issued as when the request names none. Must be one of the organisation's businesses after discovery (C5_NG_BUSINESS_NOT_FOUND) and never NRS's demo business (C5_NG_DEMO_BUSINESS). The older name business_id is accepted |
role | service_provider or taxpayer. Platform-owned (Role): only a super admin acting on the organisation may change it. Anyone else sending a different value gets 422 C5_NG_ROLE_PLATFORM_OWNED and nothing in the request is written, keys included; sending the current value changes nothing |
auto_acknowledge | Default true: acknowledge a received invoice to NRS as soon as its content is fetched (Acknowledging) |
default_hsn_code | The HSN code (0000.00, for example 1006.30) of an invoice line from the generic paths that carries none of its own (Invoice-line defaults). Empty or null removes it; any other format is refused with 422 C5_NG_DEFAULT_HSN_CODE_INVALID and nothing in the request is written |
default_product_category | The product category of such a line, at most 255 characters (422 C5_NG_DEFAULT_PRODUCT_CATEGORY_INVALID otherwise). Empty or null removes it, and each line's description is used |
enabled | Saving the settings enrols the organisation in NRS e-invoicing; false withdraws it |
service_id | Ignored: the Service ID comes from the business |
nrs_public_key, certificate_b64 | Refused with 400 C5_NG_BUSINESS_KEYS_REQUIRED (or C5_NG_SAMPLE_CERTIFICATE): QR keys belong to one business and are stored with PUT /api/v1/nigeria/businesses/{business_id}/keys |
For each key, send both halves. A request carrying only one half keeps the stored other half; with nothing stored
for that key, both are required (400 C5_NG_TAXPAYER_CREDENTIALS_REQUIRED, C5_NG_SI_CREDENTIALS_REQUIRED or
C5_NG_AP_CREDENTIALS_REQUIRED). Both halves sent as empty strings remove that key.
After saving, the platform re-discovers the organisation's NRS businesses. It tries the organisation's own SI key,
then its own taxpayer key, then an SI key inherited from a parent organisation. An inherited key only refreshes the
businesses the organisation already lists, so a client organisation never sees the operator's other clients. A
service_provider that holds its own AP key also lists the businesses under its own Access Point account (the NRS
entities filed under its access point), except any business another organisation under it already lists; an
inherited AP key lists nothing, and a failed listing removes nothing. Discovery reads up to 20 pages of NRS's
entity list. Discovery never removes a business's stored QR keys. If NRS cannot be reached or refuses the key, the settings are
still saved and discovery_message says what happened. Saving a taxpayer key also makes every received invoice of
the organisation that is waiting for its content fetch it at once (Content pending).
The default business is chosen on every save and every read: the stored default while the organisation still has that business; otherwise the organisation's only business; otherwise none, and each invoice must name its business. NRS's demo business is never the default.
A service provider (whose role the platform has set to service_provider) stores its SI and AP keys:
{
"si_api_key": "<x-api-key of the System Integrator key>",
"si_api_secret": "<x-api-secret of the System Integrator key>",
"ap_api_key": "<x-api-key of the Access Point key>",
"ap_api_secret": "<x-api-secret of the Access Point key>",
"base_url": "https://eivc-k6z6d.ondigitalocean.app"
}
A client organisation under it stores its own taxpayer key and inherits the SI and AP keys:
curl -X PUT "$API/api/v1/nigeria/settings" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{
"taxpayer_api_key": "<x-api-key of the taxpayer key>",
"taxpayer_api_secret": "<x-api-secret of the taxpayer key>",
"default_business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"auto_acknowledge": true
}'
PUT answers with the settings as GET /api/v1/nigeria/settings (authenticated) returns them:
{
"configured": true,
"api_key_masked": "7c2e...9b41",
"has_api_secret": true,
"base_url": "https://eivc-k6z6d.ondigitalocean.app",
"service_id": "A1B2C3D4",
"business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"has_public_key": true,
"has_certificate": true,
"business_name": "Acme Supplies Ltd",
"tin": "12345678-0001",
"entity_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"discovered": true,
"discovery_message": null,
"has_taxpayer_key": true,
"has_si_key": true,
"has_ap_key": true,
"si_ap_inherited_from": "Example Provider Ltd",
"role": "taxpayer",
"webhook_url": "https://einvoicing.example.com/nrs/webhook",
"auto_acknowledge": true,
"default_business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"enabled": true,
"credential_store_available": true,
"default_hsn_code": "1006.30",
"default_product_category": null
}
| Field | Meaning |
|---|---|
configured, has_api_secret, has_taxpayer_key | A taxpayer key is stored for this organisation |
api_key_masked | The taxpayer x-api-key, first and last four characters only |
has_si_key, has_ap_key | An SI or AP key is available, the organisation's own or inherited |
si_ap_inherited_from | The organisation whose SI or AP key is used, when it is not this one |
business_id, service_id, business_name, tin, entity_id | The default business; all null when there is none |
has_public_key, has_certificate | The default business's QR keys are stored |
discovered | The default business has a Service ID from NRS |
discovery_message | Why discovery did not run or found no default; null otherwise |
webhook_url | The https URL to register in the NRS portal (The webhook) |
credential_store_available | false when the encrypted credential store is not configured or cannot be read on this platform |
default_hsn_code, default_product_category | The invoice-line defaults; null when not set |
Where the settings liveโ
An organisation's NRS settings are kept in two parts, split by who may write them:
| Part | Fields | Written by |
|---|---|---|
| Tenant settings | enabled, default_business_id, auto_acknowledge, default_hsn_code, default_product_category | The organisation: PUT /api/v1/nigeria/settings, or the generic PUT /api/v1/jurisdictions/NG/settings |
| Platform-owned settings | role, base_url and the business list | Only the checked paths of the Nigeria routes (discovery, onboarding, the base-URL and role checks above), or the platform's operators |
The generic PUT /api/v1/jurisdictions/NG/settings (settings:manage, or authority:manage) accepts only the
tenant fields and stores the body whole, so a field left out takes its default (enabled: false,
default_business_id: null, auto_acknowledge: true, no default_hsn_code or default_product_category). The
two line defaults are stored only when set, and an HSN code not in 0000.00 format or a product category longer
than 255 characters is refused. Any other field (role, base_url, businesses, ...) is refused and nothing is
written:
{
"error": "invalid_settings",
"error_code": "JURISDICTION_SETTINGS_INVALID",
"message": "Settings for NG do not match the pack's schema.",
"issues": [{ "type": "extra_forbidden", "loc": ["businesses"], "msg": "Extra inputs are not permitted" }],
"request_id": "req_2b3c4d5e6f7a48190a1b2c3d4e5f6071"
}
GET /api/v1/jurisdictions/NG/settings returns the tenant part only, for example
{ "code": "NG", "settings": { "enabled": true, "default_business_id": null, "auto_acknowledge": true } }
(with default_hsn_code and default_product_category when they are set).
default_business_id is a preference: it is used only while it names a business in the organisation's
platform-owned list that is not NRS's demo business.
Roleโ
Each organisation has an NRS role: service_provider (the System Integrator and Access Point operator) or
taxpayer (the default). The platform sets it; GET /api/v1/nigeria/settings shows it. It carries two
privileges:
- a
service_providerholding its own AP key has the businesses of its Access Point account added to its business list, so an invoice NRS delivers to one of them is matched to the operator rather than left unassigned; - a
service_provider's own SI and AP account businesses are validated and signed with that account's own key (step 1 of Issuing an invoice). For an organisation with thetaxpayerrole, every invoice is validated and signed with its taxpayer key.
Which NRS calls an organisation can make depends on the keys it holds. A request to change the role from anyone but a super admin is refused:
{
"code": "C5_NG_ROLE_PLATFORM_OWNED",
"error": "C5_NG_ROLE_PLATFORM_OWNED",
"message": "The organisation's NRS role (service provider or taxpayer) is set by the platform, not in the settings; contact support to change it.",
"role": "taxpayer",
"request_id": "req_5c6d7e8f9a0b41c2d3e4f5a6b7c8d9e0"
}
Businessesโ
GET /api/v1/nigeria/businesses (authenticated) returns the organisation's NRS businesses as last discovered.
?refresh=true re-discovers them from NRS first, by the rules in Settings; stored QR keys are kept.
{
"ok": true,
"businesses": [
{
"business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"business_name": "Acme Supplies Ltd",
"tin": "12345678-0001",
"tax_id": null,
"service_id": "A1B2C3D4",
"entity_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"reference": "acme-supplies",
"has_keys": true,
"is_demo": false,
"account": "taxpayer",
"app_reference": "example-provider-ap",
"signs_without_qr": false
}
],
"default_business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"message": null
}
has_keys says whether the business's QR keys are stored. is_demo marks NRS's demo business, which NRS creates in
every System Integrator account: it is listed but can never issue an invoice, sign a QR code or be the default. A
refresh that could not reach NRS answers "ok": false with a message and the unchanged list.
account says which NRS account the business was discovered under, and so which key signs as it:
account | The business is | Validated and signed with |
|---|---|---|
taxpayer | A taxpayer's business (every business listed before this field existed reads as taxpayer) | The organisation's own taxpayer key; its QR keys are required |
si | Listed or onboarded with the organisation's own SI key | That SI key, when the organisation is a service_provider |
ap | One of the NRS entities filed under the organisation's own access point | That AP key, when the organisation is a service_provider |
A service_provider's si and ap businesses are its account businesses. It issues as them with its own key of
that account, never an inherited one. For an organisation whose role is taxpayer, every business follows the
taxpayer row. It cannot issue as one that another organisation under it lists
(409 C5_NG_BUSINESS_HELD_BY_ANOTHER_ORG): that business issues its own invoices. NRS issues cryptographic keys only
to approved businesses, so an account business may have none. signs_without_qr: true marks such a business: its
invoices are signed without a QR code (Issuing an invoice). app_reference is the access point
NRS filed the business's entity under when it was listed, or null when NRS named none.
As a service provider sees one of its AP account businesses that has no NRS keys:
{
"business_id": "3d4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f6a",
"business_name": "Example Provider Ltd",
"tin": "55667788-0001",
"tax_id": null,
"service_id": "E5F6A7B8",
"entity_id": "7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"reference": "example-provider",
"has_keys": false,
"is_demo": false,
"account": "ap",
"app_reference": "example-provider-ap",
"signs_without_qr": true
}
Onboard a businessโ
POST /api/v1/nigeria/businesses (settings:manage) creates a taxpayer business with NRS, using the
organisation's SI key or the one it inherits. If the SI account has no NRS entity yet, the platform creates one
first; otherwise it uses entity_id, or the account's first entity. The new business is added to the business list
of the organisation that makes the call: to onboard a client, call it as the client organisation.
| Field | Required | Notes |
|---|---|---|
name | Yes | The registered business name |
tin | Yes | The business's TIN |
reference | No | Your reference for the business; generated from the name when omitted |
entity_id | No | An existing NRS entity of the SI account |
curl -X POST "$API/api/v1/nigeria/businesses" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "name": "Bright Retail Ltd", "tin": "87654321-0001" }'
{
"ok": true,
"businesses": [
{
"business_id": "9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"business_name": "Bright Retail Ltd",
"tin": "87654321-0001",
"tax_id": null,
"service_id": "B7C8D9E0",
"entity_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"reference": "bright-retail-ltd-4c1e9a",
"has_keys": false,
"is_demo": false,
"account": "taxpayer",
"app_reference": null,
"signs_without_qr": false
}
],
"default_business_id": "9b8a7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"message": "Business onboarded successfully."
}
The Service ID is read from the IRN template NRS returns for the new business.
When a business with that TIN is already registered in the SI account, nothing is created at NRS. If no other
organisation under the same service provider (the service provider included) lists that business, it is added to
the caller's list and the answer is "ok": false with "message": "A business with TIN 87654321-0001 is already registered; it is now in this organisation's list โ no need to onboard it again.". If another organisation lists
it, the request is refused and nothing changes, so one client never takes over another client's business:
{
"code": "C5_NG_BUSINESS_HELD_BY_ANOTHER_ORG",
"error": "C5_NG_BUSINESS_HELD_BY_ANOTHER_ORG",
"message": "That business already belongs to another organisation under the same service provider, so it cannot be added here; ask the service provider.",
"tin": "87654321-0001",
"request_id": "req_4f3e2d1c0b9a48776e5d4c3b2a190807"
}
Any other NRS refusal answers "ok": false with NRS's error in message; an NRS answer without the new business's
id is 502 C5_NG_NRS_BAD_RESPONSE. No SI key anywhere: 400 C5_NG_SI_CREDENTIALS_REQUIRED.
Store a business's QR keysโ
PUT /api/v1/nigeria/businesses/{business_id}/keys (settings:manage) stores the two values of that business's
crypto_keys.txt, exactly as they appear in the file:
{
"nrs_public_key": "<the public_key value from the business's crypto_keys.txt>",
"certificate_b64": "<the certificate value from the business's crypto_keys.txt>"
}
{
"ok": true,
"businesses": [
{
"business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"business_name": "Acme Supplies Ltd",
"tin": "12345678-0001",
"tax_id": null,
"service_id": "A1B2C3D4",
"entity_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
"reference": "acme-supplies",
"has_keys": true,
"is_demo": false,
"account": "taxpayer",
"app_reference": "example-provider-ap",
"signs_without_qr": false
}
],
"default_business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"message": "Cryptographic keys saved for Acme Supplies Ltd."
}
The keys go into the encrypted credential store under that business and are never returned. These are refused
with "ok": false and a message: a missing value; a business that is not in the organisation's list; NRS's demo
business; NRS's published sample certificate (its certificate value begins bEF0V3FJbmo5); a public key that
cannot be read.
NRS verify_stamp names a QR code's issuer from the certificate inside it. A QR built with another business's
bundle, or with NRS's published sample, is reported as issued by that other business, whatever the invoice says.
The platform builds a QR only with the issuing business's own stored keys and refuses the sample certificate and
the demo business everywhere.
Reference listsโ
GET /api/v1/nigeria/resources/{name} (authenticated) returns one of NRS's code lists. name is one of states,
lgas, invoice-types, tax-categories, currencies, payment-means, services-codes,
invoice-quantity-codes, countries. Any other name answers 404 with "error": "unknown_resource" and the
allowed list.
curl "$API/api/v1/nigeria/resources/invoice-types" -H "X-API-Key: $KEY"
{
"name": "invoice-types",
"items": [
{ "code": "381", "value": "Commercial Invoice", "name": "Commercial Invoice" },
{ "code": "380", "value": "Credit Note", "name": "Credit Note" }
],
"cached": false
}
Every item carries code and name beside NRS's own fields; an LGA also carries state_code (for example
{ "code": "NG-LA-IKJ", "name": "Ikeja", "state_code": "NG-LA" }). The platform fetches a list with the
organisation's taxpayer key, else its SI key, else its AP key (400 C5_NG_TAXPAYER_CREDENTIALS_REQUIRED with
none), and keeps it in memory for 24 hours per NRS base URL; cached says whether this answer came from that
cache.
NRS health checkโ
POST /api/v1/nigeria/health-check (settings:manage, no body) calls NRS's self-health-check with the AP key. NRS
then posts a test notification to the webhook URL registered in the NRS portal; the platform answers it 200 and
records nothing.
{
"ok": true,
"nrs": { "code": 200, "data": { "ok": true } },
"key_from": "Example Provider Ltd",
"message": "NRS accepted the health check; it now posts a test notification to the registered webhook URL."
}
nrs is NRS's answer as it came back; key_from names the organisation whose AP key was used.
Look up a buyer by TINโ
GET /api/v1/nigeria/lookup/tin/{tin} (invoices:send) asks NRS, with the organisation's AP key or the one it
inherits, which access point serves the business registered under a TIN
(GET base_url/api/v1/invoice/transmit/lookup/tin/{TIN}). Use it before issuing to see whether a buyer can receive
a transmitted invoice.
curl "$API/api/v1/nigeria/lookup/tin/87654321-0001" -H "X-API-Key: $KEY"
{
"ok": true,
"found": true,
"app_reference": "bright-erp-ap",
"business_reference": "bright-retail",
"business_tin": "***************",
"up": true,
"has_webhook_setup": true
}
app_reference names the buyer's access point and up says whether NRS reports it reachable. The TIN comes back
masked, as NRS gives it. When NRS knows no business under the TIN the answer is 200 with "found": false and the
other fields null. A TIN that is not letters, digits and hyphens (at most 64 characters) is refused with 422
(C5_NG_CUSTOMER_TIN_REQUIRED); another NRS refusal is 502 C5_NG_NRS_ERROR; no AP key anywhere is
400 C5_NG_AP_CREDENTIALS_REQUIRED.
Issuing an invoiceโ
POST /api/v1/nigeria/invoices (invoices:send) issues one invoice. Validation and signing happen synchronously in
this call: if NRS refuses the invoice, the answer says why at once and nothing is issued or recorded.
| Field | Type | Required | Description |
|---|---|---|---|
invoice | object | Yes | The NRS invoice JSON (schema) |
invoice_no | string | Unless invoice.invoice_number is set | The invoice number, the first part of the IRN: letters, digits and single hyphens between them, at most 64 characters |
business_id | string | Unless the organisation has a default business | The business issuing the invoice. Else invoice.business_id; else the organisation's default |
transmit | boolean | No, default true | Transmit the signed invoice to the buyer's access point. Never done for a B2C invoice, nor for a supplier on another access point (step 4 below). With false the invoice is issued and recorded with no legs; Transmit now starts them later |
What the platform does, in order:
- Checks what it needs, refusing before NRS is called: the issuing business (one of the organisation's, never
NRS's demo business:
400 C5_NG_BUSINESS_REQUIRED,C5_NG_BUSINESS_NOT_FOUND,C5_NG_DEMO_BUSINESS) and the key that signs as it, by the business'saccount:- A taxpayer-account business: the organisation's own taxpayer key
(
400 C5_NG_TAXPAYER_CREDENTIALS_REQUIRED) and the business's own QR keys (400 C5_NG_BUSINESS_KEYS_REQUIRED). - One of a service provider's own account businesses: the organisation's own SI key for an
sibusiness, or its own AP key for anapbusiness; an inherited key is refused (400 C5_NG_SI_CREDENTIALS_REQUIREDorC5_NG_AP_CREDENTIALS_REQUIRED). A business that another organisation under the service provider lists is refused (409 C5_NG_BUSINESS_HELD_BY_ANOTHER_ORG). QR keys are not required: without them the invoice is signed without a QR code (step 7).
- A taxpayer-account business: the organisation's own taxpayer key
(
- Completes the invoice: sets
business_id; fills the supplier'stinandparty_namefrom the business when they are empty; setsinvoice_kindtoB2Bwhen it is missing (any value other thanB2B,B2CorB2Gis refused with422 C5_NG_INVOICE_KIND_INVALID); setsinvoice_number; gives apayment_meansentry withoutpayment_due_datethe invoice'sdue_date; and builds the IRN. - Checks the invoice, refusing before NRS is called: a credit or debit note without
billing_reference(400 C5_NG_BILLING_REFERENCE_REQUIRED); no customer TIN (400 C5_NG_CUSTOMER_TIN_REQUIRED); apayment_meansentry still withoutpayment_due_date, which NRS validates but then refuses to sign (400 C5_NG_PAYMENT_DUE_DATE_REQUIRED); an invalid invoice number (400 C5_NG_INVOICE_NUMBER_INVALID); an IRN this organisation has issued through the platform already (409 C5_NG_DUPLICATE_IRN). - Finds where the supplier is, for an invoice to be transmitted (not B2C, not
transmit: false, and transmission switched on). Only the supplier's own access point can transmit an invoice. The supplier is on the service provider's access point when the business is one of itsapaccount businesses. Otherwise the platform asks NRS which access point serves the supplier's TIN (GET base_url/api/v1/invoice/transmit/lookup/tin/{TIN}, AP key, a single try with an 8-second time-out), or falls back to the business'sapp_reference. This step never refuses the invoice: when the answer cannot be had, the invoice is issued as usual and the transmit leg checks again before it transmits. - Validates the invoice with NRS, with the key from step 1.
- Signs it with NRS, with the same key. From here on the invoice exists at NRS under its IRN. If NRS's answer to the signing request is lost, the platform asks NRS whether it holds the invoice before deciding (NRS unreachable).
- Builds the QR code from the IRN and the business's own keys, and checks it with NRS
verify_stamp(at most 10 seconds). The issuer NRS names is recorded; if it is not the business,issuer_matchesisfalseandmessagesays so. An account business with no NRS keys gets no QR code:"qr": "not_available","qr_reason_code": "C5_NG_NO_BUSINESS_KEYS",qr_png_base64andqr_verifiedarenull, andmessagereads "NRS issues cryptographic keys only to approved businesses; this invoice is signed without a QR." - Records the invoice: one sent transaction with status
issued, the IRN, the QR, the invoice as sent to NRS and a UBL XML rendering for preview. It then creates the legs, each only where the platform has that step switched on:nrs_transmitandnrs_statusfor an invoice to transmit;nrs_statusalone for a B2C invoice, or for a supplier that step 4 found on another access point; none for an invoice issued withtransmit: false. - Answers with the IRN, the QR code, the transaction id and the legs.
A supplier on another access point. When step 4 finds the supplier on another access point, the invoice is still
issued, and NRS holds it, but no nrs_transmit leg is created: that access point transmits it. The answer says so
with "transmit_owed": false, "transmit_reason_code": "C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT",
supplier_app_reference naming that access point, and a transmit_reason (also added to message) of the form
Only the supplier's access point can transmit this invoice; {business name} is on {supplier_app_reference}. The
invoice's status is signed with that reason (List issued invoices) unless NRS's own
status, read by the nrs_status leg or with ?refresh=true, reports it transmitted.
If the QR code cannot be built after NRS has signed, the invoice is still recorded, without a QR
("qr": "not_built", qr_png_base64: null).
The IRNโ
The IRN is {invoice number}-{Service ID}-{YYYYMMDD}:
- the invoice number is
invoice_no(orinvoice.invoice_number); - the Service ID is the issuing business's, from NRS;
- the date is the invoice's
issue_date, or today's UTC date whenissue_dateis missing or not an ISO date.
For INV1001 issued by Acme Supplies Ltd on 12 October 2026 the IRN is INV1001-A1B2C3D4-20261012. Leave
invoice.irn out and the platform builds it. An irn you send is kept only when all three parts match: the
invoice number, the business's Service ID and the issue_date; otherwise it is rebuilt from the invoice. An irn
that is not the business's and has no invoice number to rebuild it from is refused (400 C5_NG_IRN_MISMATCH).
Invoice type codesโ
invoice_type_code takes NRS's own codes, from GET /api/v1/nigeria/resources/invoice-types:
| Code | NRS meaning | billing_reference |
|---|---|---|
381 | Commercial Invoice | Not used |
380 | Credit Note | Required |
384 | Debit Note | Required |
396 | Invoice Request | Not used |
These are not the UNCL1001 meanings used by Peppol, where 380 is the commercial invoice. A Nigerian commercial
invoice is 381; 380 makes it a credit note.
A credit note or debit note names the invoice it corrects in billing_reference, an array of
{ "irn", "issue_date" }. The platform refuses a 380 or 384 whose billing_reference is missing, empty, or has
an entry without both values, before NRS is called.
Invoice kindsโ
invoice_kind is B2B (business to business), B2C (business to consumer) or B2G (business to government), in
any letter case. A missing value is sent to NRS as B2B; any other value is refused before NRS is called:
{
"code": "C5_NG_INVOICE_KIND_INVALID",
"error": "C5_NG_INVOICE_KIND_INVALID",
"message": "invoice_kind must be B2B, B2C or B2G.",
"invoice_kind": "G2B",
"request_id": "req_1a2b3c4d5e6f47089a0b1c2d3e4f5061"
}
A B2C invoice is signed and stamped like any other but is not transmitted; NRS still requires the customer's TIN on it, and the platform checks for it.
States and LGAsโ
Addresses carry NRS's codes for the state and the local government area (LGA):
postal_address.state and postal_address.lga. Take them from GET /api/v1/nigeria/resources/states and
GET /api/v1/nigeria/resources/lgas; each LGA there carries its state_code. The codes in the example below are
examples.
Exampleโ
A commercial invoice from Acme Supplies Ltd to Bright Retail Ltd, saved as invoice-request.json:
{
"invoice_no": "INV1001",
"business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"transmit": true,
"invoice": {
"issue_date": "2026-10-12",
"due_date": "2026-11-11",
"invoice_type_code": "381",
"invoice_kind": "B2B",
"document_currency_code": "NGN",
"tax_currency_code": "NGN",
"accounting_supplier_party": {
"party_name": "Acme Supplies Ltd",
"tin": "12345678-0001",
"email": "billing@acme-supplies.example",
"telephone": "+2348010000001",
"business_description": "Office equipment and software",
"postal_address": {
"street_name": "1 Example Street",
"city_name": "Ikeja",
"postal_zone": "100001",
"lga": "NG-LA-IKJ",
"state": "NG-LA",
"country": "NG"
}
},
"accounting_customer_party": {
"party_name": "Bright Retail Ltd",
"tin": "87654321-0001",
"email": "accounts@bright-retail.example",
"telephone": "+2348010000002",
"business_description": "Retail stores",
"postal_address": {
"street_name": "2 Example Road",
"city_name": "Ikeja",
"postal_zone": "100001",
"lga": "NG-LA-IKJ",
"state": "NG-LA",
"country": "NG"
}
},
"tax_total": [
{
"tax_amount": 75000,
"tax_subtotal": [
{
"taxable_amount": 1000000,
"tax_amount": 75000,
"tax_category": { "id": "STANDARD_VAT", "percent": 7.5 }
}
]
}
],
"legal_monetary_total": {
"line_extension_amount": 1000000,
"tax_exclusive_amount": 1000000,
"tax_inclusive_amount": 1075000,
"payable_amount": 1075000
},
"invoice_line": [
{
"hsn_code": "8471.30",
"product_category": "Laptop computers",
"invoiced_quantity": 2,
"line_extension_amount": 900000,
"item": {
"name": "Laptop, 14-inch",
"description": "14-inch business laptop",
"sellers_item_identification": "ACM-LT14"
},
"price": { "price_amount": 450000, "base_quantity": 1, "price_unit": "EA" }
},
{
"hsn_code": "8523.49",
"product_category": "Software",
"invoiced_quantity": 1,
"line_extension_amount": 100000,
"item": {
"name": "Office software licence, 1 year",
"sellers_item_identification": "ACM-SW01"
},
"price": { "price_amount": 100000, "base_quantity": 1, "price_unit": "EA" }
}
]
}
}
curl -X POST "$API/api/v1/nigeria/invoices" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d @invoice-request.json
NRS receives the invoice with business_id, irn (INV1001-A1B2C3D4-20261012) and invoice_number added. The
answer, 200:
{
"ok": true,
"validation": { "code": 200, "data": { "ok": true } },
"sign": { "code": 201, "data": { "ok": true } },
"message": null,
"issues": [],
"signed": true,
"duplicate": false,
"transaction_id": "2c9e4b1a-7d3f-4e8a-9b6c-5f1a2d3e4c7b",
"irn": "INV1001-A1B2C3D4-20261012",
"qr_png_base64": "iVBORw0KGgoAAAANSUhEUgAAAUoAAAFKAQAAAABTUiuoAAACโฆ",
"qr_signature": "Xb7Rk2Pq9Lm4Tz8Wv1Nc5Hd3Jf6Ga0Ysโฆ",
"qr_verified": true,
"issuer": "Acme Supplies Ltd",
"issuer_matches": true,
"legs": [
{
"leg_type": "nrs_transmit",
"status": "pending",
"reason_code": null,
"reason": null,
"attempts": 0,
"updated_at": "2026-10-12T09:14:03.512345+00:00",
"next_attempt_at": null,
"external_ref": null
},
{
"leg_type": "nrs_status",
"status": "pending",
"reason_code": null,
"reason": null,
"attempts": 0,
"updated_at": "2026-10-12T09:14:03.512345+00:00",
"next_attempt_at": "2026-10-12T09:16:03.512345+00:00",
"external_ref": null
}
],
"reason_code": null,
"nrs_error": null,
"qr": "stamped",
"qr_reason_code": null,
"signed_with": "taxpayer",
"transmit_owed": true,
"transmit_reason_code": null,
"transmit_reason": null,
"supplier_app_reference": "example-provider-ap"
}
| Field | Meaning |
|---|---|
ok, signed | NRS validated and signed the invoice, and it is recorded |
irn | The invoice's IRN |
transaction_id | The platform's id for the invoice; use it with the other /api/v1/nigeria/invoices/{transaction_id} routes |
signed_with | The key that validated and signed: taxpayer, si or ap |
qr | stamped: the invoice carries a QR code. not_available: an account business with no NRS keys, signed without a QR (qr_reason_code C5_NG_NO_BUSINESS_KEYS). not_built: the QR could not be built after NRS signed |
qr_png_base64 | The QR code as a PNG image, base64-encoded, to print on the invoice; null without a QR |
qr_signature | The text the QR encodes (The QR code); null without a QR. The API description calls it the text to paste into NRS's Validate QR Code page |
qr_verified | true: NRS read the QR back; false: NRS refused it; null: NRS's verification service did not answer, or answered 5xx or 429, so nothing is known (message then ends "NRS verification service unavailable.") |
issuer, issuer_matches | The issuer NRS names for the QR, and whether it is the issuing business |
legs | What happens next: nrs_transmit is due at once; nrs_status first reads NRS two minutes later. A B2C invoice, or one whose supplier is on another access point, has only nrs_status, first read after 30 seconds; an invoice issued with transmit: false has none |
transmit_owed | Whether a transmission is owed from here: false for B2C, transmit: false, or a supplier on another access point |
transmit_reason_code, transmit_reason | C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT and a sentence naming the supplier's access point, when it was asked for and only that access point can transmit; null otherwise |
supplier_app_reference | The supplier's access point, when step 4 could find it; null otherwise |
validation, sign | NRS's own answers to the two calls |
message | A note when something needs attention (for example issuer_matches: false, an invoice signed without a QR, or a supplier on another access point); null otherwise |
Refusalsโ
Refused by NRS. NRS's validation or signing refused the invoice. The answer is 200 with "ok": false;
nothing is issued or recorded. Fix the fields named in issues[] and send the invoice again:
{
"ok": false,
"validation": {
"code": 400,
"data": null,
"message": "error has occurred",
"error": {
"id": "5d0c1f6e-2b3a-4c8d-9e7f-1a2b3c4d5e6f",
"handler": "invoice_actions",
"public_message": "validation failed: we are unable to process your request",
"details": "invoicerequest.invoice.invoiceline[0].hsn_code is required when product_category is provided"
}
},
"sign": null,
"message": "Line 1: HSN code is required when product category is provided. Enter an HSN code in 0000.00 format (e.g. 1006.30).",
"issues": [
"Line 1: HSN code is required when product category is provided. Enter an HSN code in 0000.00 format (e.g. 1006.30)."
],
"signed": false,
"duplicate": false,
"transaction_id": null,
"irn": "INV1001-A1B2C3D4-20261012",
"qr_png_base64": null,
"qr_signature": null,
"qr_verified": null,
"issuer": null,
"issuer_matches": null,
"legs": [],
"reason_code": "C5_NG_VALIDATION_REJECTED",
"nrs_error": {
"status": 400,
"handler": "invoice_actions",
"id": "5d0c1f6e-2b3a-4c8d-9e7f-1a2b3c4d5e6f",
"details": "invoicerequest.invoice.invoiceline[0].hsn_code is required when product_category is provided",
"public_message": "validation failed: we are unable to process your request"
},
"qr": null,
"qr_reason_code": null,
"signed_with": null,
"transmit_owed": null,
"transmit_reason_code": null,
"transmit_reason": null,
"supplier_app_reference": null
}
NRS's public_message is the same for most refusals; the useful text is details, which names the field.
issues[] turns each into a sentence: invoiceline[0] becomes "Line 1", field names become words, and NRS's
generic phrases are left out. reason_code is C5_NG_VALIDATION_REJECTED for a validation refusal and
C5_NG_SIGN_REJECTED for a signing refusal.
Refused by the platform. The invoice breaks a rule the platform checks before calling NRS:
{
"code": "C5_NG_CUSTOMER_TIN_REQUIRED",
"error": "C5_NG_CUSTOMER_TIN_REQUIRED",
"message": "NRS requires the customer's TIN on every invoice, consumer (B2C) invoices included.",
"issues": ["accounting_customer_party.tin is required, also for a B2C invoice."],
"request_id": "req_9a8b7c6d5e4f40312a1b2c3d4e5f6a7b"
}
Duplicate IRN. An IRN is issued once. If this organisation has already issued the same IRN through the
platform, the platform answers 409 without calling NRS and names the existing invoice:
{
"code": "C5_NG_DUPLICATE_IRN",
"error": "C5_NG_DUPLICATE_IRN",
"message": "An invoice with this IRN has already been issued through this service; use a new invoice number.",
"irn": "INV1001-A1B2C3D4-20261012",
"transaction_id": "2c9e4b1a-7d3f-4e8a-9b6c-5f1a2d3e4c7b",
"request_id": "req_0f1e2d3c4b5a49687f6e5d4c3b2a1908"
}
NRS refuses a signing request for an IRN it has signed already in one of two ways:
- an explicit answer that the invoice is a duplicate, already exists or is already signed;
- its generic
400refusal, "unable to complete this operation at this time, kindly try again later". NRS gives the same answer for other reasons too, so it proves nothing on its own.
After either, the platform asks NRS whether it holds that invoice (GET base_url/api/v1/invoice/confirm/{IRN},
with the key that issued it: step 1). The outcome:
- NRS holds it, and this organisation has no record of it (an earlier attempt whose answer was lost): the
invoice is recorded as issued, exactly as in the example, with
"ok": true,"duplicate": false,"message": "NRS already held this invoice (signed by an earlier attempt); it is recorded now."and NRS's confirm record insign({ "recovered": "duplicate", "confirm": { โฆ } }). - NRS holds it, and this organisation has already recorded it: a duplicate. The answer is
200with"ok": false,"duplicate": trueandreason_codeC5_NG_DUPLICATE_IRN; nothing is recorded. - NRS does not hold it, or cannot say:
- after NRS's explicit duplicate answer: a duplicate, as above (example below);
- after NRS's generic refusal: a signing refusal. The answer is
200with"ok": false,"duplicate": false,reason_codeC5_NG_SIGN_REJECTEDand"message": "NRS could not sign this invoice."; nothing is recorded.issues[]leaves NRS's generic text out, andnrs_errorcarries NRS's answer.
A duplicate after NRS's explicit answer:
{
"ok": false,
"validation": { "code": 200, "data": { "ok": true } },
"sign": {
"code": 400,
"data": null,
"message": "error has occurred",
"error": {
"id": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
"handler": "invoice_actions",
"public_message": "error has occurred",
"details": "duplicate irn: invoice already signed"
}
},
"message": "Invoice INV1001 was already signed with NRS under IRN INV1001-A1B2C3D4-20261012. Use a new invoice number to issue again.",
"issues": ["Duplicate IRN: invoice already signed."],
"signed": false,
"duplicate": true,
"transaction_id": null,
"irn": "INV1001-A1B2C3D4-20261012",
"qr_png_base64": null,
"qr_signature": null,
"qr_verified": null,
"issuer": null,
"issuer_matches": null,
"legs": [],
"reason_code": "C5_NG_DUPLICATE_IRN",
"nrs_error": {
"status": 400,
"handler": "invoice_actions",
"id": "7e6d5c4b-3a29-4180-9f8e-7d6c5b4a3928",
"details": "duplicate irn: invoice already signed",
"public_message": "error has occurred"
},
"qr": null,
"qr_reason_code": null,
"signed_with": null,
"transmit_owed": null,
"transmit_reason_code": null,
"transmit_reason": null,
"supplier_app_reference": null
}
The details text is NRS's and varies; the platform reads a signing refusal whose text says the invoice is a
duplicate, already exists or is already signed as NRS's explicit duplicate answer. When the duplicate is refused,
issue with a new invoice number.
NRS unreachable. When NRS cannot be reached, or answers 5xx or 429, after the
retries, the outcome depends on the step:
- Before signing (validation):
502 C5_NG_NRS_UNREACHABLE. Nothing was issued or recorded. - While signing. A signing request is never sent twice, but it may have reached NRS before its answer was
lost. The platform asks NRS (
GET base_url/api/v1/invoice/confirm/{IRN}, with the key that issued it) whether it holds the invoice:- NRS holds it: the invoice is recorded as issued and the answer is
200with"ok": true,"message": "NRS stopped answering while signing, but confirms it holds the invoice; it is recorded."and NRS's confirm record insign({ "recovered": "confirm", "confirm": { โฆ } }). - NRS does not hold it:
502 C5_NG_NRS_UNREACHABLEwith"message": "NRS stopped answering while signing and does not hold the invoice: nothing was issued; issue it again."and theirn. Nothing was issued or recorded. - NRS cannot say:
502 C5_NG_SIGN_OUTCOME_UNKNOWNwith theirn; nothing was recorded. Send the same invoice again once NRS answers: if NRS signed it the first time, it reports a duplicate, the platform finds the invoice with confirm and records it ("ok": true, as above); otherwise NRS signs it now.
- NRS holds it: the invoice is recorded as issued and the answer is
{
"code": "C5_NG_SIGN_OUTCOME_UNKNOWN",
"error": "C5_NG_SIGN_OUTCOME_UNKNOWN",
"message": "NRS stopped answering while signing, so it is not known whether the invoice was signed. Check its status before issuing it again; nothing was recorded.",
"irn": "INV1001-A1B2C3D4-20261012",
"request_id": "req_7d6c5b4a39284f17e6d5c4b3a2918070"
}
Credit and debit notesโ
A credit note is a 380 naming the invoice it corrects. Only the fields that differ from the example are shown:
{
"invoice_no": "CN1001",
"invoice": {
"issue_date": "2026-10-20",
"invoice_type_code": "380",
"invoice_kind": "B2B",
"billing_reference": [
{ "irn": "INV1001-A1B2C3D4-20261012", "issue_date": "2026-10-12" }
]
}
}
Its IRN is CN1001-A1B2C3D4-20261020. A debit note is the same with 384.
B2C invoicesโ
Set "invoice_kind": "B2C" and keep accounting_customer_party.tin. The answer has only the nrs_status leg: a B2C
invoice is signed, stamped and recorded, and NRS holds it, but it is not transmitted to an access point. The status
leg ends at NRS's first answer.
Validate without issuingโ
POST /api/v1/nigeria/validate (invoices:send) checks an invoice with NRS without issuing it.
| Field | Type | Description |
|---|---|---|
invoice | object | The NRS invoice JSON |
sign | boolean | Default false. true issues the invoice exactly as POST /api/v1/nigeria/invoices with transmit: false (recorded, no legs) |
invoice_no | string | As for issuing |
business_id | string | As for issuing |
With sign: false the platform needs the business and the key that signs as it (step 1 of
Issuing an invoice, without the QR keys), completes the invoice and
makes the same checks as for issuing (except the duplicate check), and calls NRS validate only. Nothing is recorded.
curl -X POST "$API/api/v1/nigeria/validate" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "invoice_no": "INV1002", "business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7", "invoice": { "...": "the NRS invoice JSON" } }'
{
"ok": true,
"validation": { "code": 200, "data": { "ok": true } },
"sign": null,
"message": null,
"issues": [],
"signed": false,
"duplicate": false,
"transaction_id": null,
"irn": "INV1002-A1B2C3D4-20261012",
"qr_png_base64": null,
"qr_signature": null,
"qr_verified": null,
"issuer": null,
"issuer_matches": null,
"legs": [],
"reason_code": null,
"nrs_error": null,
"qr": null,
"qr_reason_code": null,
"signed_with": null,
"transmit_owed": null,
"transmit_reason_code": null,
"transmit_reason": null,
"supplier_app_reference": null
}
A refusal by NRS has the same shape as for issuing ("ok": false, issues[], reason_code
C5_NG_VALIDATION_REJECTED, nrs_error).
The QR codeโ
An invoice carries a QR code whenever its business has NRS keys: always for a taxpayer-account business, which cannot
issue without them. A service provider's account business that NRS issued no keys to is signed without one
("qr": "not_available"). The platform builds the QR code as NRS specifies:
payload = { "irn": "{IRN}.{unix timestamp}", "certificate": "{certificate from crypto_keys.txt, as it is}" }ciphertext = RSA PKCS#1 v1.5 encryption of the payload with the public_key from crypto_keys.txt(NRS's key)- QR content =
base64(ciphertext), rendered as a PNG.
For an issued invoice, this QR content is qr_signature, in the answer to issuing and in
One issued invoice; qr_png_base64 is the same content as a PNG image. It is the same text
the invoice PDF prints as the QR (The invoice PDF and UBL preview).
The certificate is used exactly as it appears in crypto_keys.txt; decoding it first makes NRS report an unknown
issuer. The platform then checks the QR with NRS: POST https://api.firsmbs.com/api/v1/integration/verify_stamp
with { "code": "{QR content}" }. That endpoint is public and takes no key. NRS answers with the IRN it read and
the issuer that owns the certificate, or 422 "Failed to validate QR Code".
verify_stamp provesNRS decrypts the QR and names the issuer bound to the certificate inside it. It does not check that the IRN was
signed, nor that the issuer is the supplier named on the invoice. The platform records the issuer and compares it
with the issuing business (issuer_matches). Anyone holding the QR text can run the same public check.
POST /api/v1/nigeria/qr (invoices:send) builds and checks a QR code for an IRN of one of the organisation's
businesses. The QR printed on an issued invoice is the one returned at issuing (and kept with the invoice); this
route builds a new one, with its own timestamp, from the business's stored keys. It does not call NRS other than
verify_stamp.
| Field | Required | Description |
|---|---|---|
invoice_no | Yes | The IRN's first part |
business_id | No | The business; default the organisation's default business |
issue_date | No | The IRN's date, YYYY-MM-DD; default today (UTC) |
service_id | No | When sent, it must be the business's own Service ID (400 C5_NG_IRN_MISMATCH) |
curl -X POST "$API/api/v1/nigeria/qr" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "invoice_no": "INV1001", "business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7", "issue_date": "2026-10-12" }'
{
"ok": true,
"irn": "INV1001-A1B2C3D4-20261012",
"signature_b64": "kQ3x7Lm0Zr9Vt2Ys8Pw1Nc6Jd4Hf5Gb0Aqโฆ",
"png_base64": "iVBORw0KGgoAAAANSUhEUgAAAUoAAAFKAQAAAABTUiuoAAACโฆ",
"issued_at_unix": 1791796443,
"needs_keys": false,
"verified": true,
"verify_message": "NRS validated this QR (issuer 'Acme Supplies Ltd').",
"issuer": "Acme Supplies Ltd",
"message": null
}
signature_b64 is the QR content; png_base64 is the same content as a PNG image. verified is true when NRS
read the QR back, false when NRS refused it (check the business's keys), and null when NRS's verification
service did not answer or answered 5xx or 429 ("verify_message": "NRS verification service unavailable"). A business without stored QR keys is refused with 400 C5_NG_BUSINESS_KEYS_REQUIRED and
"needs_keys": true; NRS's demo business with 400 C5_NG_DEMO_BUSINESS.
Sending from the generic pathsโ
An organisation in Nigeria (its organisation record's country is NG) can also send through the platform's
generic paths, the ones every other country uses. On a service with the Nigeria pack switched on, those paths
issue its invoices through NRS exactly as Issuing an invoice describes (validate, sign, IRN,
QR, record, transmit and track), never over Peppol: no Peppol validation, no Peppol ID, no default sender. Every other
organisation's invoices go as they always have.
| Path | What happens for an organisation in Nigeria |
|---|---|
POST /api/v1/invoices | Issued through NRS; answers 202 with an nrs block (below) |
POST /api/v1/invoices/validate | Mapped, checked and validated with NRS. Never signed, nothing recorded (below) |
POST /api/v1/invoices/import/csv | Each row is issued through NRS. A row's result is in the import job's transactions (status issued) or errors (validation_layer AUTHORITY, with the reason code and its sentences). The file takes the Nigeria columns |
POST /api/v1/invoices/import/csv/preview | Each row is mapped and checked as issuing would check it (NRS is not called); a problem is an error of layer AUTHORITY |
GET /api/v1/invoices/import/csv/template | The Nigeria template (below) |
POST /api/v1/drafts/{draft_id}/send | The draft is issued through NRS; a refused draft is marked error with the reason |
POST /api/v1/batches/csv | The file is read as the CSV import reads it: the Nigeria columns, the seller from the company profile, no Peppol ID needed. The batch is then validated and processed as below |
POST /api/v1/batches/{batch_id}/validate and POST /api/v1/batches/{batch_id}/process | Each item is validated with NRS (never signed), then issued through NRS |
POST /api/v1/invoices/credit-note/send | The credit note is issued through NRS as a 380 citing the invoice it credits |
Connectors: Odoo, Zoho Books, Amazon, bol.com, and SAP's POST /api/v1/connectors/{connector_id}/documents/{doc_id}/send | Each document is issued through NRS, each line's HSN code taken as Connectors describes. SAP's dry_run validates with NRS and never signs. None of the checks that only a Peppol send needs is made (Connectors) |
POST /api/v1/documents, which takes a finished UBL document, is not one of these paths: the platform does not issue
a document sent there through NRS.
The invoice is the platform's usual invoice JSON (the same body as for any country). The platform maps it to NRS's invoice JSON, then issues it with the organisation's NRS settings and keys.
The Nigeria fieldsโ
Four optional fields carry what NRS needs and the generic invoice has no field for. They are read only for an organisation in Nigeria and are left out of every other invoice.
| Field | Where | Description |
|---|---|---|
ng_hsn_code | each line | The line's HSN code in 0000.00 format, for example 1006.30. Required by NRS on every line: a line without one takes the organisation's default_hsn_code (Invoice-line defaults) |
ng_product_category | each line | The line's product category, free text (at most 255 characters). Default: the organisation's default_product_category, else the line's description |
ng_business_id | invoice | The NRS business issuing the invoice (Businesses). Default: the organisation's default business |
ng_invoice_kind | invoice | B2B (default), B2C or B2G (Invoice kinds) |
Invoice-line defaultsโ
NRS requires an HSN code on every line, and many lines reach the platform without one: a connected system that
holds no classification, a spreadsheet without the column. The organisation can set two defaults in its
settings (PUT /api/v1/nigeria/settings, or NRS Invoicing, Settings in the portal):
| Setting | Format | Used for |
|---|---|---|
default_hsn_code | 0000.00, for example 1006.30 | A line with no ng_hsn_code of its own |
default_product_category | Free text, at most 255 characters | A line with no ng_product_category of its own |
A line's HSN code is, in order: its own ng_hsn_code (which a connector fills from the source system's own
classification, below); else the organisation's default_hsn_code; else the line is refused
(422 C5_NG_GENERIC_MAPPING, the issue naming the line and where to set a default). Its product category is its
own ng_product_category, else the organisation's default_product_category, else the line's description. Use
the default for an organisation that sells one kind of goods or service; one with several sets the code per line or
per product in its own system.
Connectorsโ
The connectors send a Nigerian organisation's invoices through NRS like every other generic path. Where the
connected system classifies its goods, that classification is each line's ng_hsn_code:
| Connector | The line's HSN code comes from |
|---|---|
| Odoo | The product's HS Code (hs_code), where the database's apps define the field. Read only for an organisation in Nigeria |
| Zoho Books | The line item's HSN/SAC (hsn_or_sac), where the organisation's Zoho Books edition has it |
| SAP | The billing item's CommodityCode, where the connected service carries it (the standard billing document service does not) |
| Amazon, bol.com | Nothing: neither marketplace holds one. Each line takes the organisation's default |
The code is read as digits only: six or more make 0000.00 from the first six (10063010, 1006.30.10 and
HS 1006 30 are all 1006.30); anything shorter (a four-digit SAC code, for example) is not used, and the line
takes the organisation's default.
None of the checks that only a Peppol send needs is made for a Nigerian organisation's documents:
- the seller's Peppol registration (SMP) that holds a connector's sending (Odoo, Zoho Books, Amazon, bol.com);
- the Peppol participant a seller must hold: Amazon and bol.com take the seller from the company profile, and SAP
needs neither
seller_peppol_idnor a buyer's Peppol identifier; - whether the buyer is on the Peppol network (Amazon, bol.com);
- Oman's rules: a 0% line needs no Oman reason code (Odoo, Zoho Books), a Zoho Books customer abroad is not an export, and a Zoho Books seller carries no Omani identifier.
NRS requires the buyer's email. It comes from the customer in Odoo, the customer's contact person in Zoho Books, the order's billing details on bol.com, and Amazon's buyer read (which needs the Tax Invoicing role); a document without one is held with that reason.
QuickBooks Online and Xero are not connected to NRS. Their connectors send to POST /api/v1/documents, which does
not issue through NRS.
CSV columnsโ
For an organisation in Nigeria the CSV and Excel import also reads these columns (any of the names shown). For every
other organisation they are not read, and invoice_kind keeps its Oman meaning.
| Column | Also | Fills |
|---|---|---|
hsn_code | line_hsn_code, line_ng_hsn_code, ng_hsn_code | the line's ng_hsn_code (0000.00; blank: the organisation's default_hsn_code) |
product_category | line_product_category, line_ng_product_category, ng_product_category | the line's ng_product_category |
invoice_kind | ng_invoice_kind | ng_invoice_kind: B2B, B2C or B2G (anything else is a row error) |
business_id | ng_business_id, nrs_business_id | ng_business_id |
billing_irn | ng_billing_irn, original_irn | a credit or debit note's preceding_invoice.number: the IRN of the invoice it amends |
billing_issue_date | ng_billing_issue_date, original_issue_date | its preceding_invoice.issue_date (YYYY-MM-DD) |
buyer_email | customer_email | buyer.email (required) |
buyer_phone | buyer_telephone, customer_phone | buyer.phone |
seller_email | seller.email (blank: the company profile's, then the organisation's) | |
seller_phone | seller_telephone | seller.phone |
buyer_peppol_id is not required (the template keeps the column, empty); the buyer's TIN goes in buyer_vat
(buyer_tin). An address without a country is in Nigeria. A numeric document_type takes NRS's meanings: 381
invoice, 380 credit note, 384 debit note. The template (GET /api/v1/invoices/import/csv/template, CSV or
?filetype=xlsx) carries the core columns, buyer_vat, and document_type, buyer_email, hsn_code,
product_category, invoice_kind, business_id, billing_irn, billing_issue_date.
How the invoice is mappedโ
| NRS field | From |
|---|---|
invoice_number | number. It becomes the IRN's first part, so it may hold only letters, digits and single hyphens between them |
invoice_type_code | document_type: invoice is 381, credit_note is 380, debit_note is 384. Never invoice_type_code (NRS's codes are not the UNTDID meanings). Any other document type is refused |
billing_reference | For a 380 or 384: preceding_invoice (and billing_references), each cited by its IRN. The IRN is the reference's number or uuid when one of them is an IRN, or else the IRN of the invoice with that number this organisation issued through the platform. The issue date is the reference's issue_date, or else the IRN's own date |
issue_date, due_date | The same fields |
document_currency_code, tax_currency_code | currency, and tax_currency (default currency) |
accounting_supplier_party | seller: name, the TIN (below), email, phone, address. An email, telephone or address field the invoice leaves out is taken from the organisation's company profile, then from its own record. The TIN may be left out: the platform fills it from the NRS business |
accounting_customer_party | buyer: name, the TIN (below), email, phone, address |
| Party TIN | The party's vat_number; else its identifier when identifier_scheme is TIN |
postal_address | street (and street2), city, postal_code, country; state only when it is an NRS state code such as NG-LA |
invoice_line[] | Per line: hsn_code from ng_hsn_code (else the organisation's default_hsn_code), product_category (ng_product_category, else default_product_category, else the description), invoiced_quantity from quantity, line_extension_amount (the line's net amount), item.name and item.description from description, price (unit_price per one unit, base_quantity 1, price_unit from unit) |
tax_total | One subtotal per NRS category and rate, from the lines: standard with a rate above 0 is STANDARD_VAT; zero and export are ZERO_VAT (0); exempt and outside_scope are EXEMPTED (0). Any other category, or a standard line without its rate, is refused |
legal_monetary_total | With totals_authority system (the default), computed from the lines; with client, the invoice's own totals |
payment_means | Only for an invoice with a due_date: one entry with the code for payment.means (credit_transfer 30, the default; cash 10; check 20; card 48; direct_debit 49; other ZZZ) and payment_due_date set to the due date. NRS signs payment means only with a due date |
note | notes, joined |
What NRS requires, and the platform checks before NRS is called: an HSN code on every line (its own or the
organisation's default); both parties' email and
postal address street, city and postal_code; the buyer's TIN (also on a B2C invoice). Document-level
allowances and charges are not sent to NRS: put them on the lines they apply to. A buyer abroad is accepted.
curl -X POST "$API/api/v1/invoices" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" -H "Idempotency-Key: inv-1003" \
-d '{
"invoice": {
"document_type": "invoice",
"number": "INV1003",
"issue_date": "2026-10-12",
"due_date": "2026-11-11",
"currency": "NGN",
"seller": { "name": "Acme Supplies Ltd", "email": "billing@acme.example",
"address": { "street": "12 Marina Road", "city": "Lagos", "postal_code": "101001",
"state": "NG-LA", "country": "NG" } },
"buyer": { "name": "Bright Retail Ltd", "vat_number": "23456789-0001", "email": "ap@bright.example",
"address": { "street": "4 Allen Avenue", "city": "Ikeja", "postal_code": "100001", "country": "NG" } },
"lines": [ { "description": "Rice, 50 kg bags", "quantity": 10, "unit_price": 50000,
"tax_category": "standard", "tax_rate": 7.5,
"ng_hsn_code": "1006.30", "ng_product_category": "Food" } ],
"totals": { "net_amount": 500000, "payable_amount": 537500 }
}
}'
The answerโ
202, the generic answer with status issued, document_number the invoice number, document_uuid the IRN,
delivery.channel authority, validation null, and an nrs block. The nrs block appears only on an
invoice issued through NRS; every other answer is unchanged and has no nrs key.
{
"transaction_id": "0c9b8a7d-6e5f-4a3b-9c2d-1e0f9a8b7c6d",
"status": "issued",
"message": "Invoice issued through NRS: validated and signed under IRN INV1003-A1B2C3D4-20261012.",
"created_at": "2026-10-12T09:14:03.512Z",
"idempotency_key": "inv-1003",
"delivery": {
"channel": "authority",
"format": "nrs",
"profile": null,
"hybrid_pdf": false,
"hybrid_profile": null,
"buyer_on_network": null,
"reason": "Issued through NRS, Nigeria's e-invoicing clearance: NRS validated and signed it; it is not sent over Peppol."
},
"document_number": "INV1003",
"document_uuid": "INV1003-A1B2C3D4-20261012",
"validation": null,
"nrs": {
"irn": "INV1003-A1B2C3D4-20261012",
"qr": "stamped",
"qr_png_base64": "iVBORw0KGgoAAAANSUhEUgAAAUoAAAFKAQAAAABTUiuoAAACโฆ",
"qr_signature": "kQ3x7Lm0Zr9Vt2Ys8Pw1Nc6Jd4Hf5Gb0Aqโฆ",
"qr_verified": true,
"issuer": "Acme Supplies Ltd",
"issuer_matches": true,
"signed_with": "taxpayer",
"transmit_owed": true,
"transmit_reason_code": null,
"transmit_reason": null,
"legs": [
{ "leg_type": "nrs_transmit", "status": "pending", "reason_code": null, "reason": null, "attempts": 0,
"updated_at": "2026-10-12T09:14:03.498Z", "next_attempt_at": null, "external_ref": null },
{ "leg_type": "nrs_status", "status": "pending", "reason_code": null, "reason": null, "attempts": 0,
"updated_at": "2026-10-12T09:14:03.498Z", "next_attempt_at": "2026-10-12T09:16:03.498Z", "external_ref": null }
]
}
}
The nrs fields mean what they mean on POST /api/v1/nigeria/invoices: qr is stamped,
not_available or not_built; signed_with is taxpayer, si or ap; transmit_reason_code is
C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT when only the supplier's own access point can transmit the invoice. The
invoice is then followed as any NRS invoice is (After issuing).
Retries. Send an Idempotency-Key. A retry under the same key answers the first issue (the message
Invoice already accepted (idempotent replay), the same transaction_id, IRN and nrs block) and does not call NRS
again. The same key with another invoice is refused with 422 IDEMPOTENCY_KEY_REUSED. The same invoice number sent
again under a new key is refused with 409 C5_NG_DUPLICATE_IRN: give it a new number.
Refusals from the generic pathsโ
| HTTP | error_code | When |
|---|---|---|
422 | C5_NG_GENERIC_MAPPING | The invoice lacks what NRS requires (above). NRS is not called. issues[] has one sentence per problem |
422 | C5_NG_VALIDATION_REJECTED, C5_NG_SIGN_REJECTED, C5_NG_DUPLICATE_IRN | NRS refused the invoice while validating or signing. issues[] holds NRS's field-level messages made readable, nrs_error NRS's error, irn the IRN. Nothing was issued |
400, 409, 502, 503 | the pack's reason code | Every refusal of Issuing an invoice, with its own status: no business or key (C5_NG_BUSINESS_REQUIRED, C5_NG_TAXPAYER_CREDENTIALS_REQUIRED, ...), NRS settings never saved (400 C5_NG_NOT_ENROLLED), the IRN issued here already (409 C5_NG_DUPLICATE_IRN), NRS unreachable (502 C5_NG_NRS_UNREACHABLE) |
Each refusal carries error_code, and the same code as code and error:
{
"code": "C5_NG_GENERIC_MAPPING",
"error": "C5_NG_GENERIC_MAPPING",
"error_code": "C5_NG_GENERIC_MAPPING",
"message": "The invoice lacks what NRS requires, so it was not sent to NRS; the issues list says what to add or change. Line 1: an HSN code is required by NRS: give ng_hsn_code in 0000.00 format (e.g. 1006.30). (and 1 more: see issues)",
"issues": [
"Line 1: an HSN code is required by NRS: give ng_hsn_code in 0000.00 format (e.g. 1006.30).",
"The buyer's email is required by NRS: give buyer.email."
],
"request_id": "req_7a6b5c4d3e2f40a1b2c3d4e5f6a7b8c9"
}
Validateโ
POST /api/v1/invoices/validate answers in its usual shape. For an organisation in Nigeria it maps the invoice,
makes the checks issuing makes (including the IRN issued here already) and calls NRS validate. It never signs and
records nothing. A problem is an error whose code is the reason code (C5_NG_GENERIC_MAPPING with the invoice
field in field, C5_NG_VALIDATION_REJECTED for NRS's own messages, or the pack's code for a missing business or
key). score is 100 when the invoice is valid and 0 when it is not. NRS unreachable is answered
502 C5_NG_NRS_UNREACHABLE.
{
"valid": false,
"score": 0,
"totals_authority": "system",
"errors": [
{
"field": "lines[0].ng_hsn_code",
"code": "C5_NG_GENERIC_MAPPING",
"severity": "error",
"message": "Line 1: an HSN code is required by NRS: give ng_hsn_code in 0000.00 format (e.g. 1006.30).",
"technical_code": "C5_NG_GENERIC_MAPPING",
"suggested_fix": null,
"auto_fix_available": false
}
],
"warnings": [],
"calculated_totals": null
}
After issuingโ
Transmissionโ
The nrs_transmit leg asks NRS to deliver the signed invoice to the buyer's access point:
POST base_url/api/v1/invoice/transmit/{IRN} with the Access Point key of the organisation or, when it has none,
of its operator. It runs as soon as the invoice is issued.
Only the supplier's own access point can transmit an invoice. Before it transmits, the leg finds where the supplier
is, as issuing does (step 4). If the supplier is on another access point, nothing is sent: the
leg ends skipped with C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT, the invoice records transmit_owed: false and a
reason naming that access point, and the nrs_status leg watches the signed invoice as it does a B2C one.
| NRS answer | Leg outcome |
|---|---|
200, or NRS says the IRN is transmitted already | submitted, with the IRN as external_ref. It becomes acknowledged when NRS reports the invoice transmitted: read by the nrs_status leg, or by the platform on a TRANSMITTED or ACKNOWLEDGED notification |
404 with handler: transmit_actions (access denied: the supplier is on another access point) | skipped, C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT, at once and never retried |
403 | failed, C5_NG_TRANSMIT_NOT_AUTHORISED |
NRS unreachable, 5xx or 429 | Tried again with back-off; never given up |
| Any other refusal | Tried again; failed with C5_NG_TRANSMIT_FAILED on the third refusal |
| No AP key on the organisation or its parents | held with C5_NG_AP_CREDENTIALS_REQUIRED; released within about five minutes of a key being stored |
If NRS sends a FAILED notification and NRS's own status does not show the invoice transmitted, the leg goes back to
pending (reason C5_NG_TRANSMIT_FAILED) and the invoice is transmitted again.
The leg is not created for a B2C invoice, for a supplier that issuing already found on another access point, or
where transmission is switched off. An invoice issued with transmit: false (or through
POST /api/v1/nigeria/validate with sign: true) has no legs at all until Transmit now creates
them.
Transmit nowโ
POST /api/v1/nigeria/invoices/{transaction_id}/transmit (invoices:send, no body) makes the transmission happen
now:
- an invoice issued with
transmit: false: thenrs_transmitleg is created, and thenrs_statusleg that watches the delivery with it ("action": "created"); - a leg that is waiting (
pending, orheldfor a key): it is made due now ("action": "queued").
leg is the transmit leg; legs lists the legs created (or the one queued) in full.
curl -X POST "$API/api/v1/nigeria/invoices/5b4a3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d/transmit" -H "X-API-Key: $KEY"
{
"ok": true,
"transaction_id": "5b4a3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
"action": "created",
"leg": { "leg_type": "nrs_transmit", "status": "pending" },
"legs": [
{
"leg_type": "nrs_transmit",
"status": "pending",
"reason_code": null,
"reason": null,
"attempts": 0,
"updated_at": "2026-10-13T08:00:12.402118+00:00",
"next_attempt_at": null,
"external_ref": null
},
{
"leg_type": "nrs_status",
"status": "pending",
"reason_code": null,
"reason": null,
"attempts": 0,
"updated_at": "2026-10-13T08:00:12.402118+00:00",
"next_attempt_at": "2026-10-13T08:02:12.402118+00:00",
"external_ref": null
}
]
}
Refused with 409: a B2C invoice, or transmission switched off on the platform (C5_NG_TRANSMIT_NOT_OWED); an
invoice already transmitted (C5_NG_DUPLICATE_IRN, "This invoice has been transmitted already.", with
leg_status); a transmission that has ended failed (the leg's reason code, with leg_status): it is not tried
again, so issue a new invoice if one is still owed.
A supplier on another access point. Before a transmission still to be made, the route finds where the supplier
is (step 4). Only the supplier's own access point can transmit, so when the supplier is on
another access point, or the invoice already records that it is, the answer is 409:
{
"code": "C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT",
"error": "C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT",
"message": "Only the supplier's access point can transmit this invoice; Bright Retail Ltd is on bright-erp-ap.",
"supplier_app_reference": "bright-erp-ap",
"leg_status": "skipped",
"request_id": "req_8a9b0c1d2e3f40516273849a0b1c2d3e"
}
- For an invoice issued with
transmit: false, nothing is created and the invoice stays as it was issued (noleg_status). - A transmit leg still waiting (
pending) is ended: it becomesskippedwith that reason, the invoice recordstransmit_owed: false, and the answer carries"leg_status": "skipped". - An invoice that already records the supplier on another access point is answered at once, with the recorded
message and
supplier_app_reference.
The invoice is not lost: NRS holds it signed, and the supplier's own access point can transmit it.
Statusโ
List issued invoicesโ
GET /api/v1/nigeria/invoices (authenticated) lists the organisation's issued invoices, newest first.
page (default 1) and page_size (default 20, at most 100; size is accepted too).
curl "$API/api/v1/nigeria/invoices?page=1&page_size=20" -H "X-API-Key: $KEY"
{
"items": [
{
"transaction_id": "2c9e4b1a-7d3f-4e8a-9b6c-5f1a2d3e4c7b",
"irn": "INV1001-A1B2C3D4-20261012",
"direction": "sent",
"invoice_no": "INV1001",
"invoice_number": "INV1001",
"invoice_type_code": "381",
"invoice_kind": "B2B",
"business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"business_name": "Acme Supplies Ltd",
"currency": "NGN",
"total_amount": 1075000,
"payable_amount": 1075000,
"issue_date": "2026-10-12",
"created_at": "2026-10-12T09:14:03.512345+00:00",
"qr_verified": true,
"issuer": "Acme Supplies Ltd",
"legs": [
{
"leg_type": "nrs_status",
"status": "pending",
"reason_code": "C5_NG_AWAITING_DELIVERY",
"reason": "NRS has not yet reported the invoice delivered to, and acknowledged by, the buyer's access point; the platform checks again.",
"attempts": 2,
"updated_at": "2026-10-12T09:17:05.002114+00:00",
"next_attempt_at": "2026-10-12T09:19:05.002114+00:00",
"external_ref": null
},
{
"leg_type": "nrs_transmit",
"status": "submitted",
"reason_code": null,
"reason": null,
"attempts": 1,
"updated_at": "2026-10-12T09:14:04.871903+00:00",
"next_attempt_at": null,
"external_ref": "INV1001-A1B2C3D4-20261012"
}
],
"status": "transmitting",
"state": "transmitting",
"reason_code": null,
"reason": null,
"qr": "stamped",
"qr_reason_code": null,
"qr_reason": null,
"buyer_name": "Bright Retail Ltd",
"buyer_tin": "87654321-0001",
"customer_name": "Bright Retail Ltd",
"supplier_name": "Acme Supplies Ltd",
"issued_at": "2026-10-12T09:14:03.512345+00:00",
"transmit_owed": true,
"transmit_reason_code": null,
"transmit_reason": null,
"supplier_app_reference": "example-provider-ap",
"signed_with": "taxpayer",
"payment_status": "PENDING"
}
],
"total": 1,
"page": 1,
"page_size": 20,
"size": 20,
"has_more": false
}
status (repeated as state) is one of:
status | Meaning |
|---|---|
signed | Issued. Nothing has been transmitted yet, or nothing is owed (B2C, transmit: false), or the supplier is on another access point: then reason_code and transmit_reason_code are C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT, and reason and transmit_reason name that access point |
transmitting | The platform has asked NRS to transmit the invoice |
transmitted | NRS reports the invoice transmitted or delivered |
acknowledged | NRS's status shows the buyer's acknowledgement |
failed | A leg failed; reason_code and reason say why |
qr, qr_reason_code and qr_reason describe the invoice's QR code as in the issuing answer, with
qr_reason the sentence for qr_reason_code. signed_with and supplier_app_reference are as in that answer.
One issued invoiceโ
GET /api/v1/nigeria/invoices/{transaction_id} (authenticated) returns the list item plus the QR code, NRS's
status (confirm), the payment status, the NRS notifications received for the invoice (events), the invoice as
sent to NRS, and a link to its UBL rendering. With ?refresh=true the platform first reads
GET base_url/api/v1/invoice/confirm/{IRN} with the organisation's taxpayer key, or with the AP key when NRS refuses
the taxpayer key (400 C5_NG_TAXPAYER_CREDENTIALS_REQUIRED with neither). An invoice of another organisation is
404.
curl "$API/api/v1/nigeria/invoices/2c9e4b1a-7d3f-4e8a-9b6c-5f1a2d3e4c7b?refresh=true" -H "X-API-Key: $KEY"
The answer has every field of the list item above, and these:
{
"qr_png_base64": "iVBORw0KGgoAAAANSUhEUgAAAUoAAAFKAQAAAABTUiuoAAACโฆ",
"qr_signature": "Xb7Rk2Pq9Lm4Tz8Wv1Nc5Hd3Jf6Ga0Ysโฆ",
"issuer_matches": true,
"signed_at": "2026-10-12T09:14:03.512345+00:00",
"service_id": "A1B2C3D4",
"confirm": {
"delivered": true,
"transmitted": true,
"payment_status": "PENDING",
"sync_date": "2026-10-12",
"acknowledged": false,
"entry_status": "NEW_ENTRY",
"checked_at": "2026-10-12T10:02:41.330872+00:00"
},
"payment": null,
"events": [
{ "message": "TRANSMITTING", "source": "webhook", "at": "2026-10-12T09:14:06.118204+00:00" },
{ "message": "TRANSMITTED", "source": "webhook", "at": "2026-10-12T09:15:12.640031+00:00" }
],
"invoice": {
"business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"irn": "INV1001-A1B2C3D4-20261012",
"invoice_number": "INV1001",
"issue_date": "2026-10-12",
"invoice_type_code": "381",
"invoice_kind": "B2B",
"document_currency_code": "NGN"
},
"preview_url": "/api/v1/transactions/2c9e4b1a-7d3f-4e8a-9b6c-5f1a2d3e4c7b/document/content",
"ubl_preview_url": "/api/v1/transactions/2c9e4b1a-7d3f-4e8a-9b6c-5f1a2d3e4c7b/document/content"
}
qr_signature is the text the QR encodes, as at issuing (The QR code); it is null unless the
invoice was stamped with a QR ("qr": "stamped"). invoice is shown abridged here; the answer holds the whole
invoice as sent to NRS. confirm is NRS's answer at
checked_at: transmitted, delivered, payment_status, sync_date and entry_status are NRS's;
acknowledged is true when NRS's answer carries an acknowledgement, or when an ACKNOWLEDGED notification arrived
while NRS reports the invoice delivered (a notification alone is never taken as true). preview_url returns the UBL
rendering through GET /api/v1/transactions/{transaction_id}/document/content (transactions:read).
The status legโ
The nrs_status leg reads GET base_url/api/v1/invoice/confirm/{IRN} (taxpayer key, else AP key) and records the
answer on the invoice. Its first read is two minutes after the leg is created (30 seconds for a B2C invoice);
then after 1, 2, 5, 15 and 30 minutes, 1, 2 and 4 hours, and every 6 hours after that. While it waits, the leg
shows C5_NG_AWAITING_DELIVERY, which is not a failure and never uses up its tries. An NRS notification for the
invoice makes it read at once.
- B2C: the leg ends
acknowledgedat NRS's first answer, since NRS holds the signed invoice and nobody receives it. - Transmitted: the leg moves the
nrs_transmitleg toacknowledgedonce NRS reports the invoice transmitted, and endsacknowledgedonce NRS reports it delivered and acknowledged. - 30 days after the leg is created: it ends
acknowledgedwithC5_NG_NOT_ACKNOWLEDGED_BY_BUYERif NRS reports the invoice delivered, otherwisefailedwithC5_NG_STATUS_TIMEOUT. - NRS unreachable: tried again, never given up. NRS refusing the read three times:
failed,C5_NG_STATUS_FAILED. No key:helduntil one is stored.
Payment statusโ
PATCH /api/v1/nigeria/invoices/{transaction_id}/payment (invoices:send) reports the invoice's payment status to
NRS with PATCH base_url/api/v1/invoice/update/{IRN}.
| Field | Required | Description |
|---|---|---|
payment_status | Yes | PAID, PARTIAL, PENDING or REJECTED (another value: 422) |
amount | For PARTIAL | The amount paid (400 C5_NG_VALIDATION_REJECTED when missing) |
reference | No | Your payment reference, at most 255 characters |
curl -X PATCH "$API/api/v1/nigeria/invoices/2c9e4b1a-7d3f-4e8a-9b6c-5f1a2d3e4c7b/payment" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "payment_status": "PARTIAL", "amount": 500000, "reference": "TRF-20261020-0042" }'
{
"ok": true,
"irn": "INV1001-A1B2C3D4-20261012",
"payment": {
"payment_status": "PARTIAL",
"amount": 500000.0,
"reference": "TRF-20261020-0042",
"at": "2026-10-20T08:30:11.902176+00:00",
"key": "taxpayer"
},
"nrs": { "code": 200, "data": { "ok": true } }
}
NRS receives { "payment_status", "amount", "reference" } (the last two only when given). The platform uses the
taxpayer key; if NRS refuses that key's role, or answers it 403 or 404, it tries the AP key. key says which
was used, and nrs is NRS's answer. The update is kept on the invoice (payment, and the last 20 updates). If NRS
refuses, the answer is 200 with "ok": false, issues[], message and nrs_error.
Download and decryptโ
POST /api/v1/nigeria/decrypt (invoices:send) downloads a signed invoice from NRS
(GET base_url/api/v1/invoice/download/{IRN}) and decrypts it. It tries the organisation's taxpayer key first, then
its own AP key, which downloads an invoice addressed to a buyer on its access point. An AP key the organisation
inherits is never used here: it would download what NRS delivered to the operator's other clients. With neither key
the answer is 400 C5_NG_TAXPAYER_CREDENTIALS_REQUIRED.
curl -X POST "$API/api/v1/nigeria/decrypt" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{ "irn": "INV1001-A1B2C3D4-20261012" }'
{
"ok": true,
"irn": "INV1001-A1B2C3D4-20261012",
"iv_hex": "9f2c4e6a8b0d1f3e5a7c9b1d3f5e7a9c",
"pub": "Q7mZ2xK9pL4vT8nR6wY1sB5d",
"plaintext": "{\"business_id\":\"6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7\",\"irn\":\"INV1001-A1B2C3D4-20261012\",โฆ}",
"invoice": {
"business_id": "6f1d2c3b-4a5e-4f60-8a71-92b3c4d5e6f7",
"irn": "INV1001-A1B2C3D4-20261012",
"issue_date": "2026-10-12",
"invoice_type_code": "381"
},
"message": null,
"not_found": false
}
NRS answers the download with { iv_hex, pub, data }. The platform decrypts data with AES-256-CFB: the key is
pub followed by the first hyphen-separated part of the x-api-key that downloaded it (32 bytes together), the IV
is iv_hex. invoice is the decrypted text parsed as JSON (abridged here). "not_found": true means NRS has no
signed invoice for that IRN that either key may download: the invoice was not signed with this organisation's
taxpayer key, nor addressed to a buyer on its access point. Nothing is stored by this call.
The invoice PDF and UBL previewโ
GET /api/v1/transactions/{transaction_id}/pdf (transactions:export) prints an NRS invoice, sent or received, as
it prints any transaction: from the UBL preview stored with it. For an NRS invoice the PDF also has a banner, first
on the page:
-
A title from the invoice type code:
381COMMERCIAL INVOICE,380CREDIT NOTE,384DEBIT NOTE,396INVOICE REQUEST. The same title heads the page. -
The QR code, 40 mm square, when the invoice has one. For a sent invoice it is the QR stamped at issuing (
qr_signature). For a received invoice it is only the supplier's own QR, when the fetched content carries one; the platform never makes a QR for an invoice it received. -
Up to four lines beside the QR. For a sent invoice:
IRN INV1001-A1B2C3D4-20261012;Signed by NRS (Nigeria Revenue Service) on 2026-10-12(the date NRS signed it);- what NRS said of the QR's issuer when the platform checked it:
QR issuer verified by NRS: Acme Supplies Ltd(QR verified by NRSwhen NRS named no issuer),QR not verified by NRS, orQR verification unavailablewhen NRS's verification service did not answer; Verify: einvoice.nrs.gov.ng/docs/testing/validate-qr-code.
An invoice without a QR has one line in place of lines 3 and 4:
QR not available: NRS issues cryptographic keys only to approved businessesfor an account business that NRS issued no keys to, otherwiseQR not available.For a received invoice: the
IRNline;Received through NRS; acknowledged 2026-10-12orReceived through NRS; not yet acknowledged; and, when the content carries the supplier's QR, the same two QR lines.
The QR is printed once, in the banner. Lower on the page, the reference line reads IRN: followed by the IRN, where
a Peppol document reads Peppol Reference:. Where the footer says how a received invoice arrived, it reads
"Received through the tax authority's e-invoicing platform", not "Received over the Peppol network".
The UBL preview (preview_url, through GET /api/v1/transactions/{transaction_id}/document/content) is a rendering
for display. What NRS signed is the NRS invoice JSON. For an NRS invoice the preview carries:
- Each party's TIN as its identifier, in
cac:PartyIdentification/cbc:IDandcac:PartyLegalEntity/cbc:CompanyID, withschemeID="NG-TIN".NG-TINis the platform's own label, not a Peppol identifier scheme. The TIN is never written as a VAT number. A party's separate Tax ID, when the invoice has one, is a second identifier withschemeID="NG-TAXID". A TIN that NRS masked (*****) is left out. - Each line's VAT rate, in
cac:Item/cac:ClassifiedTaxCategory/cbc:Percent: the line's own rate when NRS's invoice gives one, otherwise the invoice's rate when the invoice has a single rate. With neither, the line has no rate. - The QR, as
cac:AdditionalDocumentReferencewithcbc:IDQRand the QR content incbc:EmbeddedDocumentBinaryObject(mimeCode="text/plain"). It is there only for a sent invoice stamped with a QR, or a received invoice whose content carries one. - For a credit or debit note, the invoice it corrects, as
cac:BillingReference.
Receiving invoicesโ
An invoice addressed to a business the platform serves reaches the platform in two ways: NRS's notification to the operator's webhook, and the operator's queue at NRS, which the platform pulls. Both lead to the same steps and create at most one received invoice per IRN on the platform.
The webhookโ
Register the webhook_url from GET /api/v1/nigeria/settings in the NRS portal (Developers, Add Webhook URL).
It is one of two addresses for the same receiver:
| Address | When |
|---|---|
https://{portal host}/nrs/webhook | The operator has a white-label portal domain. The portal passes the call to the API with the host it arrived on; only POST is accepted |
The API's own receiver under its base URL, $API/api/v1/public/nrs/webhook?operator={organisation slug} (POST /api/v1/public/nrs/webhook) | Otherwise |
The receiver finds the operator from the request: the organisation whose white-label domain is in
X-Forwarded-Host, else in Host, else the organisation named by ?operator=. The operator is the organisation
that holds the Access Point key, that one or its nearest parent. A request whose operator cannot be found is logged
and dropped.
NRS posts a small JSON body:
POST /nrs/webhook HTTP/1.1
Host: einvoicing.example.com
Content-Type: application/json
{ "irn": "DC5521-D4E5F6A7-20261014", "message": "TRANSMITTED" }
The same call on the API address:
curl -X POST "$API/api/v1/public/nrs/webhook?operator=example-provider" \
-H "Content-Type: application/json" \
-d '{ "irn": "DC5521-D4E5F6A7-20261014", "message": "TRANSMITTED" }'
The answer is always 200:
{ "ok": true }
| Field | Values |
|---|---|
irn (or IRN) | An IRN of the form {invoice number}-{Service ID}-{YYYYMMDD} |
message (or status, event) | TRANSMITTING, TRANSMITTED, ACKNOWLEDGED or FAILED, in any letter case |
Anything else (NRS's test notification, a body that is not JSON, a body over 64 KiB, another message) is
answered 200 and ignored. The receiver takes no credentials and does not trust the body: the body only names an
IRN, and the platform reads that IRN back from NRS with the operator's own Access Point key before it changes
anything. Repeated notifications are recorded once per IRN and message. Only the names of request headers are
logged, never their values.
What the platform does with a notification:
- An invoice issued by an organisation under the operator: the notification is added to that invoice's
events, and itsnrs_statusleg reads NRS at once. OnTRANSMITTEDorACKNOWLEDGED, the platform also readsGET base_url/api/v1/invoice/confirm/{IRN}itself and, if NRS reports the invoice transmitted, moves asubmittedtransmit leg toacknowledged. Only NRS's answer moves the invoice's status. - An invoice addressed to the operator's access point: the platform reads
GET base_url/api/v1/invoice/transmit/lookup/{IRN}with the AP key, and creates a received invoice only when NRS names the operator's own access point as the buyer's. It fails closed: if NRS does not know the IRN, names another access point for the buyer, or the operator's own access-point name cannot be read from NRS, nothing is created from the notification. An invoice that really is addressed to the operator then arrives through the pull, which NRS answers only with invoices waiting for that access point. Otherwise one received invoice is created with annrs_receiveleg; later notifications for that IRN are added to itsevents.
When the supplier and the buyer are both served under the same operator, both apply to one IRN. If the platform
cannot process a notification, it still answers 200; the next pull picks the invoice up.
Pullโ
Every five minutes the platform asks NRS for waiting invoices, GET base_url/api/v1/invoice/transmit/pull, for each
operator that holds its own Access Point key, and receives each one as it would a notification. NRS answers the
pull only with invoices waiting for that access point, so a pulled invoice needs no access-point check (each run
is bounded: at most 10 operators, 25 invoices and 15 seconds). The same run releases legs held for a missing key once
the key is stored.
A pulled item carries the IRN and two times, not the invoice: pushed_at and confirmed_at. The platform keeps them
on the received invoice as nrs_pushed_at and nrs_confirmed_at. A confirmed_at means NRS's own record shows the
invoice acknowledged, so the invoice is marked acknowledged ("acknowledged_by": "nrs") and is not acknowledged
again. The content is fetched by the receive leg (Fetching the content).
POST /api/v1/nigeria/received/pull (invoices:send, no body) pulls now, with the organisation's AP key or the one
it inherits, up to 50 invoices. It receives everything waiting at the operator's access point; each invoice lands on
the organisation it matches, which need not be the caller.
curl -X POST "$API/api/v1/nigeria/received/pull" -H "X-API-Key: $KEY"
{ "ok": true, "pulled": 1, "received": 1, "seen": 1, "skipped": 0 }
pulled and received both count the invoices newly received; seen counts the items with an IRN that were
processed, including ones received before; skipped counts items NRS listed without an IRN. Refused with 409 C5_NG_NOT_ENROLLED when receiving is switched off on the platform, and
400 C5_NG_AP_CREDENTIALS_REQUIRED without an AP key.
Which organisation receives an invoiceโ
The platform matches the buyer against the operator's organisations (the operator and every active organisation under it that is enrolled in NRS e-invoicing), by the first rule that matches:
- the buyer's NRS
business_id; - the buyer's business reference;
- the buyer's TIN. NRS's lookup masks TINs, so this rule only works when the pulled invoice carries the TIN.
When several organisations match, the deepest in the hierarchy wins. When none does, the invoice lands on the
operator's own organisation with "unassigned": true (C5_NG_RECEIVED_UNASSIGNED) and is never acknowledged
automatically. matched_by on the received invoice says which rule matched.
Fetching the contentโ
The nrs_receive leg completes a received invoice, recording each step so that a retry resumes where it stopped:
-
Parties:
GET base_url/api/v1/invoice/transmit/lookup/{IRN}with the AP key. Both parties' access points and references are kept asparties(TINs masked, as NRS gives them). -
Content, from the first source that has it:
- the invoice inside the pulled item, if NRS included one (decrypted with the AP key when it is encrypted);
GET base_url/api/v1/invoice/download/{IRN}with the operator's AP key, decrypted with that same key. NRS answers it for an invoice addressed to a buyer on the operator's access point, which is every invoice received here;- the same download with the receiving organisation's own taxpayer key;
- else none yet.
content_sourceon the received invoice says where the content came from (pullordownload). -
QR check: when the content carries a QR code, NRS
verify_stampnames its issuer, which is compared with the supplier's name. -
UBL preview: a UBL XML rendering is stored with the invoice, with the supplier's own QR when the content carries one (The invoice PDF and UBL preview).
-
Acknowledgement, when it applies (Acknowledging).
Content pending. When the content cannot be fetched yet (neither the AP key nor the receiving organisation's
taxpayer key could download it, and the pulled item did not carry the invoice), the invoice is kept with what NRS's
lookup gave
("content": "pending", reason C5_NG_CONTENT_PENDING) and is not acknowledged: nobody has the invoice yet. The
leg stays pending and tries again on its own after 1, 2, 5, 10 and 15 minutes, then every 30 minutes, for
7 days. When the organisation saves its taxpayer key with PUT /api/v1/nigeria/settings, its waiting invoices are
fetched at once. After 7 days without content the leg ends acknowledged with C5_NG_CONTENT_PENDING. The GET
routes never fetch or acknowledge anything: GET /api/v1/nigeria/received/{transaction_id} only reads.
No AP key: the leg is held until one is stored. NRS unreachable: tried again, never given up. NRS refusing three
times: failed, C5_NG_RECEIVE_FAILED.
Acknowledgingโ
Acknowledging tells NRS the buyer has received the invoice: PATCH base_url/api/v1/invoice/transmit/{IRN} with the
operator's Access Point key.
- Automatically, by the
nrs_receiveleg, once the content is stored, when the receiving organisation'sauto_acknowledgeis on (the default), the invoice is not unassigned, and it has not been acknowledged (or refused by NRS) already. - On request, with
POST /api/v1/nigeria/received/{transaction_id}/acknowledge(invoices:send, no body).
curl -X POST "$API/api/v1/nigeria/received/8e7d6c5b-4a39-4b2c-9d1e-0f2a3b4c5d6e/acknowledge" -H "X-API-Key: $KEY"
{
"ok": true,
"transaction_id": "8e7d6c5b-4a39-4b2c-9d1e-0f2a3b4c5d6e",
"acknowledged": true,
"acknowledged_at": "2026-10-14T11:20:44.015327+00:00"
}
An invoice acknowledged already answers "ok": true with "message": "Already acknowledged.". If NRS refuses, the
answer is 200 with "ok": false, "acknowledged": false, issues[] and message.
acknowledged_by on the received invoice says how it was acknowledged:
acknowledged_by | Meaning |
|---|---|
auto | By the nrs_receive leg, under auto_acknowledge |
manual | With POST /api/v1/nigeria/received/{transaction_id}/acknowledge |
nrs | NRS's own record already showed it acknowledged: the pulled item carried a confirmed_at (Pull) |
The inboxโ
GET /api/v1/nigeria/received (authenticated) lists the organisation's received invoices, newest first, with the
same paging as the issued list. An operator's list includes the unassigned invoices that landed on it.
curl "$API/api/v1/nigeria/received?page=1&page_size=20" -H "X-API-Key: $KEY"
{
"items": [
{
"transaction_id": "8e7d6c5b-4a39-4b2c-9d1e-0f2a3b4c5d6e",
"irn": "DC5521-D4E5F6A7-20261014",
"direction": "received",
"invoice_no": "DC5521",
"invoice_number": "DC5521",
"invoice_type_code": "381",
"invoice_kind": "B2B",
"business_id": null,
"business_name": null,
"currency": "NGN",
"total_amount": 537500,
"payable_amount": 537500,
"issue_date": "2026-10-14",
"created_at": "2026-10-14T11:02:17.204518+00:00",
"qr_verified": true,
"issuer": "Delta Components Ltd",
"legs": [
{
"leg_type": "nrs_receive",
"status": "acknowledged",
"reason_code": null,
"reason": null,
"attempts": 1,
"updated_at": "2026-10-14T11:02:19.880412+00:00",
"next_attempt_at": null,
"external_ref": "DC5521-D4E5F6A7-20261014"
}
],
"status": "acknowledged",
"state": "acknowledged",
"reason_code": null,
"reason": null,
"supplier_name": "Delta Components Ltd",
"supplier_reference": "delta-components",
"supplier_tin": "11223344-0001",
"buyer_name": "Acme Supplies Ltd",
"buyer_reference": "acme-supplies",
"received_at": "2026-10-14T11:02:17.204518+00:00",
"content": "stored",
"acknowledged": true,
"acknowledged_at": "2026-10-14T11:02:19.880412+00:00",
"acknowledged_by": "auto",
"nrs_pushed_at": null,
"nrs_confirmed_at": null,
"unassigned": false,
"matched_by": "business_id"
}
],
"total": 1,
"page": 1,
"page_size": 20,
"size": 20,
"has_more": false
}
A received invoice's status is received, acknowledged or failed. The names come from the invoice's content
only: until the content is fetched, supplier_name and buyer_name are null, reason_code is
C5_NG_CONTENT_PENDING, and NRS's business references for the two parties are in supplier_reference and
buyer_reference. acknowledged_by is described in Acknowledging; nrs_pushed_at and
nrs_confirmed_at are the times a pulled item carried (Pull), null until the invoice has been seen in a
pull.
GET /api/v1/nigeria/received/{transaction_id} (authenticated) adds the parties NRS named, the notifications, the
invoice (once its content is stored) and the UBL preview link. It only reads: it never calls NRS, fetches content or
acknowledges.
{
"parties": {
"supplier": {
"app_reference": "delta-erp-ap",
"business_reference": "delta-components",
"business_erp_system": "Others",
"business_tin": "***************",
"has_webhook_setup": true,
"up": true
},
"customer": {
"app_reference": "example-provider-ap",
"business_reference": "acme-supplies",
"business_erp_system": "Others",
"business_tin": "***************",
"has_webhook_setup": true,
"up": true
}
},
"content_source": "download",
"events": [
{ "message": "TRANSMITTING", "source": "webhook", "at": "2026-10-14T11:02:17.204518+00:00" },
{ "message": "TRANSMITTED", "source": "webhook", "at": "2026-10-14T11:03:02.551870+00:00" }
],
"invoice": {
"irn": "DC5521-D4E5F6A7-20261014",
"invoice_number": "DC5521",
"issue_date": "2026-10-14",
"invoice_type_code": "381",
"invoice_kind": "B2B",
"document_currency_code": "NGN",
"accounting_supplier_party": { "party_name": "Delta Components Ltd", "tin": "11223344-0001" },
"accounting_customer_party": { "party_name": "Acme Supplies Ltd", "tin": "12345678-0001" },
"legal_monetary_total": { "payable_amount": 537500 }
},
"preview_url": "/api/v1/transactions/8e7d6c5b-4a39-4b2c-9d1e-0f2a3b4c5d6e/document/content",
"ubl_preview_url": "/api/v1/transactions/8e7d6c5b-4a39-4b2c-9d1e-0f2a3b4c5d6e/document/content"
}
(Shown with the list item's fields left out and invoice abridged.) content_source is pull or download
(Fetching the content), null while the content is pending. While the content is pending,
invoice is null, content is "pending" and reason reads "The invoice was received from NRS, but its content
could not be fetched yet with the access point's key or the buyer organisation's taxpayer key; it is fetched again
automatically."
Retries, time-outs and loggingโ
Each call to NRS.
- A request times out after 30 seconds, or 10 seconds to connect. The
verify_stampcheck of a QR code has 10 seconds, 5 to connect, and is not retried: when it does not answer, or answers5xxor429,qr_verifiedisnull(unknown), neverfalse. - A call is tried up to three times, waiting 0.5 and then 1.5 seconds, when NRS cannot be reached, the request times
out, or NRS answers
5xxor429. An NRS answer in the4xxrange is never retried. - Calls that create something at NRS (sign, transmit, entity and business creation) are repeated only when the
connection was never made. A signing request that reached NRS is never sent twice; if its answer is lost, the
platform asks NRS with
confirminstead (NRS unreachable). - Redirects are not followed, so the NRS key headers go only to the screened base URL.
- Each call carries one
X-Correlation-IDheader, the same on all its tries. Every log line of the call carries that id with the method, the endpoint, the HTTP status, the time taken and the attempt number, and for an NRS error also NRS'shandler, erroridanddetails. Request and response headers and bodies are not logged.
Legs. A leg whose NRS call fails because NRS is unreachable, times out, or answers 5xx or 429 is tried again
after 30 seconds, then after twice as long each time up to one hour, and is never given up. A leg whose call NRS
refuses is tried again on the same schedule and ends failed on the third refusal (a 403 to a transmission fails
at once). A leg waiting for a key is held and released by the five-minute run once the key is stored. A received
invoice waiting for its content is retried on its own schedule (Content pending). Every leg
shows attempts, next_attempt_at, reason_code and reason.
Platform switchesโ
Transmission (nrs_transmit), status tracking (nrs_status) and receiving (nrs_receive, with the pull) are each
switched on for a platform by its administrators. Issuing (validate, sign, QR, record) does not depend on them.
| Switched off | Effect |
|---|---|
| Transmission | Issued invoices get no nrs_transmit leg; POST /api/v1/nigeria/invoices/{transaction_id}/transmit answers 409 C5_NG_TRANSMIT_NOT_OWED for an invoice without one ("Transmission through NRS is switched off on this service.") |
| Status tracking | Issued invoices get no nrs_status leg; GET /api/v1/nigeria/invoices/{transaction_id}?refresh=true still reads NRS |
| Receiving | Notifications are still recorded on issued invoices, but no received invoice is created and nothing is pulled; POST /api/v1/nigeria/received/pull answers 409 C5_NG_NOT_ENROLLED |
Mapping to NRS MBS endpointsโ
base_url is the organisation's NRS base URL (Environments).
| NRS MBS endpoint | NRS key | Called by |
|---|---|---|
POST base_url/api/v1/invoice/validate | Taxpayer; for a service provider's own account business, its own SI or AP key | Issuing; POST /api/v1/nigeria/validate |
POST base_url/api/v1/invoice/sign | Taxpayer; for a service provider's own account business, its own SI or AP key | Issuing; POST /api/v1/nigeria/validate with sign: true |
GET base_url/api/v1/invoice/confirm/{IRN} | Taxpayer, else AP | nrs_status leg; GET /api/v1/nigeria/invoices/{transaction_id}?refresh=true; issuing, to settle a sign whose answer was lost or a duplicate (with the key that issued it: the taxpayer key, or a service provider's own SI or AP key for its own account business); the webhook, on TRANSMITTED or ACKNOWLEDGED for an issued invoice |
GET base_url/api/v1/invoice/download/{IRN} | Taxpayer or AP | POST /api/v1/nigeria/decrypt (taxpayer key first, then the organisation's own AP key); nrs_receive leg (the operator's AP key first, then the receiving organisation's taxpayer key) |
PATCH base_url/api/v1/invoice/update/{IRN} | Taxpayer, else AP | PATCH /api/v1/nigeria/invoices/{transaction_id}/payment |
POST base_url/api/v1/invoice/transmit/{IRN} | AP | nrs_transmit leg |
PATCH base_url/api/v1/invoice/transmit/{IRN} | AP | Acknowledgement: nrs_receive leg, POST /api/v1/nigeria/received/{transaction_id}/acknowledge |
GET base_url/api/v1/invoice/transmit/lookup/{IRN} | AP | Webhook; nrs_receive leg |
GET base_url/api/v1/invoice/transmit/lookup/tin/{TIN} | AP | GET /api/v1/nigeria/lookup/tin/{tin}; before a transmission, to find the supplier's access point (issuing, the nrs_transmit leg, Transmit now) |
GET base_url/api/v1/invoice/transmit/pull | AP | Five-minute pull; POST /api/v1/nigeria/received/pull |
GET base_url/api/v1/invoice/transmit/self-health-check | AP | POST /api/v1/nigeria/health-check |
GET base_url/api/v1/entity and GET base_url/api/v1/entity/{id} | SI (or a taxpayer key NRS lets list) | Business discovery; also with the AP key, to learn the operator's own access-point name for the webhook check |
POST base_url/api/v1/entity and POST base_url/api/v1/entity/{id}/business | SI | POST /api/v1/nigeria/businesses |
GET base_url/api/v1/invoice/resources/{name} | Taxpayer, else SI, else AP | GET /api/v1/nigeria/resources/{name} |
POST https://api.firsmbs.com/api/v1/integration/verify_stamp | None (public) | Issuing; POST /api/v1/nigeria/qr; nrs_receive leg |
Every call sends the key as the x-api-key and x-api-secret headers.
Invoice schema essentialsโ
| Field | Required | Notes |
|---|---|---|
business_id | โ | NRS business UUID. Filled in by the platform from the issuing business |
irn | โ | {invoice number}-{Service ID}-{YYYYMMDD}. Built by the platform (The IRN) |
invoice_number | Set by the platform from invoice_no when absent | |
issue_date | โ | ISO date (YYYY-MM-DD); the IRN's date |
due_date | optional | ISO date |
invoice_type_code | โ | NRS's codes: 381 Commercial Invoice, 380 Credit Note, 384 Debit Note, 396 Invoice Request (Invoice type codes). Not the UNCL1001 meanings |
invoice_kind | โ | B2B, B2C or B2G. The platform sends B2B when it is missing and refuses any other value (422 C5_NG_INVOICE_KIND_INVALID) |
document_currency_code, tax_currency_code | โ | For example NGN; NRS's list is GET /api/v1/nigeria/resources/currencies |
billing_reference | for notes | Array of { irn, issue_date } of the invoice being corrected. Required on a credit note (380) or debit note (384); the platform refuses a note without it before calling NRS. Not used on a commercial invoice (381) |
accounting_supplier_party | โ | party_name, tin, email, postal_address (and telephone, business_description). The supplier TIN must be the TIN on the NRS business; the platform fills tin and party_name from the business when they are empty |
โฆ_party.postal_address | โ | street_name, city_name, postal_zone, lga, state, country. NRS documents postal_zone, lga and state as mandatory on its per-party schema pages, though its main schema example omits lga and state; when last tested the sandbox enforced postal_zone and accepted an address without lga and state. Send all three. Codes from GET /api/v1/nigeria/resources/states and GET /api/v1/nigeria/resources/lgas |
accounting_customer_party | โ | Same shape. tin is required on every invoice, B2C included: the platform refuses an invoice without it |
tax_total[] | โ | tax_amount and tax_subtotal[] with taxable_amount, tax_amount, tax_category { id, percent }; categories from GET /api/v1/nigeria/resources/tax-categories (standard VAT: STANDARD_VAT, 7.5 %) |
legal_monetary_total | โ | line_extension_amount, tax_exclusive_amount, tax_inclusive_amount, payable_amount |
invoice_line[] | โ | Each line: hsn_code (goods, with product_category) or isic_code (services), invoiced_quantity, line_extension_amount, item { name, โฆ }, price { price_amount, base_quantity, price_unit } |
payment_means[], payment_terms_note, buyer_reference, tax_point_date, actual_delivery_date | optional | Payment means codes from GET /api/v1/nigeria/resources/payment-means |
The platform computes no amounts and no tax: the invoice carries its own lines, totals and tax, and NRS validates
them. Apart from the fields named above, the platform sends the invoice to NRS as submitted and returns NRS's own
field-level messages in issues[]. The authoritative schema, code lists and changelog are published by NRS at
einvoice.nrs.gov.ng/docs.
Environmentsโ
| Environment | Address | Notes |
|---|---|---|
| NRS sandbox | https://eivc-k6z6d.ondigitalocean.app | The default base_url |
| NRS production | Issued by NRS in the service-provider portal when production access is granted | Set it as base_url with PUT /api/v1/nigeria/settings, re-entering every stored key in the same request |
QR verification (verify_stamp) | https://api.firsmbs.com | Public, no key. One setting for the whole platform, not per organisation |
Sandbox checklistโ
- Keys. The platform sets the service provider's role to
service_provider. The service provider stores its SI and AP keys, and each taxpayer organisation its taxpayer key, withPUT /api/v1/nigeria/settings.GET /api/v1/nigeria/settingsshould showhas_taxpayer_key,has_si_keyandhas_ap_keyastrue, withsi_ap_inherited_fromnaming the service provider for a client organisation. - Webhook. Register
webhook_urlin the NRS portal, thenPOST /api/v1/nigeria/health-check: NRS answers and sends a test notification. - Business.
GET /api/v1/nigeria/businesses?refresh=true(for the service provider this also lists its Access Point account's businesses), orPOST /api/v1/nigeria/businessesfor a new one. Wait for NRS to approve a new business: its QR keys come from the Approved Business row. - QR keys.
PUT /api/v1/nigeria/businesses/{business_id}/keyswith that business'scrypto_keys.txt. Check that thecertificatevalue does not beginbEF0V3FJbmo5, which is NRS's published sample. - Validate.
POST /api/v1/nigeria/validateuntil"ok": true. - Issue.
POST /api/v1/nigeria/invoices:"signed": true,"qr_verified": trueandissuernaming your business. If the issuer is another name, the stored QR keys are not the business's own. A service provider's account business with no NRS keys is signed without a QR code ("qr": "not_available"), soqr_verifiedandissuerarenullfor it. Iftransmit_reason_codeisC5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT, the supplier's own access point transmits the invoice, not yours. - Track.
GET /api/v1/nigeria/invoices/{transaction_id}?refresh=true:confirmshows NRS's view; the legs show the transmission. - Download.
POST /api/v1/nigeria/decryptwith the IRN returns the invoice you issued. - Receive. Check the buyer first with
GET /api/v1/nigeria/lookup/tin/{tin}. Have an invoice sent to one of your businesses: it appears inGET /api/v1/nigeria/received(or afterPOST /api/v1/nigeria/received/pull), its content is fetched by the receive leg, and it is acknowledged automatically or withPOST /api/v1/nigeria/received/{transaction_id}/acknowledge.
Sandbox notesโ
380is a credit note on NRS, not an invoice. NRS's invoice-type list uses381for a commercial invoice. Sending380makes the document a credit note, which needs abilling_reference.- Each NRS key has its own set of calls. A taxpayer key cannot list entities: NRS answers
403(entity_actions). An Access Point key downloads invoices addressed to a buyer on its own access point, and transmits only for a supplier on its own access point (404withtransmit_actionsotherwise). The platform picks the key each call needs; a key stored in the wrong field shows up asC5_NG_WRONG_KEY_ROLE. postal_zoneis enforced on addresses, and NRS documentslgaandstateas mandatory too; their codes come from NRS's own lists.invoice_kindis mandatory. The platform sendsB2Bwhen it is missing and refuses any value other thanB2B,B2CorB2G; set it explicitly.- B2C still needs a customer TIN. The platform refuses a B2C invoice without one before calling NRS.
- Use the certificate value as it is in the QR payload; decoding it first makes NRS answer "Unknown Issuer".
- An IRN is issued once. It contains the issue date. The platform refuses an IRN it has issued, and NRS refuses one it has signed.
- HSN or ISIC per line. Goods lines carry
hsn_codeandproduct_category, service linesisic_code. - Read
error.details, notpublic_message. NRS returns the same generic public message for most validation failures, whiledetailsnames the field; the platform'sissues[]is built fromdetails.