Skip to main content

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:

  1. 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.
  2. 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.
  3. Track. Read the invoice's status from NRS until NRS reports it delivered and acknowledged, and report payment status changes to NRS.
  4. 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.
Check that your service has the Nigeria endpoints

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_code JURISDICTION_NOT_AVAILABLE means 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.

Every identity on this page is an example

"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โ€‹

AspectDetails
Tax authorityNigeria Revenue Service (NRS), formerly the Federal Inland Revenue Service (FIRS)
E-invoicing platformNRS MBS: einvoice.nrs.gov.ng
ModelClearance. 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 rolesSystem 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)
IdentifiersTIN (Taxpayer Identification Number). NRS also gives each business a business_id (UUID) and an 8-character Service ID
Invoice referenceIRN: {invoice number}-{Service ID}-{YYYYMMDD}, for example INV1001-A1B2C3D4-20261012
Document formatNRS invoice JSON (UBL-aligned field names). The platform also keeps a UBL XML rendering of every invoice for preview
Invoice typesNRS's own list: 381 Commercial Invoice, 380 Credit Note, 384 Debit Note, 396 Invoice Request (details)
Invoice kindsinvoice_kind: B2B, B2C or B2G
QR codeNRS'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)
EnvironmentsSandbox and production
Peppol statisticsNRS 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 documentationeinvoice.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.

KeyUsually held byThe platform uses it toInherited from a parent organisation
Taxpayer keyEach taxpayer organisation, for its own invoicesValidate, sign, download and confirm the organisation's own invoices; report payment status; read NRS's reference listsNever
System Integrator (SI) keyThe service providerList 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 businessesYes, except to sign as an account business
Access Point (AP) keyThe service providerTransmit 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 businessesYes, 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 belowWho has it
authenticatedAny valid API key or signed-in user of the organisation
invoices:sendAn API key with the send or admin scope, or a user whose role grants invoices:send
settings:manageAn 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โ€‹

