Nvono API NRS Access Point ← Home
https://api.nvono.com/api/v1
Integration Guide · v1

Send invoices to NRS through Nvono

Nvono is a NRS-accredited Access Point Provider. Post your invoice data to this API and Nvono validates it, signs it with NRS, and transmits it on your behalf — returning the IRN, cryptographic stamp, and QR code, with live status delivered to your webhook.

Base URL
https://api.nvono.com/api/v1 — confirm your assigned host with your Nvono contact.
Format
JSON over HTTPS. Send Content-Type: application/json.
Auth
API key in the x-api-key header.
Environments
Same base URL for both. The key you use selects the environment: nvo_test_… routes to the NRS sandbox, nvo_live_… to production. There is no separate sandbox host — just use your test key against this base URL. (Confirm your assigned host with your Nvono contact.)

There are two ways to integrate, depending on where invoices get signed:

  • Nvono signs & transmits — you send raw invoice data; Nvono handles validation, signing, and transmission. See the guide →
  • You sign, Nvono transmits — you already hold NRS-signed IRNs and only need the Access Point to transmit them. See the guide →

Authentication

Every request is authenticated with an API key sent in the x-api-key header. Generate your key in the Nvono platform under Settings → API Keys. The full key is shown only once at creation, so store it securely.

http
GET /api/v1/documents HTTP/1.1
Host: api.nvono.com
x-api-key: nvo_live_9f2c7b1a8e64d0f3c5a1b7e2

Keys carry scopes that limit what they can do. For the full send-and-transmit flow, your key needs: documents:write, documents:read, firs:sign, and firs:transmit.

Keep it secret

An API key acts on your account. Never embed it in browser or mobile code — call this API from your server. Rotate a key immediately if exposed (Settings → API Keys → Revoke).

Requests & responses

Successful responses wrap the result in a consistent envelope:

json · success
{
  "success": true,
  "data": { /* the resource */ },
  "meta": { "timestamp": "2026-07-09T09:14:36.818Z", "requestId": "1801a93b-…" }
}

Errors use the same shape with success: false. The error.code is stable and machine-readable; error.details lists field-level validation messages.

json · error
{
  "success": false,
  "error": {
    "code": "BAD_REQUEST",
    "message": "telephone must start with the character +",
    "details": [ { "message": "..." } ]
  },
  "meta": { "timestamp": "…", "path": "/api/v1/documents", "requestId": "…" }
}

Quote the requestId when contacting support. Validation is strict — unknown fields are rejected with 400, so send only the documented fields.

Idempotency

Safely retry a create after a network timeout by sending an Idempotency-Key header — any unique string you generate per logical request (a UUID works well). If Nvono has already processed that key, it returns the original response instead of creating a second document.

http
POST /api/v1/documents
x-api-key: nvo_live_…
Idempotency-Key: a1f3c9e2-7b64-4d0f-9c5a-1b7e2c8d4f60
  • Reuse the same key on a retry → you get the first response back, no duplicate.
  • Same key with a different body → 422 (the key is bound to its first payload).
  • Retry while the first request is still running → 409; wait and retry.

Keys are remembered for 24 hours and scoped to your account. Currently honoured on POST /documents.

The invoice lifecycle

An invoice moves through these stages. Nvono tracks the first four on firsStatus and the last two on transmissionStatus.

01
Create
Draft document with your line items
02
Sign
Finalize → NRS validates & signs; IRN + QR issued
03
Transmit
Access Point sends it to the buyer via NRS
04
Acknowledge
NRS confirms delivery; webhook fires

Guide: submit, sign & transmit

Use this when you want Nvono to do everything from raw invoice data.

Step 1 — Create the document

Post the invoice. See the field reference for every option.

