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. In Pass-through mode, copy the current firsBusinessId / nrsIdentity.nrsBusinessId value into body business_id. Continue using immutable gatewaySubBusinessId in the Gateway header. They initially match, but an audited migration can advance the NRS mapping without changing the Gateway ID.
  3. Use nrsIdentity.tin and nrsIdentity.businessName for that same represented issuer.
  4. Build the IRN from the active nrsIdentity.irnTemplate, after reverifying the persisted identity version whenever current regulator data is required.
  5. Use an issue date of today or earlier, formatted YYYY-MM-DD.
  6. Fetch NRS resource codes at runtime. The codes in the examples below are illustrative, not permanent configuration.
  7. 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 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:

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.

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.

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.

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.

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, whose valueType is either fixed or percentage.
  • An invoice-level discount, whose type is either flat or percentage. This differs from the per-line adjustment token fixed.
  • payeeData, taxRepresentativeData, paymentDetails, paymentTerms, and taxPointDate.
  • billingReferences for 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:

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?

On this page