NRS API Through Your Domain
Call well-known NRS endpoints through the approved Gateway host and understand which routes are protected, relayed, and billed.
Your approved domain is an authenticated NRS API entry point for the operations listed on this page. For a supported direct NRS operation, replace the NRS origin with your Gateway origin and keep its documented HTTP method, path, and NRS request body.
NRS path: /api/v1/invoice/resources/tax-categories
Gateway URL: https://acme.invoice.tryduplo.com/api/v1/invoice/resources/tax-categoriesSend the API key generated in Duplo Dashboard as x-api-key on every request, as shown in the examples below.
Use only the Gateway headers documented for each operation. Gateway applies the approved business, state, and route rules before forwarding an eligible NRS request. A valid hostname and API key do not authorize an arbitrary URL path.
If you send x-request-id, Gateway treats it only as optional caller correlation metadata. It does not become the trusted Gateway or support identifier. Capture the Gateway-generated X-Request-ID on every response; for a Problem Details error, it equals body requestId.
X-Duplo-Gateway-Hop is a reserved loop-detection header. Do not send it or copy it from an internal request. A non-zero value on an inbound request is rejected with 508 GW_ROUTING_LOOP_DETECTED and requires an operator to correct routing.
The path is an allowlist, not a catch-all proxy
Gateway accepts only the exact method and canonical path shapes in the supported-endpoint table below, plus the Gateway-owned management endpoints in the API reference. An unlisted path returns GW_ROUTE_NOT_FOUND; a known path with the wrong method returns GW_METHOD_NOT_ALLOWED and an Allow header. Never construct a Gateway URL from untrusted input.
NRS publishing a new route, resource name, HTTP method, or query parameter does not make it available through the Gateway. Wait for a Duplo Gateway release and the corresponding update to this documentation before using it through your approved domain.
Billing rule
Ordinary NRS calls through the domain are unbilled. The only billable route is the exact POST /api/v1/invoice/sign operation. A sign attempt is debited before NRS is called; a definitive rejection is reversed, while an unconfirmed NRS outcome can retain the charge temporarily for reconciliation. Read Billing and safe retries before automating sign retries.
How routing works
Authenticate
The host and API key resolve one approved business and state in Duplo Dashboard.
Authorize
Gateway matches one documented method and canonical path, then applies tenant, mode, and represented-business rules.
Relay
For an eligible direct NRS route, Gateway forwards only the approved operation.
The supported surface has distinct handling rules:
POST /api/v1/invoice/signis intercepted for mode handling, tenant checks, billing, and recovery.POST /api/v1/invoice/validate,GET /api/v1/invoice/transmit/lookup/{irn}, andPOST /api/v1/invoice/transmit/{irn}are represented-business-bound Pass-through routes.- The resource, recipient, health, entity, and NRS-login operations listed below are bounded direct NRS routes. Inbound download and acknowledgement are recognized but blocked as described below.
- Every unlisted method/path combination, including another shape under
/api/v1/invoice/transmit, is rejected instead of being forwarded. /sub-businesses,/invoices,/health, and platform webhook/administration paths belong to Duplo, not NRS.
Gateway matches the same canonical path for authorization, billing classification, and forwarding. Query parameters are rejected on direct NRS relay routes. Gateway-owned collection reads accept only their explicitly documented query fields; for example, GET /sub-businesses accepts page and limit. Do not send encoded path separators, dot segments, backslashes, fragments, duplicate separators, malformed percent escapes, oversized paths, unknown or repeated query fields, or duplicate security-sensitive headers. Those values are rejected; Gateway does not normalize one target for authorization and forward a different target.
Requests with a JSON body must send Content-Type: application/json; unsupported or missing media types are rejected with GW_REQUEST_MEDIA_TYPE_UNSUPPORTED before a body is forwarded.
Header requirements by route
| Operation | x-api-key | x-sub-business-id | Idempotency key | Charge |
|---|---|---|---|---|
| Listed resource, health, recipient, entity, or NRS-login call | Required | Not a Gateway requirement | Not a Gateway requirement | No |
| List or read represented-business mappings | Required | Do not send | Not required | No |
| Validate raw invoice | Required | Required | Not required | No |
| Sign invoice | Required | Required | Strongly recommended in Pass-through; required in Stored | Yes |
| Look up a signed Pass-through IRN | Required | Required | Not required | No |
| Transmit a signed Pass-through IRN | Required | Required | Standard Idempotency-Key or legacy x-idempotency-key is optional in the current version; preserve it when supplied | No |
| Download or acknowledge an inbound IRN | Not currently available | Not currently available | Not applicable | No |
| Stored invoice read/transmit | Required | Sent where that operation documents it | Required for transmit | No additional sign charge |
Pass-through sign also accepts a positive x-attempt-number, which defaults to 1. Keep it stable with the idempotency key for one logical attempt; read Billing and safe retries before creating a later attempt.
The credential IP list covers this hostname
ipWhitelisted is enforced on authenticated customer-facing custom-domain
Gateway requests as well as direct Spend API calls. Duplo's trusted edge
overwrites the configured client-IP header, and Gateway carries that address
over its authenticated internal authorization hop. Gateway never trusts a
caller's X-Forwarded-For value. An empty list is unrestricted; when the
list is configured, a missing, malformed, or non-matching trusted address
returns 403. See Webhook and network settings.
NRS may impose additional fields or semantics on an ordinary proxied route. Follow the official NRS e-Invoicing documentation, version 1.1.6 for the regulator-owned contract.
NRS contract compatibility
Gateway uses an explicit NRS adapter selected independently for TEST and LIVE. The current Gateway adapter version is v1; it is a Duplo integration-contract version and is distinct from the version label in NRS's public documentation.
For an ordinary successful relay, Gateway preserves the NRS 2xx status and body bytes after filtering response headers. Unknown additive response fields therefore remain compatible. Gateway performs stricter runtime validation only when its own safety decision depends on the response, including onboarding authentication/entity selection, represented-business identity, sign success, and IRN ownership.
A provider change may require a Gateway release
A new NRS path, method, API version, credential header, redirect, renamed decision field, or changed field type is not adopted automatically. Gateway redirects are disabled. Duplo tests a new adapter with golden fixtures and the NRS sandbox in TEST, then uses a limited LIVE canary before changing the selected contract version for that state.
Gateway never treats an unfamiliar sign envelope as success merely because its HTTP status is 2xx. Signing requires HTTP 2xx plus body code: 201 and data.ok: true; malformed JSON, 202, empty 204, a renamed field, or another unknown result is GW_SIGN_OUTCOME_UNCONFIRMED after debit. Deployment-owned NRS credential rejections are mapped to an operator dependency failure rather than an invoice or customer-credential rejection.
Credential rotation may stage a current and next credential, but Gateway does not retry an uncertain mutation with another credential. Identity drift is also explicit: changed name, template, or active status can be reviewed, while business-ID or TIN drift requires an audited migration. Never silently replace the stored NRS identity or stop accepting a previously published identifier alias.
Supported NRS endpoints
This table is the complete NRS route allowlist exposed through the Gateway for the current release. It is still not a replacement for the NRS payload reference: response fields and regulator-owned resource values can change independently of Gateway. However, a new NRS route, resource endpoint, method, or query option remains unavailable until both the Gateway and this table are released with support for it.
| Method and path | Purpose | Gateway handling |
|---|---|---|
POST /api/v1/utilities/authenticate | Authenticate an NRS portal user and obtain an entity identifier | Direct, unbilled diagnostic |
GET /api/v1/entity/{entityId} | Read the entity and its NRS businesses | Direct and unbilled |
GET /api/v1/invoice/transmit/lookup/tin/{tin} | Check whether a recipient TIN can receive | Direct and unbilled |
GET /api/v1/invoice/transmit/self-health-check | Check the current sender/NRS connection | Direct and unbilled |
GET /api/v1/invoice/resources/tax-categories | Fetch current tax categories and rates | Direct and unbilled |
GET /api/v1/invoice/resources/vat-exemptions | Fetch current VAT exemptions | Direct and unbilled |
GET /api/v1/invoice/resources/hs-codes | Fetch product classification codes | Direct and unbilled |
GET /api/v1/invoice/resources/services-codes | Fetch service classification codes | Direct and unbilled |
GET /api/v1/invoice/resources/invoice-types | Fetch the current invoice type codes | Direct and unbilled |
GET /api/v1/invoice/resources/invoice-quantity-codes | Fetch invoice unit/quantity codes | Direct and unbilled |
GET /api/v1/invoice/resources/payment_means | Fetch payment means codes. Note the underscore | Direct and unbilled |
GET /api/v1/invoice/resources/currencies | Fetch currency codes | Direct and unbilled |
GET /api/v1/invoice/resources/countries | Fetch country codes | Direct and unbilled |
GET /api/v1/invoice/resources/states | Fetch Nigerian state codes | Direct and unbilled |
GET /api/v1/invoice/resources/lgas | Fetch Nigerian local government areas | Direct and unbilled |
POST /api/v1/invoice/validate | Validate a raw NRS invoice | Pass-through-only, child-bound, unbilled |
POST /api/v1/invoice/sign | Sign an invoice or accept it for Stored processing | Gateway-controlled and billable at sign |
GET /api/v1/invoice/transmit/lookup/{irn} | Check parties for an already signed IRN | Pass-through-only, child/IRN-bound, unbilled |
POST /api/v1/invoice/transmit/{irn} | Transmit an already signed IRN | Pass-through-only, child/IRN-bound, unbilled |
GET /api/v1/invoice/download/{irn} | Download inbound encrypted invoice material | Recognized but blocked by Gateway with 503 GW_INVOICE_OPERATION_UNAVAILABLE; no NRS call is made |
PATCH /api/v1/invoice/transmit/{irn} | Acknowledge an inbound invoice | Recognized but blocked by Gateway with 503 GW_INVOICE_OPERATION_UNAVAILABLE; no NRS call is made |
Inbound download and acknowledgement are not production-ready
Gateway cannot yet prove that a requested raw inbound IRN belongs to the represented business. Both operations are therefore disabled in application code in every environment and return 503 GW_INVOICE_OPERATION_UNAVAILABLE before tenant resolution or an NRS call. Do not retry them repeatedly. Use the parent-scoped Stored invoice reads for materialized inbound invoices, or contact Duplo Support with the returned requestId. Duplo will enable these raw routes only after a Spend-backed IRN ownership check and cross-tenant tests are deployed.
Retrieve the stored represented-business identity
Successful Gateway onboarding returns gatewaySubBusinessId, the backward-compatible firsBusinessId, and the complete durable nrsIdentity object. At onboarding, the immutable Gateway identity is allocated from the selected NRS business.id, so the values initially match:
gatewaySubBusinessId == firsBusinessId == nrsIdentity.nrsBusinessIdAfter an audited identity migration, gatewaySubBusinessId stays unchanged while firsBusinessId and nrsIdentity.nrsBusinessId advance together to the current NRS mapping. Always use gatewaySubBusinessId in x-sub-business-id and represented-business paths; use the current nrsIdentity.nrsBusinessId as business_id in Pass-through invoice bodies. The identity object also contains the NRS entity ID, nullable reference, registered business name, TIN, and IRN template. Treat every identifier as an opaque string, not a UUID.
After onboarding, list the current mappings with GET /sub-businesses?page=1&limit=50, or read one with GET /sub-businesses/{gatewaySubBusinessId}. These requests use only the approved host and parent x-api-key; they need no x-sub-business-id, idempotency key, body, or NRS credentials. Both unbilled Gateway-owned reads use stored data and do not call NRS. See List represented businesses and Read a represented business back for complete requests, response contracts, and recovery guidance.
NRS authentication itself contributes only data.entity_id. Gateway then fetches the entity and selects one active business to obtain the remaining stored identity. Provider transaction IDs, status text, messages, and receipt timestamps from the raw authentication envelope are not durable business identity and are not published by onboarding or the read endpoint.
Verify the current identity directly with NRS
The onboarding and represented-business information responses are persisted snapshots, not live NRS reads. When you need to check whether NRS has since changed the entity, registered name, TIN, active state, or IRN template, authenticate the represented business's NRS portal user through this unbilled diagnostic:
curl --request POST \
--url https://acme.invoice.tryduplo.com/api/v1/utilities/authenticate \
--header 'content-type: application/json' \
--header 'x-api-key: pk_test_replace_with_your_key' \
--data '{
"email": "nrs-owner@acme.example",
"password": "replace-with-a-transient-secret"
}'A common response is:
{
"code": 200,
"data": {
"entity_id": "entity-demo"
}
}Then read the entity:
curl \
--url https://acme.invoice.tryduplo.com/api/v1/entity/entity-demo \
--header 'x-api-key: pk_test_replace_with_your_key'{
"code": 200,
"data": {
"id": "entity-demo",
"reference": "entity-reference-demo",
"businesses": [
{
"id": "nrs-business-demo",
"name": "Acme Retail Limited",
"tin": "12345678-0001",
"irn_template": "{{invoice_id}}-2A3A045D-{{YYYYMMDD}}",
"is_active": true
}
]
}
}Select only the active business with an exact normalized TIN match. Its id must match the current firsBusinessId and nrsIdentity.nrsBusinessId in the Gateway snapshot; its name, TIN, and IRN template should also match. gatewaySubBusinessId can intentionally remain the original stable routing identity after an audited migration. Remove the NRS password from memory and logs as soon as the diagnostic finishes. Do not call this authentication endpoint for every invoice.
The entity or business reference can be absent and is not an onboarding or invoice requirement. Gateway stores the bounded entity reference first, or the selected-business reference as a fallback, and returns entityReference: null when neither exists. If current NRS identity differs from the stored mapping, stop signing and use the reviewed identity-reverification workflow. Never silently overwrite your mapping or onboard a duplicate.
Recipient lookup and health
Check a receiver before building a transmission workflow:
curl \
--url https://acme.invoice.tryduplo.com/api/v1/invoice/transmit/lookup/tin/11111111-0001 \
--header 'x-api-key: pk_test_replace_with_your_key'{
"code": 200,
"data": {
"up": true,
"has_webhook_setup": true
}
}Check the NRS connection selected for the current domain and state:
curl \
--url https://acme.invoice.tryduplo.com/api/v1/invoice/transmit/self-health-check \
--header 'x-api-key: pk_test_replace_with_your_key'{
"code": 200,
"data": {
"ok": true
}
}These calls say nothing about your wallet balance in Duplo Dashboard or domain approval workflow. A Gateway host health check is separately available at GET /health and returns { "status": "ok" }.
Fetch NRS resources instead of hard-coding them
curl \
--url https://acme.invoice.tryduplo.com/api/v1/invoice/resources/tax-categories \
--header 'x-api-key: pk_test_replace_with_your_key'A resource response can resemble:
{
"code": 200,
"data": [
{
"code": "STANDARD_VAT",
"value": "Standard VAT",
"percent": "7.5"
}
]
}Treat STANDARD_VAT, 7.5, service code 6201, and quantity code HUR in these guides as snapshots used to explain shape. Fetch the current NRS catalogs, cache them for a bounded period, and refresh them when NRS validation reports an unknown or changed value.
Invoice endpoint responses
Common NRS success bodies seen by the Gateway integration are:
{ "code": 200, "data": { "ok": true } }{ "code": 201, "data": { "ok": true, "irn": "INV000001-2A3A045D-20260818" } }{ "code": 200, "data": { "ok": true } }For signed-IRN lookup, current integration evidence shows party objects such as accounting_supplier_party, accounting_customer_party, bill_party, and ship_party, each with an up flag. The exact lookup schema is not sufficiently stable to use as a generated type; preserve unknown fields and check the current NRS documentation.
Understand response ownership
Your client can encounter three response families:
| Family | Examples | Parsing rule |
|---|---|---|
| NRS success relay | A successful resource, entity, recipient lookup, or other eligible direct NRS operation | Preserve the sanitized NRS 2xx status, content type, and body; tolerate additive fields |
| Gateway application | Route/authentication failures, protected invoice rules, represented-business onboarding, Stored invoice operations, and every mapped NRS non-2xx response | For a failure with application/problem+json, branch on code and retry.strategy; retain requestId; do not expect raw NRS error text |
| Duplo Dashboard API | Signup, key settings, domain configuration | Read the Duplo Dashboard API response envelope; it can also include timestamp and path |
Use the response Content-Type before selecting a parser. A successful NRS body can use a different schema from a Gateway-owned success. A DNS, TLS, CDN, or load-balancer failure can occur before Gateway and will not have Gateway Problem Details.
Do not assume every successful response uses the same HTTP status. Log the HTTP status, sanitized response body, and Gateway-generated X-Request-ID without logging credentials, query secrets, or full invoice payloads. See the Gateway error code reference for the complete GW_* catalogue and Handle Gateway error responses for safe retry rules.
Continue with the Pass-through guide, Stored guide, or the complete official NRS reference.
How is this guide?