MethodPathPermissionPurpose
GET/api/v1/nigeria/settingsauthenticatedThe 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/settingssettings:manageStore 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/businessesauthenticatedThe organisation's NRS businesses; ?refresh=true re-discovers them from NRS
POST/api/v1/nigeria/businessessettings:manageOnboard a taxpayer business with NRS (SI key)
PUT/api/v1/nigeria/businesses/{business_id}/keyssettings:manageStore a business's QR keys (public_key and certificate from its crypto_keys.txt)
GET/api/v1/nigeria/resources/{name}authenticatedNRS reference lists (states, LGAs, invoice types, ...), cached 24 hours
POST/api/v1/nigeria/health-checksettings:manageNRS'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:sendAsk NRS whether a buyer's TIN is reachable through an access point (AP key)
POST/api/v1/nigeria/invoicesinvoices:sendIssue an invoice: validate, sign, QR and record it, then transmit it and track its status
GET/api/v1/nigeria/invoicesauthenticatedInvoices issued through NRS, newest first, paged
GET/api/v1/nigeria/invoices/{transaction_id}authenticatedOne issued invoice with its QR, legs and NRS status; ?refresh=true reads NRS first
POST/api/v1/nigeria/invoices/{transaction_id}/transmitinvoices:sendTransmit now (also for an invoice issued with transmit: false)
PATCH/api/v1/nigeria/invoices/{transaction_id}/paymentinvoices:sendReport the payment status to NRS
POST/api/v1/nigeria/validateinvoices:sendValidate with NRS without issuing; with sign: true, issue without transmitting
POST/api/v1/nigeria/qrinvoices:sendBuild and check the QR code for an IRN with the business's own keys
POST/api/v1/nigeria/decryptinvoices:sendDownload a signed invoice from NRS and decrypt it (taxpayer key, then the organisation's own AP key)
GET/api/v1/nigeria/receivedauthenticatedThe inbox: invoices received through NRS, newest first, paged
GET/api/v1/nigeria/received/{transaction_id}authenticatedOne received invoice with its content and parties (read-only)
POST/api/v1/nigeria/received/{transaction_id}/acknowledgeinvoices:sendAcknowledge receipt to NRS (AP key)
POST/api/v1/nigeria/received/pullinvoices:sendPull waiting invoices from NRS now (AP key)
POST/api/v1/public/nrs/webhooknone (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.

HTTPMeaning
400A key, a business or a setting is missing, or the input is refused (reason code in the body)
403The caller lacks the permission (above)
404No such invoice in this organisation ("message": "Transaction not found"), or an unknown resource list
409A 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
422An 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[])
502NRS 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)
503C5_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 codeWhen
C5_NG_TAXPAYER_CREDENTIALS_REQUIREDThe 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_REQUIREDNo 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_REQUIREDNo 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_UNAVAILABLEThe encrypted credential store is not configured on this platform
C5_NG_WRONG_KEY_ROLENRS refused the key for this call: a key of another kind is stored in that field
C5_NG_UNSAFE_BASE_URLThe NRS base URL points at a host the platform will not connect to
C5_NG_NRS_ERRORNRS 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_RESPONSENRS answered with something that could not be read (for example a created business without an id)
C5_NG_KEYS_REQUIRED_FOR_NEW_BASE_URLA new base URL was sent without every stored key re-entered, key and secret (missing lists them)
C5_NG_ROLE_PLATFORM_OWNEDA role change from anyone but a super admin (422); nothing in the request was written
C5_NG_DEFAULT_HSN_CODE_INVALIDA 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_INVALIDA default_product_category longer than 255 characters (422); nothing in the request was written
C5_NG_NOT_ENROLLEDThe 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_REQUIREDNo business named and no default business set
C5_NG_BUSINESS_NOT_FOUNDThe 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_BUSINESSThe business named is NRS's demo business, which never issues invoices, signs QR codes or becomes the default
C5_NG_BUSINESS_HELD_BY_ANOTHER_ORGOnboarding 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_REQUIREDA 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_KEYSNot 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_CERTIFICATEThe certificate is NRS's published sample, not the business's own
C5_NG_BILLING_REFERENCE_REQUIREDA credit note (380) or debit note (384) without billing_reference
C5_NG_CUSTOMER_TIN_REQUIREDaccounting_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_INVALIDinvoice_kind is not B2B, B2C or B2G (422)
C5_NG_PAYMENT_DUE_DATE_REQUIREDA 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_INVALIDThe invoice number is missing, or is not letters, digits and single inner hyphens of at most 64 characters
C5_NG_IRN_MISMATCHAn 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_IRNThe 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_MAPPINGFrom 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_REJECTEDNRS refused the invoice at validation (see issues[]); also an invalid payment status request
C5_NG_SIGN_REJECTEDNRS refused to sign the invoice (see issues[])
C5_NG_NRS_UNREACHABLENRS could not be reached (or answered 5xx / 429) after the retries. While issuing, it means nothing was issued
C5_NG_SIGN_OUTCOME_UNKNOWNNRS stopped answering while signing and could not confirm whether it signed; nothing was recorded
C5_NG_TRANSMIT_NOT_OWEDThe invoice is B2C, or transmission is switched off on this platform
C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINTOnly 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_AUTHORISEDNRS answered 403 to the transmission (leg reason)
C5_NG_TRANSMIT_FAILEDNRS 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_DELIVERYNRS has not yet reported the invoice delivered and acknowledged; the status leg checks again (leg reason, not a failure)
C5_NG_STATUS_FAILEDThe status could not be read from NRS after three refusals (leg reason)
C5_NG_STATUS_TIMEOUTNRS did not report the invoice delivered within 30 days (leg reason)
C5_NG_NOT_ACKNOWLEDGED_BY_BUYERDelivered, but not acknowledged by the buyer within 30 days (leg reason)
C5_NG_RECEIVE_FAILEDA received invoice could not be fetched after three refusals (leg reason)
C5_NG_CONTENT_PENDINGA 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_UNASSIGNEDA received invoice that matches none of the operator's organisations
C5_NG_LEGACY_RECORDAn 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:

