Issuing an E-Invoice

Create a customer, reserve an invoice reference number, then create, sign, and transmit an invoice.

This guide walks one invoice from nothing to delivered. It assumes you already have an API key and can authenticate; if not, start with Getting Started.

Your business also needs its NRS setup completed in Duplo Dashboard. Until it is, every endpoint on this page returns 403 with NRS/e-invoicing is not set up for this business, whatever the request looks like.

Building a gateway for other businesses?

This guide is for a business invoicing under its own TIN. If you invoice on behalf of businesses you represent, you want the NRS Invoice Gateway instead, which gives you a dedicated domain and a system integrator relationship.

The invoice lifecycle

An invoice moves through four stages. Each stage is its own endpoint, so you can inspect the invoice between steps.

Validate and sign are separately callable, but most integrations use the submit endpoint, which runs both in one request. The walkthrough below uses submit.

Creating a customer

An invoice references a customer by customerId rather than carrying their details inline, so create the customer first and reuse the returned ID. Customers are stored per business and per mode. Thus, customers created while your business is in test mode will not work in live mode.

To store a customer, make a request to the Create a customer endpoint. Every field is required unless the samples mark it otherwise:

const axios = require("axios");

const createCustomer = async () => {
  const response = await axios.post(
    "https://dashboard.tryduplo.com/api/e-invoicing/customers",
    {
      firstName: "Example Buyer",
      lastName: "Limited",
      email: "ap@buyer.example",
      phone: "+2348098765432",
      tin: "87654321-0001", // optional, but NRS needs it
      country: "NG",
      state: "FCT", // optional
      city: "Abuja", // optional
      address: "2 Buyer Avenue",
      postcode: "900001",
      module: "invoice",
    },
    {
      headers: {
        Authorization: "Bearer <your-api-key>",
        "Content-Type": "application/json",
      },
    },
  );

  console.log(response.data);
};

createCustomer();

In your payload, you have to set module to invoice so the customer is usable for e-invoicing. firstName and lastName are joined into the customer name that appears on the invoice. country is an ISO 3166-1 alpha-2 code, the equivalent for Nigeria is NG.

The address and postcode are mandatory, and address is limited to 50 characters. A customer without them is rejected at creation.

When you are creating a customer, the tin is optional. However, NRS requires a tin for a customer during the validation stage, and a customer created without one will cause the invoice to fail.

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

JSON
{
  "statusCode": 201,
  "timestamp": "2024-01-23T09:58:11.204Z",
  "path": "/api/e-invoicing/customers",
  "message": "Customer added successfully",
  "data": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "businessId": "f3f3c6a1-45e3-44aa-b05a-1fbe98d52d09",
    "reference": "cust_invoice_A1B2C3D4E5F6",
    "modules": ["invoice"],
    "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",
    "createdAt": "2024-01-23T09:58:11.187Z",
    "updatedAt": "2024-01-23T09:58:11.187Z"
  }
}

Keep the id from the response: that is the customerId you send on every invoice for this customer.

Creating the same customer twice

Sending the same customer to the create endpoint again does not give you a second customer. If both the email and the tin match a customer you already created, the request fails with a 409. If the email matches but the tin is different, Duplo updates the existing record instead of creating a new one. Neither case returns a new id, so store the id from the first response and reuse it rather than creating the customer again.

Reserving an Invoice Reference Number (IRN)

With the customer stored, the next thing the invoice needs is its reference number. Every invoice needs an IRN, and you get one from Duplo rather than composing it yourself. To get your IRN, make a request to the Generate an IRN endpoint as shown below:

curl --request GET \
  --url https://dashboard.tryduplo.com/api/e-invoicing/invoices/generate-irn \
  --header 'Authorization: Bearer <your-api-key>'

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

JSON
{
  "statusCode": 200,
  "timestamp": "2024-01-23T10:15:04.512Z",
  "path": "/api/e-invoicing/invoices/generate-irn",
  "message": "Invoice Reference Number generated successfully",
  "data": {
    "invoiceReferenceNumber": "DUP00000001-2A3A045D-20240123",
    "systemInvoiceNumber": "DUP00000001"
  }
}

Use invoiceReferenceNumber from the response above as the invoiceReferenceNumber on the invoice you create next. It is also the reference in the path of every later call for that invoice.

Reserve a new IRN for every invoice. The systemInvoiceNumber inside it can only be used once, so reusing an IRN returns 400 System invoice number DUP00000001 already exists.

Creating the invoice

You now have the two values the invoice document depends on: the customerId you stored, and the IRN you just reserved. Everything else describes the sale itself.

This is the largest payload in the flow, so the sample below stays at the minimum NRS accepts: one line item, one tax, and the supplier's own details. Post it to the Create an invoice endpoint. As before, anything without a comment is required:

const axios = require("axios");

