Sign and Transmit Invoices in Pass-Through Mode
Step-by-step guide to signing your first invoice in Pass-through mode, from checking your connection and onboarding a represented business to validating, signing, and transmitting the invoice.
This guide walks you through signing and transmitting your first invoice in test state on a domain approved in PASSTHROUGH mode. In Pass-through mode, you send the NRS invoice document through Duplo and get the NRS result back synchronously.
Note
If your domain is approved in STORED mode, follow the guide on signing and
transmitting invoices in Stored mode
instead.
Prerequisites
To follow this guide, you need the following:
- A top-level business in Duplo Dashboard that is active and activated for e-invoicing. If you don't have one, follow this guide to set up your business in Duplo Dashboard.
- A test API key. You can generate your API key in Duplo Dashboard under Settings > Developer API while your business is in test state.
- A Gateway domain approved in
PASSTHROUGHmode. See our guide on submitting a domain request. - Enough test funds in your test-state NGN wallet for one signing fee. Signing is billed in test, but it draws on test funds. See Billing and safe retries.
- A business to invoice for that is active in NRS and has selected Duplo as its Access Point Provider, plus its NRS portal email and password. See the represented-business prerequisites.
The examples below use acme as the domain label and pk_test_replace_with_your_key as the API key. Replace both with your own.
If a request fails
The Gateway returns an error with a code and a retry.strategy. Decide
what to do from those two fields, never from the English text, and keep the
requestId for Duplo Support. See Handle Gateway error
responses for what each strategy means.
Checking your connection
Start by confirming that your API key, your approved domain, and the Gateway's NRS connection work together. If a later step fails, you then know the problem is in that request and not in your setup.
To check the connection, make a GET request to the Get NRS Sender Health endpoint. This call is unbilled.
For example:
curl \
--url https://acme.invoice.tryduplo.com/api/v1/invoice/transmit/self-health-check \
--header 'x-api-key: pk_test_replace_with_your_key'When you send the request, you will receive a response similar to the one below:
{
"code": 200,
"data": {
"ok": true
}
}If you receive a 401 instead, the API key is wrong for this domain or state. See Handle Gateway error responses for other failures.
Onboarding a represented business
A represented business is the business whose NRS identity appears on the invoice. The Gateway only validates and signs invoices for a business it has verified with NRS, so you onboard each business once before its first invoice.
To onboard a business, make a POST request to the Onboard Represented Business endpoint with the business's NRS portal credentials. Onboarding is unbilled.
The request needs the following headers:
x-api-key(required): your test API key.x-idempotency-key(required): a key that identifies this onboarding, so a retry after a dropped connection returns the original result instead of starting again. Duplo doesn't issue it; you create it yourself. See designing stable idempotency keys to learn how to create one.
For example:
curl --request POST \
--url https://acme.invoice.tryduplo.com/sub-businesses \
--header 'content-type: application/json' \
--header 'x-api-key: pk_test_replace_with_your_key' \
--header 'x-idempotency-key: onboard-acme-retail-0001' \
--data '{
"nrsCredentials": {
"email": "nrs-owner@acme.example",
"password": "replace-at-runtime"
}
}'This request works when the NRS entity has exactly one active business. If it has more than one, add a tin field to select one, as described in Send the onboarding request.
On a successful request, you will receive a response similar to the one below:
{
"onboardingIntentId": "33333333-3333-4333-8333-333333333333",
"gatewaySubBusinessId": "nrs-business-demo",
"firsBusinessId": "nrs-business-demo",
"nrsIdentity": {
"entityId": "entity-demo",
"entityReference": "entity-reference-demo",
"nrsBusinessId": "nrs-business-demo",
"businessName": "Acme Retail Limited",
"tin": "12345678-0001",
"irnTemplate": "{{invoice_id(e.g:INV00XXX)}}-2A3A045D-{{YYYYMMDD(e.g:20261005)}}"
},
"status": "completed",
"replayed": false,
"cryptoVersion": 0
}Store the following values from the response, because the rest of this guide uses them:
gatewaySubBusinessId: the value you send in thex-sub-business-idheader on every invoice request.firsBusinessId: the value you send as the invoice'sbusiness_id.nrsIdentity.tin: the value you send as the invoice'saccounting_supplier_party.tin.nrsIdentity.irnTemplate: the template you build the invoice'sirnfrom.
Building the invoice
Every invoice needs an Invoice Reference Number (IRN) built from the represented business's irnTemplate. NRS and the Gateway use the IRN to identify the invoice in every later request, from signing to transmission and support. To build one, replace {{invoice_id(...)}} with your own invoice reference and {{YYYYMMDD(...)}} with the issue date. The text in parentheses only shows the expected format:
{{invoice_id(e.g:INV00XXX)}}-2A3A045D-{{YYYYMMDD(e.g:20261005)}} → INV000001-2A3A045D-20260818Then save the invoice below as invoice.json. It is the service invoice from Build invoice payloads, with business_id set to the firsBusinessId from the onboarding response:
{
"business_id": "nrs-business-demo",
"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
}
}Replace the supplier details with your represented business's own, and the IRN with one built from its template. The resource codes, such as STANDARD_VAT and 6201, are illustrative. In production, fetch them from the unbilled resource endpoints instead of hard-coding them.
Validating the invoice
Validation checks the document with NRS without charging a fee, so you catch a rejected document before the billed signing step.
To validate the invoice, make a POST request to the Validate NRS Invoice endpoint with invoice.json.
For example:
curl --request POST \
--url https://acme.invoice.tryduplo.com/api/v1/invoice/validate \
--header 'content-type: application/json' \
--header 'x-api-key: pk_test_replace_with_your_key' \
--header 'x-sub-business-id: nrs-business-demo' \
--data @invoice.jsonOn a successful request, you will receive a response similar to the one below:
{
"code": 200,
"data": {
"ok": true
}
}If NRS rejects the document, correct it and validate again. Validation is never billed, so retrying after a correction is safe.
Signing the invoice
Signing is the step where NRS signs the invoice under its IRN. Only a signed IRN can be transmitted to the buyer.
To sign the invoice, make a POST request to the Sign or Store NRS Invoice endpoint with the same invoice.json. Don't change the document between validating and signing it, so you sign exactly what passed validation.
Along with x-api-key and x-sub-business-id, the request needs the following headers:
x-idempotency-key: a new key you create for this sign attempt, such as the IRN plus the attempt number, so a resent request isn't billed twice. Store it with the invoice before you send the request.x-attempt-number: the attempt number, starting at1. Keep it unchanged when you resend the same attempt.
Signing is billed
This is the only billable Gateway request. If the connection drops, resend
with the same idempotency key, attempt number, and document. Never retry a
GW_SIGN_OUTCOME_UNCONFIRMED response, even with a new key, because the
invoice may already be signed and billed. Read Billing and safe
retries before you automate retries.
For example:
curl --request POST \
--url https://acme.invoice.tryduplo.com/api/v1/invoice/sign \
--include \
--header 'content-type: application/json' \
--header 'x-api-key: pk_test_replace_with_your_key' \
--header 'x-sub-business-id: nrs-business-demo' \
--header 'x-idempotency-key: sign-INV000001-2A3A045D-20260818-1' \
--header 'x-attempt-number: 1' \
--data @invoice.jsonA sign succeeds when the HTTP status is 2xx and the body matches the one below. NRS may add more fields under data:
{
"code": 201,
"data": {
"ok": true
}
}Pass-through mode does not store the invoice in Duplo, so keep the request, the NRS response, the IRN, and the X-Request-ID response header in your own system.
Transmitting the signed invoice
Signing doesn't deliver the invoice. Transmission sends the signed invoice to the buyer through NRS.
To transmit the invoice, make a POST request to the Transmit Signed Invoice endpoint with the IRN in the path and an empty JSON object as the body. Transmission is unbilled.
For example:
curl --request POST \
--url https://acme.invoice.tryduplo.com/api/v1/invoice/transmit/INV000001-2A3A045D-20260818 \
--header 'content-type: application/json' \
--header 'x-api-key: pk_test_replace_with_your_key' \
--header 'x-sub-business-id: nrs-business-demo' \
--data '{}'On a successful request, you will receive a response similar to the one below:
{
"code": 200,
"data": {
"ok": true
}
}NRS can also return code: 201. Either way, success requires data.ok: true.
Note
Transmission is unbilled, but it still changes state at NRS. If you receive
GW_TRANSMIT_OUTCOME_UNCONFIRMED, don't transmit again. Keep the returned
operationId and contact Duplo Support.
Related guides
How is this guide?
Last updated on
Onboard Represented Businesses
Verify each NRS-registered issuer, create its Gateway mapping, and optionally store the material used for invoice QR codes.
Sign and Transmit Invoices in Stored Mode
Step-by-step guide to signing your first invoice in Stored mode, from checking your connection and onboarding a represented business to submitting, polling, and transmitting the invoice.