WhatWhere it comes fromStored 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 pairPUT /api/v1/nigeria/settings
The business: business_id, Service ID, TINAssigned by NRS when the business is createdDiscovered from NRS, or created with POST /api/v1/nigeria/businesses
The business's QR keys: public_key and certificate from its crypto_keys.txtNRS portal: System Integrator, API Integration, Approved Business list, Get Cryptographic Keys on that business's rowPUT /api/v1/nigeria/businesses/{business_id}/keys
Creating a business is not the same as NRS approving it

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 own key instructions are out of date

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.

FieldNotes
taxpayer_api_key, taxpayer_api_secretThe organisation's own taxpayer key. The older names api_key and api_secret set the same key
si_api_key, si_api_secretSystem Integrator key
ap_api_key, ap_api_secretAccess Point key
base_urlNRS 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_idThe 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
roleservice_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_acknowledgeDefault true: acknowledge a received invoice to NRS as soon as its content is fetched (Acknowledging)
default_hsn_codeThe 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_categoryThe 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
enabledSaving the settings enrols the organisation in NRS e-invoicing; false withdraws it
service_idIgnored: the Service ID comes from the business
nrs_public_key, certificate_b64Refused 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
}
FieldMeaning
configured, has_api_secret, has_taxpayer_keyA taxpayer key is stored for this organisation
api_key_maskedThe taxpayer x-api-key, first and last four characters only
has_si_key, has_ap_keyAn SI or AP key is available, the organisation's own or inherited
si_ap_inherited_fromThe organisation whose SI or AP key is used, when it is not this one
business_id, service_id, business_name, tin, entity_idThe default business; all null when there is none
has_public_key, has_certificateThe default business's QR keys are stored
discoveredThe default business has a Service ID from NRS
discovery_messageWhy discovery did not run or found no default; null otherwise
webhook_urlThe https URL to register in the NRS portal (The webhook)
credential_store_availablefalse when the encrypted credential store is not configured or cannot be read on this platform
default_hsn_code, default_product_categoryThe 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:

PartFieldsWritten by
Tenant settingsenabled, default_business_id, auto_acknowledge, default_hsn_code, default_product_categoryThe organisation: PUT /api/v1/nigeria/settings, or the generic PUT /api/v1/jurisdictions/NG/settings
Platform-owned settingsrole, base_url and the business listOnly 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_provider holding 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 the taxpayer role, 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:

accountThe business isValidated and signed with
taxpayerA taxpayer's business (every business listed before this field existed reads as taxpayer)The organisation's own taxpayer key; its QR keys are required
siListed or onboarded with the organisation's own SI keyThat SI key, when the organisation is a service_provider
apOne of the NRS entities filed under the organisation's own access pointThat 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.

FieldRequiredNotes
nameYesThe registered business name
tinYesThe business's TIN
referenceNoYour reference for the business; generated from the name when omitted
entity_idNoAn 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.

The QR code must carry the supplier's own certificate

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.