const createInvoice = async () => {
  const response = await axios.post(
    "https://dashboard.tryduplo.com/api/e-invoicing/invoices",
    {
      invoiceNumber: "INV-2024-001", // optional
      invoiceReferenceNumber: "DUP00000001-2A3A045D-20240123",
      invoiceType: "381",
      issueDate: "2024-01-23",
      documentCurrency: "NGN",
      taxCurrency: "NGN",
      customerId: "123e4567-e89b-12d3-a456-426614174000",
      supplierData: {
        name: "Acme Limited",
        email: "billing@acme.example",
        phoneNumber: "+2348012345678",
        tin: "12345678-0001",
        country: "NG",
        state: "Lagos", // optional
        city: "Ikeja", // optional
        address: "1 Seller Street",
        postcode: "100001",
      },
      items: [
        {
          description: "Annual platform subscription",
          unit: "Months",
          quantity: 12, // optional, defaults to 1
          unitPrice: 25000,
          serviceCategory: "Information technology services",
          isicCode: "M-6201",
          taxes: [{ name: "Standard Value-Added Tax", rate: 0.075 }], // optional
        },
      ],
    },
    {
      headers: {
        Authorization: "Bearer <your-api-key>",
        "Content-Type": "application/json",
      },
    },
  );

  console.log(response.data);
};

createInvoice();

supplierData describes your own business as it appears on the invoice, and its country takes the same ISO 3166-1 alpha-2 code the customer does, so Nigeria is NG here too.

invoiceType is a numeric document code rather than a word. 381 is a commercial invoice, which is what a normal sale needs. 380 and 384 are credit and debit notes, and both require billingReferences pointing at the original invoice. The invoice type reference lists all six.

issueDate cannot be in the future. Today and any past date are accepted, and tomorrow is rejected with an Issue date cannot be in the future error.

Every item needs a category, and there are two ways to give it one: productCategory with an hsnCode, or serviceCategory with an isicCode. They are mutually exclusive, and an item carrying neither is rejected. An hsnCode is exactly four digits, a dot, then two digits, as in 2201.10, while an isicCode carries its sector letter, as in M-6201.

unit comes from a fixed list: Hour, Day, Weeks, Months, Pieces, Litres, Kilograms, Grams, Pounds, and Ounces. quantity defaults to 1 if you leave it out, and taxes is optional for a line that carries none.

saveAsDraft decides the status the invoice starts in. Omit it and the invoice is created, set it to true and the invoice is draft, and either way it stays fully editable until you validate.

Two rules that reject invoices most often

Item description accepts a narrow character set: letters, digits, spaces, &, ", [, ], and -, with no double spaces. Commas and full stops are rejected, so write Annual platform subscription rather than Subscription, annual.

Tax rate is a decimal, so 7.5% VAT is 0.075. Sending 7.5 computes a tax total a hundred times larger than you meant.

On a successful request, you should receive a response similar to the one below, abbreviated here because the full record echoes every field you sent along with the customer:

JSON
{
  "statusCode": 201,
  "timestamp": "2024-01-23T10:22:47.980Z",
  "path": "/api/e-invoicing/invoices",
  "message": "Invoice created successfully",
  "data": {
    "id": "9c1f3b2a-77e4-4c1d-9b6a-2f5d8e0a4c73",
    "businessId": "f3f3c6a1-45e3-44aa-b05a-1fbe98d52d09",
    "businessState": "TEST",
    "invoiceNumber": "INV-2024-001",
    "systemInvoiceNumber": "DUP00000001",
    "invoiceReferenceNumber": "DUP00000001-2A3A045D-20240123",
    "invoiceType": "381",
    "issueDate": "2024-01-23",
    "documentCurrency": "NGN",
    "taxCurrency": "NGN",
    "customerId": "123e4567-e89b-12d3-a456-426614174000",
    "subtotalAmount": 300000,
    "taxTotal": 22500,
    "allowanceTotal": 0,
    "chargeTotal": 0,
    "totalAmount": 322500,
    "amountDue": 322500,
    "status": "created",
    "firsStatus": null,
    "direction": "outbound",
    "transmissionStatus": "not_sent",
    "paymentStatus": "pending",
    "createdAt": "2024-01-23T10:22:47.902Z",
    "updatedAt": "2024-01-23T10:22:47.902Z"
  }
}

You do not compute totals. Duplo derives subtotalAmount, taxTotal, totalAmount, and amountDue from the line items, per-line allowances and charges, and any invoice-level discount.

Submitting for validation and signing

The invoice exists in Duplo, but NRS has not seen it yet, and it carries no signature or QR code until it does. The next step is to validate and sign the invoice, which are two separate stages of the lifecycle. However, the submit endpoint does both in a single call, and that is what this guide uses.

Make a request to the Submit an invoice endpoint with the IRN in the path, and an idempotency key so a network retry cannot bill you twice:

curl --request POST \
  --url https://dashboard.tryduplo.com/api/e-invoicing/invoices/DUP00000001-2A3A045D-20240123/submit \
  --header 'Authorization: Bearer <your-api-key>' \
  --header 'x-idempotency-key: submit-DUP00000001-2A3A045D-20240123' \
  --header 'x-idempotency-request-hash: 9b2c1f0a7d3e4c5b6a8f0d1e2c3b4a5968770e1d2c3b4a5f6e7d8c9b0a1f2e3d'