curl
curl -X POST https://api.nvono.com/api/v1/documents \
  -H "x-api-key: nvo_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "documentType": "invoice",
    "documentNumber": "INV-2026-00042",
    "documentDate": "2026-07-09",
    "transactionType": "B2B",
    "currency": "NGN",
    "firsEnabled": true,
    "counterparty": {
      "name": "Dean & Davis Ltd",
      "tin": "01281054-0001",
      "email": "accounts@deandavis.ng",
      "phone": "+2348021234567",
      "address": "20 Rumuibekwe Road",
      "city": "Port Harcourt",
      "stateCode": "RI",
      "countryCode": "NG"
    },
    "lines": [{
      "description": "Magnesium Oxide",
      "quantity": 147.163,
      "unitPrice": 53000,
      "unit": "KGM",
      "itemType": "goods",
      "hsCode": "2519.90",
      "taxes": [{ "taxTypeCode": "VAT-STD" }]
    }]
  }'

The response returns the new document with "documentStatus": "draft" and an id (UUID). Use that id in the next steps. documentStatus is the document's own lifecycle — draftfinal (after finalize) → void — distinct from firsStatus (signing) and transmissionStatus (delivery). When listing, the query param that filters it is named status (see listing & filtering).

Three rules that prevent rejections

Phone → E.164: any phone must start with + (e.g. +2348021234567).  Classification: goods lines need an hsCode (format 0000.00); service lines need itemType:"service" and a serviceCode.  Currency: must be one Nvono has enabled for your account.

Taxes on a line

Each line needs its tax treatment. There are two ways to declare it.

1 — By tax type (recommended, used in Step 1 above). Pass a taxes array where each entry references a configured tax type by taxTypeCode (or taxTypeId). Nvono resolves the rate, whether it is inclusive or exclusive, whether it is withholding (WHT), and the NRS tax category from that record — so you only send the reference. You can send more than one (e.g. VAT + WHT):

json
"lines": [{
  "description": "Consulting",
  "quantity": 1,
  "unitPrice": 100000,
  "itemType": "service",
  "serviceCode": "83121500",
  "taxes": [
    { "taxTypeCode": "VAT-STD" },
    { "taxTypeCode": "WHT-5" }
  ]
}]

Discover the tax types available to you (codes, rates, labels) with GET /firs/resources/tax-types.

2 — Simple (VAT only, tax-exclusive). If you don't use tax types, set taxCategory and taxRate instead. The unit price is treated as net and VAT is added on top — e.g. "taxCategory": "S", "taxRate": 7.5.

How tax types resolve

The stored tax type is authoritative. A rate, isInclusive or isDeductible you send alongside a referenced tax type is ignored — the configured record wins. An unknown taxTypeCode/taxTypeId is rejected with 400.  Inclusive means the unit price already includes that tax (Nvono back-calculates the taxable base); otherwise tax is added on top. Withholding tax types are subtracted from the amount due, not added.

Line discount

Send discountRate — a percentage of the unit price. Nvono folds it into the unit price (e.g. unitPrice 1000 with discountRate 10 → net 900), so the NRS invoice shows a single net price. Totals (taxable base, VAT, line total) reflect the discount.

Step 2 — Finalize & sign

Finalizing with submitToFirs: true runs the full signing workflow automatically: validate IRN → validate invoice → sign → generate QR.

curl
curl -X POST https://api.nvono.com/api/v1/documents/{id}/finalize \
  -H "x-api-key: nvo_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "submitToFirs": true }'

On success the document carries signed firsStatus, plus irn, csid (cryptographic stamp), and qrCode.

Step 3 — Transmit

Signing does not transmit. Send the signed invoice through the Access Point:

curl
curl -X POST https://api.nvono.com/api/v1/firs/transmit/{id}/transmit \
  -H "x-api-key: nvo_live_…"

The repeated transmit is intentional — the resource group is /firs/transmit and /{id}/transmit is the action on it.

transmissionStatus becomes transmitting, then transmitted and finally acknowledged as NRS confirms. To transmit many at once, use POST /firs/transmit/bulk with { "documentIds": ["…"] }.

Step 4 — Track status