FieldTypeRequiredDescription
invoiceobjectYesThe NRS invoice JSON (schema)
invoice_nostringUnless invoice.invoice_number is setThe invoice number, the first part of the IRN: letters, digits and single hyphens between them, at most 64 characters
business_idstringUnless the organisation has a default businessThe business issuing the invoice. Else invoice.business_id; else the organisation's default
transmitbooleanNo, default trueTransmit 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:

  1. 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's account:
    • 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 si business, or its own AP key for an ap business; an inherited key is refused (400 C5_NG_SI_CREDENTIALS_REQUIRED or C5_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).
  2. Completes the invoice: sets business_id; fills the supplier's tin and party_name from the business when they are empty; sets invoice_kind to B2B when it is missing (any value other than B2B, B2C or B2G is refused with 422 C5_NG_INVOICE_KIND_INVALID); sets invoice_number; gives a payment_means entry without payment_due_date the invoice's due_date; and builds the IRN.
  3. 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); a payment_means entry still without payment_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).
  4. 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 its ap account 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's app_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.
  5. Validates the invoice with NRS, with the key from step 1.
  6. 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).
  7. 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_matches is false and message says 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_base64 and qr_verified are null, and message reads "NRS issues cryptographic keys only to approved businesses; this invoice is signed without a QR."
  8. 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_transmit and nrs_status for an invoice to transmit; nrs_status alone for a B2C invoice, or for a supplier that step 4 found on another access point; none for an invoice issued with transmit: false.
  9. 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 (or invoice.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 when issue_date is 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:

CodeNRS meaningbilling_reference
381Commercial InvoiceNot used
380Credit NoteRequired
384Debit NoteRequired
396Invoice RequestNot 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"
}
FieldMeaning
ok, signedNRS validated and signed the invoice, and it is recorded
irnThe invoice's IRN
transaction_idThe platform's id for the invoice; use it with the other /api/v1/nigeria/invoices/{transaction_id} routes
signed_withThe key that validated and signed: taxpayer, si or ap
qrstamped: 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_base64The QR code as a PNG image, base64-encoded, to print on the invoice; null without a QR
qr_signatureThe 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_verifiedtrue: 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_matchesThe issuer NRS names for the QR, and whether it is the issuing business
legsWhat 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_owedWhether a transmission is owed from here: false for B2C, transmit: false, or a supplier on another access point
transmit_reason_code, transmit_reasonC5_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_referenceThe supplier's access point, when step 4 could find it; null otherwise
validation, signNRS's own answers to the two calls
messageA 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 400 refusal, "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 in sign ({ "recovered": "duplicate", "confirm": { โ€ฆ } }).
  • NRS holds it, and this organisation has already recorded it: a duplicate. The answer is 200 with "ok": false, "duplicate": true and reason_code C5_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 200 with "ok": false, "duplicate": false, reason_code C5_NG_SIGN_REJECTED and "message": "NRS could not sign this invoice."; nothing is recorded. issues[] leaves NRS's generic text out, and nrs_error carries 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 200 with "ok": true, "message": "NRS stopped answering while signing, but confirms it holds the invoice; it is recorded." and NRS's confirm record in sign ({ "recovered": "confirm", "confirm": { โ€ฆ } }).
    • NRS does not hold it: 502 C5_NG_NRS_UNREACHABLE with "message": "NRS stopped answering while signing and does not hold the invoice: nothing was issued; issue it again." and the irn. Nothing was issued or recorded.
    • NRS cannot say: 502 C5_NG_SIGN_OUTCOME_UNKNOWN with the irn; 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.
{
"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.

FieldTypeDescription
invoiceobjectThe NRS invoice JSON
signbooleanDefault false. true issues the invoice exactly as POST /api/v1/nigeria/invoices with transmit: false (recorded, no legs)
invoice_nostringAs for issuing
business_idstringAs 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:

  1. payload = { "irn": "{IRN}.{unix timestamp}", "certificate": "{certificate from crypto_keys.txt, as it is}" }
  2. ciphertext = RSA PKCS#1 v1.5 encryption of the payload with the public_key from crypto_keys.txt (NRS's key)
  3. 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".

What verify_stamp proves

NRS 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.

FieldRequiredDescription
invoice_noYesThe IRN's first part
business_idNoThe business; default the organisation's default business
issue_dateNoThe IRN's date, YYYY-MM-DD; default today (UTC)
service_idNoWhen 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.

PathWhat happens for an organisation in Nigeria
POST /api/v1/invoicesIssued through NRS; answers 202 with an nrs block (below)
POST /api/v1/invoices/validateMapped, checked and validated with NRS. Never signed, nothing recorded (below)
POST /api/v1/invoices/import/csvEach 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/previewEach 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/templateThe Nigeria template (below)
POST /api/v1/drafts/{draft_id}/sendThe draft is issued through NRS; a refused draft is marked error with the reason
POST /api/v1/batches/csvThe 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}/processEach item is validated with NRS (never signed), then issued through NRS
POST /api/v1/invoices/credit-note/sendThe 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}/sendEach 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.