On a successful request, you should receive a response similar to the one below, shown here as the data object only:

JSON
{
  "invoiceId": "9c1f3b2a-77e4-4c1d-9b6a-2f5d8e0a4c73",
  "firsStatus": "signed",
  "status": "signed",
  "processedSynchronously": true
}

Submit tries to complete both calls synchronously inside a 30 second window. A 200 carries processedSynchronously, which tells you whether it managed it:

  • processedSynchronously: true means validate and sign both completed inside the window. The invoice is signed, its QR code is attached, and there is nothing further to do.
  • processedSynchronously: false means NRS was slow or briefly unavailable, so the invoice moved to submitted with firsStatus: pending and Duplo's background processor took it over. The invoice is not signed yet, and what you do next is covered in When a submit is handed off.

A 400 is the third outcome, and it carries no such field. NRS rejected the invoice outright and the message carries the reason it gave. That is terminal for this attempt: the invoice stays at created so you can correct the document and submit again, and retrying the same document unchanged will fail the same way.

When a submit is handed off (processedSynchronously: false)

There is no completion callback, so poll Get invoice until firsStatus leaves pending.

The processor retries on its own schedule, waiting 30 seconds, then 2 minutes, 10 minutes, 30 minutes, and an hour between attempts. Poll on roughly that shape rather than in a tight loop: most invoices settle on the first or second attempt, and a checking interval of a minute or so is enough.

After five failed attempts it gives up. A failure during validation returns the invoice to unsubmitted, a failure during signing leaves it rejected, any bundle quota consumed is refunded, and the supplier address on the invoice gets an email explaining what happened. Worst case, that is about 45 minutes from submit to a settled answer.

Signing is billable and irreversible

Signing is charged per invoice, and how depends on your plan. On pay-as-you-go it debits your NGN business wallet before the NRS call, so an underfunded wallet fails the submit with 400 PAYG Wallet debit failed, followed by the reason. On a bundle it draws down your invoice quota, and on an annual plan it is already covered. Send an idempotency key on every submit and sign so a network retry cannot bill you twice.

A signed invoice can no longer be edited: the update endpoint only accepts a draft or created invoice. To correct a signed invoice, raise a credit note against it. An invoice that NRS moved to rejected is a different case, handled by the resubmit endpoint under a new IRN.

Retrying safely

Submit and sign both accept an idempotency key, and they are the only two endpoints that do, because they are the only billable ones. Sending one takes two headers:

HeaderValue
x-idempotency-keyYour own key, 8 to 128 characters of letters, digits, ., _, :, or -
x-idempotency-request-hashSHA-256 of the request, as 64 lowercase hex characters. Required whenever you send a key

Repeat a call with the same key and the same hash and you get the first call's result replayed, with no second charge. Two mistakes are rejected rather than replayed:

  • Reusing the key with a different hash returns 409 Idempotency key was already used for a different request.
  • Reusing the key on a different invoice returns 409 Idempotency key was already used for a different invoice, because keys are unique across your business, not per invoice.

A key is claimed only once the call gets as far as billing. If the wallet debit itself fails, the key is released, so you can top up and retry with the same key. Deriving the key from your own invoice identifier, rather than a fresh random value per attempt, is what makes a retry after a timeout safe.

Transmitting to the recipient

Signing makes an invoice valid. Transmitting is what delivers it, and it is a separate, explicit call, so a signed invoice sits with you until you make it.

Make a request to the Transmit an invoice endpoint with the same IRN once the invoice is signed:

curl --request POST \
  --url https://dashboard.tryduplo.com/api/e-invoicing/invoices/DUP00000001-2A3A045D-20240123/transmit \
  --header 'Authorization: Bearer <your-api-key>' \
  --header 'Content-Type: application/json' \
  --data '{ "firsTransmit": true }'

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

JSON
{
  "statusCode": 200,
  "timestamp": "2024-01-23T10:41:02.317Z",
  "path": "/api/e-invoicing/invoices/DUP00000001-2A3A045D-20240123/transmit",
  "message": "Invoice transmitted",
  "data": {
    "invoiceId": "9c1f3b2a-77e4-4c1d-9b6a-2f5d8e0a4c73",
    "transmissionStatus": "transmitted",
    "emailedTo": []
  }
}

firsTransmit defaults to true, which delivers through NRS to a recipient that is connected to it. If your recipient is not on NRS, set firsTransmit to false and pass at least one address in emails to send an email copy instead. Leaving firsTransmit on and supplying emails does both.

To find out which applies before you call, use recipient lookup, which reports whether the recipient's TIN is NRS-connected.

After a successful transmit, transmissionStatus moves from not_sent to transmitted and status becomes sent. Both are safe to re-enter, so resending does not break the invoice.

Next steps

That is one invoice from nothing to delivered. What happens afterwards, reading the invoice back, filtering your invoices, correcting one before it is signed, recording payment, and acting on invoices other businesses send you, is covered in Manage Issued Invoices.

How is this guide?

Last updated on

On this page