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:

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:

Self-health response
{
  "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:

Onboarding response
{
  "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 the x-sub-business-id header on every invoice request.
  • firsBusinessId: the value you send as the invoice's business_id.
  • nrsIdentity.tin: the value you send as the invoice's accounting_supplier_party.tin.
  • nrsIdentity.irnTemplate: the template you build the invoice's irn from.

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:

IRN built from the template
{{invoice_id(e.g:INV00XXX)}}-2A3A045D-{{YYYYMMDD(e.g:20261005)}}  →  INV000001-2A3A045D-20260818

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

invoice.json
{
  "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.json

On a successful request, you will receive a response similar to the one below:

Validation success
{
  "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 at 1. 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.json

A sign succeeds when the HTTP status is 2xx and the body matches the one below. NRS may add more fields under data:

Sign success contract
{
  "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:

Transmit success
{
  "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.

How is this guide?

Last updated on

On this page