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.

This guide walks you through signing and transmitting your first invoice in test state on a domain approved in STORED mode. In Stored mode, Duplo stores the invoice you submit, then validates, bills, and signs it in the background.

Note

If your domain is approved in PASSTHROUGH mode, follow the guide on signing and transmitting invoices in Pass-through 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 accepts 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 when you submit the invoice, and in the body when you transmit it.
  • nrsIdentity.businessName: the value you send as the invoice's supplierData.name.
  • nrsIdentity.tin: the value you send as the invoice's supplierData.tin.
  • nrsIdentity.irnTemplate: the template you build the invoice's invoiceReferenceNumber from.

Creating the buyer as a customer

A Stored invoice names its buyer by customerId instead of carrying the buyer's details. Duplo uses the customer record to fill in the buyer's details for NRS, so you create the buyer once and reuse its ID on later invoices.

To create a customer, make a POST request to the Create Invoice Customer endpoint with the buyer's details. Customers belong to your top-level business, so this request carries no x-sub-business-id.

For example:

curl --request POST \
  --url https://acme.invoice.tryduplo.com/customers \
  --header 'x-api-key: pk_test_replace_with_your_key' \
  --header 'Content-Type: application/json' \
  --data '{
    "firstName": "Example Buyer",
    "lastName": "Limited",
    "email": "ap@buyer.example",
    "phone": "+2348098765432",
    "tin": "87654321-0001",
    "country": "NG",
    "state": "FCT",
    "city": "Abuja",
    "address": "2 Buyer Avenue",
    "postcode": "900001",
    "module": "invoice"
  }'

On a successful request, you will receive a 201 response with the customer in data. Store data.id, because it is the invoice's customerId. Include the buyer's name, TIN, email, address, city, country, and postcode, because NRS validation later needs all of them.

If a customer with the same email and TIN already exists, you will receive 409 GW_CUSTOMER_ALREADY_EXISTS. List customers with GET /customers?email=ap%40buyer.example and reuse the matching id. See Create or resolve the customer for details.

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 through signing, 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 stored-invoice.json. It is the service invoice from Build invoice payloads. Set customerId to the ID you stored when you created the customer:

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

Replace the supplier details with your represented business's own. supplierData.name and supplierData.tin must match the nrsIdentity from the onboarding response, because the Gateway rejects an invoice whose supplier doesn't match the business in x-sub-business-id. Duplo calculates the totals from the items, so you don't send them. The resource codes, such as STANDARD_VAT and 6201, are illustrative. In production, fetch them from the unbilled resource endpoints.

Submitting the invoice

Submitting hands the invoice to Duplo, which stores it and then validates, bills, and signs it in the background. Your request returns as soon as the invoice is stored, so it doesn't wait on NRS.

To submit the invoice, make a POST request to the Sign or Store NRS Invoice endpoint with stored-invoice.json. Along with x-api-key and x-sub-business-id, send a new x-idempotency-key you create for this invoice, such as stored- plus the IRN, so a resent request returns the original invoice instead of creating a second one.

Signing is billed after acceptance

Duplo debits the signing fee when it signs the invoice, not when the submit request returns 202. Store the idempotency key with the invoice before you submit. If the request times out, resend the identical body with the same key. Never submit a replacement for an invoice that is still processing, because each accepted invoice can be signed and billed. See Billing and safe 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: stored-INV000001-2A3A045D-20260818' \
  --data @stored-invoice.json

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

Intake response
{
  "statusCode": 202,
  "message": "Invoice accepted for asynchronous signing",
  "data": {
    "accepted": true,
    "invoiceId": "66666666-6666-4666-8666-666666666666"
  }
}

Store invoiceId, because you use it to read and transmit the invoice. A 202 means Duplo accepted the invoice, not that NRS has validated or signed it.

Polling until processing ends

Because Duplo processes the invoice in the background, the only way to learn the result is to read the invoice. Keep reading it until status reaches one of these end states:

  • signed: NRS signed the invoice and the signing charge succeeded. You can now transmit it.
  • created, with firsStatus null: validation failed and the invoice wasn't signed. Read firsRejectionReason for the cause.
  • rejected: signing or billing failed. Read firsRejectionReason for the cause.

To read the invoice, make a GET request to the Get Stored Invoice endpoint with the invoiceId. Processing time varies, so wait longer between each read rather than polling at a fixed interval.

For example:

curl \
  --url https://acme.invoice.tryduplo.com/invoices/66666666-6666-4666-8666-666666666666 \
  --header 'x-api-key: pk_test_replace_with_your_key'

When the invoice is signed, you will receive a response containing fields similar to the ones below:

Response excerpt for a signed invoice
{
  "statusCode": 200,
  "data": {
    "id": "66666666-6666-4666-8666-666666666666",
    "gatewaySubBusinessId": "nrs-business-demo",
    "invoiceReferenceNumber": "INV000001-2A3A045D-20260818",
    "status": "signed",
    "firsStatus": "signed",
    "firsRejectionReason": null,
    "transmissionStatus": "not_sent"
  }
}

If processing ends at created or rejected, don't submit a corrected copy until you know whether the original IRN was signed. See the Stored status model.

Transmitting the signed invoice

Duplo stops after signing, so the invoice doesn't reach the buyer until you transmit it. Transmission sends the signed invoice to the buyer through NRS.

To transmit the invoice, make a POST request to the Transmit Stored Invoice endpoint with the invoiceId in the path. The body carries gatewaySubBusinessId to prove that the invoice belongs to that represented business. Transmission is unbilled.

For example:

curl --request POST \
  --url https://acme.invoice.tryduplo.com/invoices/66666666-6666-4666-8666-666666666666/transmit \
  --header 'content-type: application/json' \
  --header 'x-api-key: pk_test_replace_with_your_key' \
  --header 'x-idempotency-key: transmit-66666666-0001' \
  --data '{
    "gatewaySubBusinessId": "nrs-business-demo"
  }'

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

Transmit response
{
  "statusCode": 200,
  "message": "Invoice transmitted",
  "data": {
    "invoiceId": "66666666-6666-4666-8666-666666666666",
    "transmissionStatus": "transmitted",
    "emailedTo": []
  }
}

Note

Check transmissionStatus, not the HTTP status. If the buyer isn't connected to the NRS network, the request still returns 200, but with transmissionStatus: "not_sent", which means the invoice wasn't delivered. A timeout or 502 leaves the outcome unknown, so read the invoice and contact Duplo Support instead of transmitting again.

How is this guide?

Last updated on

On this page