Build Invoice Payloads
Prepare identity-bound Pass-through and Stored invoice documents with current NRS resource values.
The Gateway accepts two invoice document shapes. Your domain's approved mode, either PASSTHROUGH or STORED, determines which one POST /api/v1/invoice/sign expects. Duplo controls that mode, so you cannot choose a shape per request.
| Concern | Pass-through document | Stored document |
|---|---|---|
| Schema owner | NRS | Duplo's intake schema, which Duplo converts to the NRS document |
| Invoice identity field | irn | invoiceReferenceNumber |
| Supplier identity | business_id plus accounting_supplier_party.tin | supplierData.name plus supplierData.tin |
| Buyer | NRS party objects | A customerId that already exists under your top-level business |
| Items | NRS invoice_line | Duplo items |
| Totals | You calculate NRS monetary totals | Duplo calculates totals from items, taxes, allowances, charges, and discount |
| Immediate response | NRS validation/sign response | Duplo 202 Accepted response |
Shared identity rules
Whichever mode you use:
- Send the represented business's
gatewaySubBusinessIdin thex-sub-business-idheader. - In Pass-through mode, copy the current
firsBusinessId/nrsIdentity.nrsBusinessIdvalue into bodybusiness_id. Continue using immutablegatewaySubBusinessIdin the Gateway header. They initially match, but an audited migration can advance the NRS mapping without changing the Gateway ID. - Use
nrsIdentity.tinandnrsIdentity.businessNamefor that same represented issuer. - Build the IRN from the active
nrsIdentity.irnTemplate, after reverifying the persisted identity version whenever current regulator data is required. - Use an issue date of today or earlier, formatted
YYYY-MM-DD. - Fetch NRS resource codes at runtime. The codes in the examples below are illustrative, not permanent configuration.
- Use tax rates in percentage points, so
7.5means 7.5% and not0.075. In Pass-through, also make your own arithmetic agree: line amounts must add up to the tax subtotals and thelegal_monetary_totalyou send.
The Gateway checks issuer ownership
For Pass-through signing, body business_id must exactly equal the stored firsBusinessId / nrsIdentity.nrsBusinessId, without leading or trailing whitespace, and supplier TIN must match that same server-stored NRS identity after outer whitespace is trimmed and case is normalized. Backslashes and control characters are rejected in both identity values. For Stored, supplier TIN is compared after trimming and case normalization; supplier name is compared after trimming, collapsing repeated whitespace, and case normalization. A mismatch returns 400 before billing or NRS signing.
IRN handling
An Invoice Reference Number is the durable identity used across validation, signing, lookup, transmission, billing, and support. Build it from the represented business's current NRS template, which joins three segments with hyphens:
INV000001-2A3A045D-20260818| Segment | In the example | Where it comes from |
|---|---|---|
| Invoice reference | INV000001 | Your own reference for this invoice |
| Service ID | 2A3A045D | The represented business's NRS record, shown as Service ID in the NRS taxpayer dashboard |
| Issue date | 20260818 | This invoice's issue date, as YYYYMMDD |
Do not invent the Service ID, reuse another represented business's template, or change an IRN after a sign attempt. Persist the IRN before the first request.
For Pass-through signing, irn must begin with a letter or number. The remaining characters can be letters, numbers, ., _, :, or -; whitespace and other punctuation are rejected by Gateway before billing.
Pass-through NRS document
This is a compact B2B service-invoice example. Replace all identity and resource values with current values for your represented business and transaction.
{
"business_id": "5be71d46-045d-4098-a466-4708afe7edac",
"irn": "INV000001-2A3A045D-20260818",
"issue_date": "2026-08-18",
"issue_time": "10:30:00",
"invoice_type_code": "381",
"invoice_kind": "B2B",
"payment_status": "PENDING",
"document_currency_code": "NGN",
"tax_currency_code": "NGN",
"accounting_cost": "1000",
"accounting_supplier_party": {
"party_name": "Acme Retail Limited",
"tin": "12345678-0001",
"email": "invoice@acme.example",
"telephone": "+2348012345678",
"postal_address": {
"street_name": "1 Integration Road",
"city_name": "Lagos",
"state": "Lagos",
"country": "NG",
"postal_zone": "100001"
}
},
"accounting_customer_party": {
"party_name": "Example Buyer Limited",
"tin": "87654321-0001",
"email": "ap@buyer.example",
"telephone": "+2348098765432",
"postal_address": {
"street_name": "2 Buyer Avenue",
"city_name": "Abuja",
"state": "FCT",
"country": "NG",
"postal_zone": "900001"
}
},
"bill_party": {
"party_name": "Example Buyer Limited",
"tin": "87654321-0001",
"email": "ap@buyer.example",
"telephone": "+2348098765432",
"postal_address": {
"street_name": "2 Buyer Avenue",
"city_name": "Abuja",
"state": "FCT",
"country": "NG",
"postal_zone": "900001"
}
},
"ship_party": {
"party_name": "Example Buyer Limited",
"tin": "87654321-0001",
"email": "ap@buyer.example",
"telephone": "+2348098765432",
"postal_address": {
"street_name": "2 Buyer Avenue",
"city_name": "Abuja",
"state": "FCT",
"country": "NG",
"postal_zone": "900001"
}
},
"invoice_line": [
{
"isic_code": "6201",
"service_category": "Computer programming activities",
"invoiced_quantity": 1,
"line_extension_amount": 1000,
"item": {
"name": "Integration service",
"description": "Integration service"
},
"price": {
"price_amount": 1000,
"base_quantity": 1,
"price_unit": "HUR"
}
}
],
"tax_total": [
{
"tax_amount": 75,
"tax_subtotal": [
{
"taxable_amount": 1000,
"tax_amount": 75,
"tax_category": {
"id": "STANDARD_VAT",
"percent": 7.5
}
}
]
}
],
"legal_monetary_total": {
"line_extension_amount": 1000,
"tax_exclusive_amount": 1000,
"tax_inclusive_amount": 1075,
"payable_amount": 1075
}
}The NRS document can include additional UBL (Universal Business Language) fields such as due date, payment means, payee, tax representative, allowances, charges, and billing references. Consult the NRS versioned reference for the complete current contract.
NRS expresses reductions and additions differently at each level, and neither uses a type discriminator:
| Level | Fields | Shape |
|---|---|---|
| Document | allowance_charge | { "charge_indicator": true, "amount": 800.60 }, where true is a charge and false an allowance. Amounts only; there is no percentage form. |
| Line | discount_rate and discount_amount, fee_rate and fee_amount | Paired fields, so you send the rate or the amount rather than declaring which kind it is. |
Stored intake document
Stored mode accepts Duplo's own invoice input and derives the routing values itself. It requires an invoice customer that already exists under your top-level business in Duplo Dashboard, in the same test/live state as the request.
The selected customer must include the invoice module and invoice metadata with postcode (the legacy postalZone name is also accepted). The supplier name and tin must resolve to the same represented business selected by x-sub-business-id; they are not free-form issuer fields.
{
"invoiceNumber": "INV-0001",
"invoiceReferenceNumber": "INV000001-2A3A045D-20260818",
"invoiceType": "381",
"issueDate": "2026-08-18",
"documentCurrency": "NGN",
"taxCurrency": "NGN",
"paymentStatus": "pending",
"supplierData": {
"name": "Acme Retail Limited",
"email": "invoice@acme.example",
"phoneNumber": "+2348012345678",
"tin": "12345678-0001",
"country": "NG",
"state": "Lagos",
"city": "Lagos",
"address": "1 Integration Road",
"postcode": "100001"
},
"customerId": "55555555-5555-4555-8555-555555555555",
"items": [
{
"description": "Integration service",
"unit": "Hour",
"quantity": 1,
"unitPrice": 1000,
"taxes": [
{
"name": "STANDARD_VAT",
"rate": 7.5
}
],
"serviceCategory": "Computer programming activities",
"isicCode": "6201"
}
],
"notes": "Payment due in 30 days"
}Each line is either a product line or a service line. A product line carries productCategory and hsnCode; a service line carries serviceCategory and isicCode, as in the example above. Send one pair per line and never mix the two, so a line with productCategory and isicCode is not valid. HSN codes currently take the shape NNNN.NN, such as 8471.30.
A Stored invoice can also include:
- Per-line
allowancesandcharges, whosevalueTypeis eitherfixedorpercentage. - An invoice-level
discount, whosetypeis eitherflatorpercentage. This differs from the per-line adjustment tokenfixed. payeeData,taxRepresentativeData,paymentDetails,paymentTerms, andtaxPointDate.billingReferencesfor credit and debit notes.
For both line adjustments and the invoice-level discount, a percentage value is a decimal fraction: 0.1 means 10%.
Duplo derives the following fields from the approved host, the API key, and the x-sub-business-id header. Stored requests must not send them:
businessId
parentBusinessId
subBusinessId
gatewaySubBusinessId
domainConfigId
domain
businessState
modeVersionStored intake rejects a request that sends any of them rather than ignoring the extra fields, so strip them before you forward an object straight from your own database.
Invoice types and original-invoice references
Stored intake recognizes these NRS invoice type codes in invoiceType. Pass-through carries the same codes in invoice_type_code, where NRS owns the rules. NRS publishes more codes than Duplo's Stored intake accepts, so read the current list from GET /api/v1/invoice/resources/invoice-types before you rely on one that is not in this table:
| Code | Type | Billing reference |
|---|---|---|
380 | Credit note | Required |
381 | Commercial invoice | Not required |
384 | Debit note | Required |
385 | Self-billed invoice | Not enforced by Duplo; follow current NRS requirements |
388 | Factored invoice | Not enforced by Duplo; follow current NRS requirements |
389 | Statement of account | Not enforced by Duplo; follow current NRS requirements |
For credit and debit notes, include at least one original invoice reference:
{
"billingReferences": [
{
"irn": "ORIGINAL1-2A3A045D-20260801",
"issueDate": "2026-08-01"
}
]
}Fetch resources at runtime
Resource catalogs change independently of your application deployment, so read them at runtime rather than hard-coding values. Cache each response with an expiry rather than indefinitely, and refresh when a validation failure points at one of these codes. These reads are unbilled:
GET /api/v1/invoice/resources/tax-categories
GET /api/v1/invoice/resources/vat-exemptions
GET /api/v1/invoice/resources/hs-codes
GET /api/v1/invoice/resources/services-codes
GET /api/v1/invoice/resources/invoice-types
GET /api/v1/invoice/resources/invoice-quantity-codes
GET /api/v1/invoice/resources/payment_means
GET /api/v1/invoice/resources/currencies
GET /api/v1/invoice/resources/countries
GET /api/v1/invoice/resources/states
GET /api/v1/invoice/resources/lgaspayment_means is the one path that uses an underscore rather than a hyphen.
See NRS API context for request headers and response examples.
How is this guide?