FieldWhereDescription
ng_hsn_codeeach lineThe 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_categoryeach lineThe line's product category, free text (at most 255 characters). Default: the organisation's default_product_category, else the line's description
ng_business_idinvoiceThe NRS business issuing the invoice (Businesses). Default: the organisation's default business
ng_invoice_kindinvoiceB2B (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):

SettingFormatUsed for
default_hsn_code0000.00, for example 1006.30A line with no ng_hsn_code of its own
default_product_categoryFree text, at most 255 charactersA 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:

ConnectorThe line's HSN code comes from
OdooThe product's HS Code (hs_code), where the database's apps define the field. Read only for an organisation in Nigeria
Zoho BooksThe line item's HSN/SAC (hsn_or_sac), where the organisation's Zoho Books edition has it
SAPThe billing item's CommodityCode, where the connected service carries it (the standard billing document service does not)
Amazon, bol.comNothing: 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_id nor 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.

ColumnAlsoFills
hsn_codeline_hsn_code, line_ng_hsn_code, ng_hsn_codethe line's ng_hsn_code (0000.00; blank: the organisation's default_hsn_code)
product_categoryline_product_category, line_ng_product_category, ng_product_categorythe line's ng_product_category
invoice_kindng_invoice_kindng_invoice_kind: B2B, B2C or B2G (anything else is a row error)
business_idng_business_id, nrs_business_idng_business_id
billing_irnng_billing_irn, original_irna credit or debit note's preceding_invoice.number: the IRN of the invoice it amends
billing_issue_dateng_billing_issue_date, original_issue_dateits preceding_invoice.issue_date (YYYY-MM-DD)
buyer_emailcustomer_emailbuyer.email (required)
buyer_phonebuyer_telephone, customer_phonebuyer.phone
seller_emailseller.email (blank: the company profile's, then the organisation's)
seller_phoneseller_telephoneseller.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 fieldFrom
invoice_numbernumber. It becomes the IRN's first part, so it may hold only letters, digits and single hyphens between them
invoice_type_codedocument_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_referenceFor 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_dateThe same fields
document_currency_code, tax_currency_codecurrency, and tax_currency (default currency)
accounting_supplier_partyseller: 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_partybuyer: name, the TIN (below), email, phone, address
Party TINThe party's vat_number; else its identifier when identifier_scheme is TIN
postal_addressstreet (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_totalOne 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_totalWith totals_authority system (the default), computed from the lines; with client, the invoice's own totals
payment_meansOnly 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
notenotes, 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โ€‹

HTTPerror_codeWhen
422C5_NG_GENERIC_MAPPINGThe invoice lacks what NRS requires (above). NRS is not called. issues[] has one sentence per problem
422C5_NG_VALIDATION_REJECTED, C5_NG_SIGN_REJECTED, C5_NG_DUPLICATE_IRNNRS 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, 503the pack's reason codeEvery 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 answerLeg outcome
200, or NRS says the IRN is transmitted alreadysubmitted, 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
403failed, C5_NG_TRANSMIT_NOT_AUTHORISED
NRS unreachable, 5xx or 429Tried again with back-off; never given up
Any other refusalTried again; failed with C5_NG_TRANSMIT_FAILED on the third refusal
No AP key on the organisation or its parentsheld 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: the nrs_transmit leg is created, and the nrs_status leg that watches the delivery with it ("action": "created");
  • a leg that is waiting (pending, or held for 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 (no leg_status).
  • A transmit leg still waiting (pending) is ended: it becomes skipped with that reason, the invoice records transmit_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:

statusMeaning
signedIssued. 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
transmittingThe platform has asked NRS to transmit the invoice
transmittedNRS reports the invoice transmitted or delivered
acknowledgedNRS's status shows the buyer's acknowledgement
failedA 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 acknowledged at NRS's first answer, since NRS holds the signed invoice and nobody receives it.
  • Transmitted: the leg moves the nrs_transmit leg to acknowledged once NRS reports the invoice transmitted, and ends acknowledged once NRS reports it delivered and acknowledged.
  • 30 days after the leg is created: it ends acknowledged with C5_NG_NOT_ACKNOWLEDGED_BY_BUYER if NRS reports the invoice delivered, otherwise failed with C5_NG_STATUS_TIMEOUT.
  • NRS unreachable: tried again, never given up. NRS refusing the read three times: failed, C5_NG_STATUS_FAILED. No key: held until 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}.

FieldRequiredDescription
payment_statusYesPAID, PARTIAL, PENDING or REJECTED (another value: 422)
amountFor PARTIALThe amount paid (400 C5_NG_VALIDATION_REJECTED when missing)
referenceNoYour 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: 381 COMMERCIAL INVOICE, 380 CREDIT NOTE, 384 DEBIT NOTE, 396 INVOICE 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:

    1. IRN INV1001-A1B2C3D4-20261012;
    2. Signed by NRS (Nigeria Revenue Service) on 2026-10-12 (the date NRS signed it);
    3. what NRS said of the QR's issuer when the platform checked it: QR issuer verified by NRS: Acme Supplies Ltd (QR verified by NRS when NRS named no issuer), QR not verified by NRS, or QR verification unavailable when NRS's verification service did not answer;
    4. 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 businesses for an account business that NRS issued no keys to, otherwise QR not available.

    For a received invoice: the IRN line; Received through NRS; acknowledged 2026-10-12 or Received 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:ID and cac:PartyLegalEntity/cbc:CompanyID, with schemeID="NG-TIN". NG-TIN is 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 with schemeID="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:AdditionalDocumentReference with cbc:ID QR and the QR content in cbc: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:

AddressWhen
https://{portal host}/nrs/webhookThe 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 }
FieldValues
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 its nrs_status leg reads NRS at once. On TRANSMITTED or ACKNOWLEDGED, the platform also reads GET base_url/api/v1/invoice/confirm/{IRN} itself and, if NRS reports the invoice transmitted, moves a submitted transmit leg to acknowledged. 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 an nrs_receive leg; later notifications for that IRN are added to its events.

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:

  1. the buyer's NRS business_id;
  2. the buyer's business reference;
  3. 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:

  1. Parties: GET base_url/api/v1/invoice/transmit/lookup/{IRN} with the AP key. Both parties' access points and references are kept as parties (TINs masked, as NRS gives them).

  2. Content, from the first source that has it:

    1. the invoice inside the pulled item, if NRS included one (decrypted with the AP key when it is encrypted);
    2. 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;
    3. the same download with the receiving organisation's own taxpayer key;
    4. else none yet.

    content_source on the received invoice says where the content came from (pull or download).

  3. QR check: when the content carries a QR code, NRS verify_stamp names its issuer, which is compared with the supplier's name.

  4. 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).

  5. 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_receive leg, once the content is stored, when the receiving organisation's auto_acknowledge is 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_byMeaning
