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

Send invoices to FIRS through Nvono

Nvono is a FIRS-accredited Access Point Provider. Post your invoice data to this API and Nvono validates it, signs it with FIRS, 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 FIRS 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 FIRS-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 → FIRS validates & signs; IRN + QR issued
03
Transmit
Access Point sends it to the buyer via FIRS
04
Acknowledge
FIRS 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 FIRS 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 FIRS 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 FIRS 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 FIRS 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 FIRS. 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
referenceDocumentNumberstringoptOriginal invoice for credit/debit notes
firsEnabledbooleanoptEnable FIRS 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 · lgaCodeoptFIRS 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

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 FIRS. 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 FIRS.
GET/documents/{id}documents:readFetch a document with its FIRS 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":"…"}.

FIRS 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 FIRS.
POST/documents/{id}/validate-invoicefirs:validateValidate the full invoice with FIRS (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 FIRS 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
FIRS calls10 / minSign / transmit operations that hit FIRS
Bulk5 / minBulk transmit and batch operations

Windows are fixed 60-second buckets. Bulk transmission additionally paces its internal FIRS calls (default 10/min per batch) so a single bulk request will not breach the FIRS 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 FIRS 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 FIRS
firs:signSign invoices, generate QR, run the FIRS workflow
firs:transmitAll Access Point transmission endpoints
*Full access

Nvono API — FIRS 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.