Either fetch the document, or (recommended) let Nvono call your webhook on every change.

curl
curl https://api.nvono.com/api/v1/documents/{id} \
  -H "x-api-key: nvo_live_…"

Guide: transmit an already-signed IRN

If your invoices are signed with NRS elsewhere and you only need Nvono as the transmitting Access Point, skip creation and signing entirely. Submit the IRNs directly.

curl · transmit by IRN
curl -X POST https://api.nvono.com/api/v1/firs/transmit/by-irn \
  -H "x-api-key: nvo_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{
      "irn": "INV36N5D00002-08106EF2-20260708",
      "documentType": "invoice",
      "transactionType": "B2B",
      "documentNumber": "INV-36N5D-00002",
      "supplierName": "Aluminium Smelter Co.",
      "supplierTin": "01056765-0001",
      "buyerName": "Dean & Davis Ltd",
      "buyerTin": "01281054-0001"
    }]
  }'

Each IRN must already be signed in NRS. Only irn is required per item; the rest help Nvono record the transmission. Then poll status for up to 100 IRNs at once:

curl · poll status
curl "https://api.nvono.com/api/v1/firs/transmit/by-irn/status?irns=INV36N5D00002-08106EF2-20260708,INV…" \
  -H "x-api-key: nvo_live_…"

The response returns a results array with each IRN's current transmission status.

Status webhooks

Rather than polling, register a URL and Nvono will POST to it on two kinds of event — when an invoice is signed or rejected (document.firs_status_changed) and when Access Point delivery progresses (transmission.status_changed). Configure it once:

curl · configure
curl -X PATCH https://api.nvono.com/api/v1/tenants/me/webhook \
  -H "x-api-key: nvo_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "webhookUrl": "https://yourapp.example/hooks/nvono",
    "webhookSecret": "whsec_your_random_secret",
    "webhookEnabled": true
  }'

Event: document.firs_status_changed

Fires when a document's signing state reaches validated, signed, rejected, or cancelled — so you can stop polling for the signing result.

json · firsStatus event
{
  "event": "document.firs_status_changed",
  "documentId": "4d7fcac6-f8d3-4897-aa05-6b4392df2fe2",
  "irn": "INV36N5D00002-08106EF2-20260708",
  "firsStatus": "signed",
  "timestamp": "2026-07-09T09:18:00.000Z",
  "data": {
    "documentNumber": "INV-2026-00042",
    "previousStatus": "validated",
    "newStatus": "signed",
    "reason": null
  }
}

Event: transmission.status_changed

Fires as the Access Point delivers a signed invoice (transmitting → transmitted → acknowledged, or failed).

json · transmission event
{
  "event": "transmission.status_changed",
  "irn": "INV36N5D00002-08106EF2-20260708",
  "documentId": "4d7fcac6-f8d3-4897-aa05-6b4392df2fe2",
  "transmissionStatus": "acknowledged",
  "timestamp": "2026-07-09T09:20:00.000Z",
  "data": {
    "documentNumber": "INV-2026-00042",
    "previousStatus": "transmitted",
    "newStatus": "acknowledged",
    "reason": null
  }
}
Verify the signature

When a webhookSecret is set, each delivery includes an X-Webhook-Signature: sha256=<hex> header — an HMAC-SHA256 of the raw request body using your secret. Recompute it and compare before trusting the payload. Respond 2xx quickly; non-2xx responses are retried.

Invoice fields

Body for POST /documents. Unknown fields are rejected.

Top level

FieldTypeReqNotes
documentTypeenumyesinvoice · credit_note · debit_note · bill
documentDatedateyesYYYY-MM-DD
counterpartyobjectyesCustomer (or vendor for a bill) — see below
linesarrayyesOne or more line items — see below
documentNumberstringoptAuto-generated if omitted
transactionTypeenumoptB2B (default) · B2G · B2C
currencystringoptISO 4217, default NGN; must be enabled for your account
dueDatedateoptYYYY-MM-DD
referenceDocumentNumberstringopt*Required for credit_note and debit_note — the original invoice's IRN. See Credit & debit notes.
firsEnabledbooleanoptEnable NRS processing for this document
notesstringoptFree text

