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.

ConcernPass-through documentStored document
Schema ownerNRSDuplo's intake schema, which Duplo converts to the NRS document
Invoice identity fieldirninvoiceReferenceNumber
Supplier identitybusiness_id plus accounting_supplier_party.tinsupplierData.name plus supplierData.tin
BuyerNRS party objectsA customerId that already exists under your top-level business
ItemsNRS invoice_lineDuplo items
TotalsYou calculate NRS monetary totalsDuplo calculates totals from items, taxes, allowances, charges, and discount
Immediate responseNRS validation/sign responseDuplo 202 Accepted response

Shared identity rules

Whichever mode you use:

  1. Send the represented business's gatewaySubBusinessId in the x-sub-business-id header.
  2. Use that same business's NRS-verified TIN and name in the supplier fields.
  3. Build the IRN from that business's current NRS IRN template.
  4. Use an issue date of today or earlier, formatted YYYY-MM-DD.
  5. Fetch NRS resource codes at runtime. The codes in the examples below are illustrative, not permanent configuration.
  6. Use tax rates in percentage points, so 7.5 means 7.5% and not 0.075. In Pass-through, also make your own arithmetic agree: line amounts must add up to the tax subtotals and the legal_monetary_total you send.

The Gateway checks issuer ownership

For Pass-through, business_id and supplier TIN must match the server-stored NRS identity. For Stored, supplier name and TIN must match the represented business. 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:

IRN structure
INV000001-2A3A045D-20260818
SegmentIn the exampleWhere it comes from
Invoice referenceINV000001Your own reference for this invoice
Service ID2A3A045DThe represented business's NRS record, shown as Service ID in the NRS taxpayer dashboard
Issue date20260818This 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.

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.

Pass-through NRS document
{
  "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:

LevelFieldsShape
Documentallowance_charge{ "charge_indicator": true, "amount": 800.60 }, where true is a charge and false an allowance. Amounts only; there is no percentage form.
Linediscount_rate and discount_amount, fee_rate and fee_amountPaired 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.

Stored intake document
{
  "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 allowances and charges, each expressed as either a fixed amount or a percentage.
  • An invoice-level discount, expressed as either a fixed amount or a percentage.
  • payeeData, taxRepresentativeData, paymentDetails, paymentTerms, and taxPointDate.
  • billingReferences for credit and debit notes.

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:

Server-owned fields to omit
businessId
parentBusinessId
subBusinessId
gatewaySubBusinessId
domainConfigId
domain
businessState
modeVersion

Stored 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:

CodeTypeBilling reference
380Credit noteRequired
381Commercial invoiceNot required
384Debit noteRequired
385Self-billed invoiceNot enforced by Duplo; follow current NRS requirements
388Factored invoiceNot enforced by Duplo; follow current NRS requirements
389Statement of accountNot enforced by Duplo; follow current NRS requirements

For credit and debit notes, include at least one original invoice reference:

Billing reference for a credit or debit note
{
  "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:

Unbilled resource paths
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/lgas

payment_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?

Last updated on

On this page