autoBy the nrs_receive leg, under auto_acknowledge
manualWith POST /api/v1/nigeria/received/{transaction_id}/acknowledge
nrsNRS'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_stamp check of a QR code has 10 seconds, 5 to connect, and is not retried: when it does not answer, or answers 5xx or 429, qr_verified is null (unknown), never false.
  • 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 5xx or 429. An NRS answer in the 4xx range 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 confirm instead (NRS unreachable).
  • Redirects are not followed, so the NRS key headers go only to the screened base URL.
  • Each call carries one X-Correlation-ID header, 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's handler, error id and details. 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 offEffect
TransmissionIssued 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 trackingIssued invoices get no nrs_status leg; GET /api/v1/nigeria/invoices/{transaction_id}?refresh=true still reads NRS
ReceivingNotifications 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 endpointNRS keyCalled by
POST base_url/api/v1/invoice/validateTaxpayer; for a service provider's own account business, its own SI or AP keyIssuing; POST /api/v1/nigeria/validate
POST base_url/api/v1/invoice/signTaxpayer; for a service provider's own account business, its own SI or AP keyIssuing; POST /api/v1/nigeria/validate with sign: true
GET base_url/api/v1/invoice/confirm/{IRN}Taxpayer, else APnrs_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 APPOST /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 APPATCH /api/v1/nigeria/invoices/{transaction_id}/payment
POST base_url/api/v1/invoice/transmit/{IRN}APnrs_transmit leg
PATCH base_url/api/v1/invoice/transmit/{IRN}APAcknowledgement: nrs_receive leg, POST /api/v1/nigeria/received/{transaction_id}/acknowledge
GET base_url/api/v1/invoice/transmit/lookup/{IRN}APWebhook; nrs_receive leg
GET base_url/api/v1/invoice/transmit/lookup/tin/{TIN}APGET /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/pullAPFive-minute pull; POST /api/v1/nigeria/received/pull
GET base_url/api/v1/invoice/transmit/self-health-checkAPPOST /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}/businessSIPOST /api/v1/nigeria/businesses
GET base_url/api/v1/invoice/resources/{name}Taxpayer, else SI, else APGET /api/v1/nigeria/resources/{name}
POST https://api.firsmbs.com/api/v1/integration/verify_stampNone (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โ€‹