counterparty

FieldReqNotes
nameyesLegal name
tinoptFormat NNNNNNNN-NNNN
emailoptValid email
phoneoptE.164, must start with +
address · cityoptStreet and city
stateCode · lgaCodeoptNRS codes (see code lists)
countryCodeoptISO 3166-1 alpha-2, default NG

lines[]

FieldTypeReqNotes
descriptionstringyesItem description
quantitynumberyes> 0
unitPricenumberyesPer-unit, excl. tax
itemTypeenumoptgoods (default) · service
hsCodestringgoodsHS code, format 0000.00
serviceCodestringserviceISIC/service code
unitstringoptUN/ECE unit code, e.g. KGM, EA, LTR
taxCategoryenumoptS standard (default) · Z zero · E exempt · O out-of-scope
taxRatenumberoptPercent, e.g. 7.5
whtRatenumberoptWithholding percent, e.g. 5
discount · discountRatenumberoptLine discount
Zero-value lines

A line whose net amount is 0 (e.g. a free/giveaway item) is automatically dropped from the NRS submission. NRS rejects an invoice if any line has a zero line-extension amount, so Nvono omits such lines when signing — they contribute nothing to totals or tax. The line still appears on your own Nvono record. If every line is zero the invoice is sent unchanged so NRS returns a meaningful error.

Credit & debit notes

A credit_note or debit_note adjusts an earlier invoice, so NRS requires it to reference that original invoice. Create it exactly like an invoice (POST /documents) but set documentType accordingly and supply referenceDocumentNumber.

referenceDocumentNumber = the original IRN

Pass the original invoice's IRN in referenceDocumentNumber (e.g. INVGJDMY00069-5C9D0ABD-20260717). Nvono builds the NRS billing_reference from it — the IRN plus the original issue date, which is read from the IRN's trailing -YYYYMMDD segment. For convenience you may instead pass the original's Nvono document number if that invoice was signed through Nvono; Nvono resolves it to the IRN. If neither resolves to a usable IRN, signing fails fast with a 400 before it reaches NRS.

json
POST /documents
{
  "documentType": "credit_note",
  "documentDate": "2026-08-04",
  "referenceDocumentNumber": "INVGJDMY00069-5C9D0ABD-20260717",
  "counterparty": { "name": "ABC Company Limited", "tin": "12345678-0001" },
  "lines": [
    { "description": "Returned goods", "quantity": 1, "unitPrice": 90000,
      "taxCategory": "S", "taxRate": 7.5 }
  ]
}

Finalize and sign the note the same way as an invoice (see Step 2). The original invoice does not need to have been signed through Nvono — a valid IRN issued elsewhere is accepted.

Listing & filtering

GET /documents is paginated and filterable. All parameters are optional query-string values; combine them freely.

ParamTypeNotes
pageinteger1-based page number. Default 1.
limitintegerItems per page. Default 20.
documentTypeenuminvoice · credit_note · debit_note · bill
statusenumFilters the response's documentStatus field (same value): draft · final · void
firsStatusstringSigning state, e.g. signed, rejected (see status values)
searchstringMatches document number or counterparty name
dateFromdateInclusive lower bound, YYYY-MM-DD
dateTodateInclusive upper bound, YYYY-MM-DD
curl · filtered list
curl "https://api.nvono.com/api/v1/documents?documentType=invoice&firsStatus=signed&dateFrom=2026-07-01&dateTo=2026-07-31&page=1&limit=50" \
  -H "x-api-key: nvo_live_…"

The response data is an array of documents and meta carries total, page, and limit for paging.

Recording payments

POST /documents/{id}/payment-status records a payment against a document and supports partial payments — call it multiple times as instalments arrive. Requires documents:write.

FieldTypeReqNotes
amountPaidnumberyesAmount paid in this event (document currency)
paymentMeansstringyesNRS payment-means code — see below. Unknown values return 400
paymentReferencestringoptBank/POS/transfer reference
notesstringoptFree text

paymentMeans is an NRS payment-means code — the UN/CEFACT code NRS records on the invoice. Common values: 10 · Cash 20 · Cheque 30 · Credit Transfer 48 · Bank Card 42 · ACH Credit 97 · Other ZZZ · Mutually Defined. Fetch the full, current catalogue from GET /firs/resources/firs-payment-means.

An unrecognised code is rejected with 400 (it is not silently coerced to “Other”), and no payment is recorded.

curl · record a payment
curl -X POST https://api.nvono.com/api/v1/documents/{id}/payment-status \
  -H "x-api-key: nvo_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "amountPaid": 250000,
    "paymentMeans": "30",
    "paymentReference": "FT26190XYZ",
    "notes": "First instalment"
  }'

Nvono updates paymentStatus to PARTIAL or PAID based on the cumulative total versus the invoice amount, and reports the paid amount to NRS. Read the ledger with GET /documents/{id}/payments.

Endpoint reference

All paths are relative to https://api.nvono.com/api/v1.

Documents

POST/documentsdocuments:writeCreate a document (invoice, credit/debit note, bill).
POST/documents/{id}/finalizedocuments:writeFinalize; with {submitToFirs:true} validates & signs automatically.
POST/documents/{id}/firs-workflowfirs:signRun the validate → sign → QR workflow on a finalized document.
POST/documents/{id}/sign-invoicefirs:signSign a validated invoice with NRS.
GET/documents/{id}documents:readFetch a document with its NRS and transmission status.
GET/documentsdocuments:readList documents (paginated, filterable).
GET/documents/{id}/pdfdocuments:readDownload the invoice PDF (raw application/pdf).
PATCH/documents/{id}documents:writeUpdate a draft document.
POST/documents/{id}/payment-statusdocuments:writeRecord a payment (supports partial payments). Body →
GET/documents/{id}/paymentsdocuments:readList the payments recorded against a document.
POST/documents/{id}/voiddocuments:writeVoid a finalized document. Optional body {"reason":"…"}.

NRS signing — granular steps

The signing flow (validate IRN → validate invoice → sign → QR) runs automatically via finalize / firs-workflow. These endpoints expose the individual steps if you need to run them separately — the two validate calls are what the firs:validate scope grants.

POST/documents/{id}/validate-irnfirs:validateValidate the document's IRN with NRS.
POST/documents/{id}/validate-invoicefirs:validateValidate the full invoice with NRS (sets firsStatus to validated/rejected).
DELETE/documents/{id}documents:deleteDelete a draft document. Finalized documents must be voided, not deleted.

Transmission — Access Point

POST/firs/transmit/{documentId}/transmitfirs:transmitTransmit one signed document.
POST/firs/transmit/bulkfirs:transmit{documentIds:[…]} — transmit many; skips unsigned/B2C/already-sent.
POST/firs/transmit/by-irnfirs:transmitTransmit externally-signed invoices by IRN.
GET/firs/transmit/by-irn/statusfirs:transmit?irns= comma-separated (max 100) — poll status.
GET/firs/transmit/lookup/{irn}firs:transmitLook up transmission status for a single IRN.
GET/firs/transmit/transmissionsfirs:transmitTransmission audit log (paginated).

Account

PATCH/tenants/me/webhookSet your status webhook URL, secret, and enable it.
GET/documents/currenciesdocuments:readCurrencies enabled for your account.

Status values

A document carries three independent status fields. Don't conflate them: documentStatus is the record's own lifecycle, firsStatus is signing, transmissionStatus is Access Point delivery.

documentStatus — record lifecycle (filter param: status)

draft final void

firsStatus — signing progress

draft pending processing validated signed rejected