FieldRequiredNotes
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_numberSet by the platform from invoice_no when absent
issue_dateโœ…ISO date (YYYY-MM-DD); the IRN's date
due_dateoptionalISO 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_referencefor notesArray 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_dateoptionalPayment 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โ€‹

EnvironmentAddressNotes
NRS sandboxhttps://eivc-k6z6d.ondigitalocean.appThe default base_url
NRS productionIssued by NRS in the service-provider portal when production access is grantedSet 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.comPublic, no key. One setting for the whole platform, not per organisation

Sandbox checklistโ€‹

  1. 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, with PUT /api/v1/nigeria/settings. GET /api/v1/nigeria/settings should show has_taxpayer_key, has_si_key and has_ap_key as true, with si_ap_inherited_from naming the service provider for a client organisation.
  2. Webhook. Register webhook_url in the NRS portal, then POST /api/v1/nigeria/health-check: NRS answers and sends a test notification.
  3. Business. GET /api/v1/nigeria/businesses?refresh=true (for the service provider this also lists its Access Point account's businesses), or POST /api/v1/nigeria/businesses for a new one. Wait for NRS to approve a new business: its QR keys come from the Approved Business row.
  4. QR keys. PUT /api/v1/nigeria/businesses/{business_id}/keys with that business's crypto_keys.txt. Check that the certificate value does not begin bEF0V3FJbmo5, which is NRS's published sample.
  5. Validate. POST /api/v1/nigeria/validate until "ok": true.
  6. Issue. POST /api/v1/nigeria/invoices: "signed": true, "qr_verified": true and issuer naming 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"), so qr_verified and issuer are null for it. If transmit_reason_code is C5_NG_SUPPLIER_ON_OTHER_ACCESS_POINT, the supplier's own access point transmits the invoice, not yours.
  7. Track. GET /api/v1/nigeria/invoices/{transaction_id}?refresh=true: confirm shows NRS's view; the legs show the transmission.
  8. Download. POST /api/v1/nigeria/decrypt with the IRN returns the invoice you issued.
  9. Receive. Check the buyer first with GET /api/v1/nigeria/lookup/tin/{tin}. Have an invoice sent to one of your businesses: it appears in GET /api/v1/nigeria/received (or after POST /api/v1/nigeria/received/pull), its content is fetched by the receive leg, and it is acknowledged automatically or with POST /api/v1/nigeria/received/{transaction_id}/acknowledge.

Sandbox notesโ€‹

  • 380 is a credit note on NRS, not an invoice. NRS's invoice-type list uses 381 for a commercial invoice. Sending 380 makes the document a credit note, which needs a billing_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 (404 with transmit_actions otherwise). The platform picks the key each call needs; a key stored in the wrong field shows up as C5_NG_WRONG_KEY_ROLE.
  • postal_zone is enforced on addresses, and NRS documents lga and state as mandatory too; their codes come from NRS's own lists.
  • invoice_kind is mandatory. The platform sends B2B when it is missing and refuses any value other than B2B, B2C or B2G; 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_code and product_category, service lines isic_code.
  • Read error.details, not public_message. NRS returns the same generic public message for most validation failures, while details names the field; the platform's issues[] is built from details.