transmissionStatus — Access Point delivery

not_transmitted transmitting transmitted acknowledged failed

paymentStatus

PENDING PARTIAL PAID REJECTED

Recording a payment via POST /documents/{id}/payment-status yields PENDING, PARTIAL, or PAID (based on the cumulative amount). REJECTED is an NRS-side outcome — it appears when NRS rejects a reported payment — not something the payment-status call produces directly.

Key fields on a signed document: irn (invoice reference), csid (cryptographic stamp ID), and qrCode.

Code lists & lookups

Fetch NRS reference data to populate your invoices. All of these accept any valid API key — no special scope required (except currencies). Paths are relative to https://api.nvono.com/api/v1.

GET/firs/resources/countriesCountries.
GET/firs/resources/statesStates — ?countryCode=NG. Provides stateCode.
GET/firs/resources/lgasLocal government areas — ?stateCode=LA. Provides lgaCode.
GET/firs/resources/product-codesHS codes for goods — ?search=&page=1&limit=100.
GET/firs/resources/service-codesService (ISIC) codes — ?search=&page=1&limit=100.
GET/firs/resources/quantity-codesUN/ECE Rec 20 unit-of-measure codes — valid values for a line item's unit (e.g. EA, KGM, MTR, C62).
GET/firs/resources/invoice-typesInvoice type codes (e.g. 380, 381).
GET/firs/resources/tax-categoriesTax category codes.
GET/firs/resources/tax-typesActive tax types — ?taxTypeCode=VAT|WHT|EXCISE|OTHER.
GET/firs/resources/firs-payment-meansPayment-means codes (NRS / UN/CEFACT) — values for a payment's paymentMeans.
GET/documents/currenciesdocuments:readCurrencies enabled for your account.
POST/firs/validate-tinValidate a counterparty TIN before sending.

Rate limits

Requests are rate-limited per API key. Exceeding a limit returns 429 with error.code: RATE_LIMIT_EXCEEDED — back off and retry. A general default applies to most endpoints; a few tiers are tighter or looser by nature of the operation.

TierLimitApplies to
Default60 / minMost endpoints, incl. document create/read
Read-heavy120 / minListing and lookup endpoints
NRS calls10 / minSign / transmit operations that hit NRS
Bulk5 / minBulk transmit and batch operations

Windows are fixed 60-second buckets. Bulk transmission additionally paces its internal NRS calls (default 10/min per batch) so a single bulk request will not breach the NRS tier.

Retry guidance

On a 429, wait to the next minute boundary before retrying, and prefer the bulk/by-IRN endpoints over tight loops of single calls. Standard RateLimit-* response headers are on the roadmap — until then, treat the limits above as the contract.

Error codes

HTTPerror.codeMeaning
400BAD_REQUESTMalformed body or a NRS validation failure (message says which field)
401UNAUTHORIZEDMissing/invalid API key, or bad key format (must start nvo_)
403FORBIDDENKey lacks the required scope
404NOT_FOUNDNo such document/resource for your account
409CONFLICTInvalid state transition (e.g. transmitting an unsigned invoice)
422VALIDATION_ERRORField validation failed — see details
429RATE_LIMIT_EXCEEDEDToo many requests — back off and retry
500INTERNAL_ERRORUnexpected error — retry; quote requestId to support

Scopes

Assign your key the least it needs. For the send-and-transmit flow: documents:write, documents:read, firs:sign, firs:transmit.

ScopeGrants
documents:readList/get documents, PDFs, payments, currencies
documents:writeCreate, finalize, update, void, record payments
documents:deleteDelete drafts
firs:validateValidate IRN / invoice with NRS
firs:signSign invoices, generate QR, run the NRS workflow
firs:transmitAll Access Point transmission endpoints
*Full access

Nvono API — NRS Access Point Integration Guide. Confirm your assigned base URL, sandbox host, and enabled currencies with your Nvono contact before going live. Interactive API explorer available on request.