INVOTA Platform
API Documentation
Complete REST API reference for the INVOTA e-invoicing platform. All endpoints conform to the FIRS NRS specification for e-invoice generation, signing, and transmission in Nigeria.
Overview
The INVOTA API gives you programmatic access to every part of the e-invoicing lifecycle, from creating a draft to confirming NRS acknowledgement. All requests go to the base URL above over HTTPS.
Authentication
The dashboard API uses bearer token authentication. After logging in, include the token in every subsequent request:
Authorization: Bearer {your-token}The B2B enterprise API uses API key authentication. See Section 8 for details.
Rate Limits
| API Group | Limit | Window |
|---|---|---|
| Dashboard API (all authenticated routes) | 60 requests | Per minute |
| B2B Enterprise API | 100 requests | Per minute |
When a rate limit is exceeded the API returns HTTP 429 with a Retry-After header.
Response Format
All responses are JSON. Successful responses include a data key. Error responses include a message key and, where applicable, an errors key with field-level details.
// Success
{ "data": { ... } }
// Validation error
{
"message": "Validation failed.",
"errors": {
"invoice_number": ["The invoice number field is required."]
}
}Multi-Tenancy
Every authenticated request is automatically scoped to the organisation of the authenticated user. It is not possible to access data belonging to another organisation through this API.
Invoice Status Flow
Invoices progress through the following statuses as they move through the NRS pipeline:
Any stage can transition to failed on NRS rejection. Failed invoices can be retried. They return to draft.
Authentication
All authentication endpoints are public. No token required.
/auth/registerRegisters a new organisation and creates the first user account (the owner). The user receives the finance role with is_owner = true.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| org_name | string | Required | Name of the business |
| tin | string | Required | FIRS Tax Identification Number |
| string | Required | Business email address | |
| password | string | Required | Minimum 8 characters |
| name | string | Required | Full name of the registering user |
| phone | string | Optional | Business phone number |
| address | string | Optional | Business address |
Response: 201 Created
{
"data": {
"token": "1|abcdefghijklmnopqrstuvwxyz",
"user": {
"id": "uuid", "name": "Adaeze Okonkwo",
"email": "adaeze@acme.ng", "role": "finance"
},
"organization": { "id": "uuid", "name": "Acme Nigeria Ltd" }
}
}/auth/loginAuthenticates a user and returns a bearer token.
Request Body
| Field | Type | Required |
|---|---|---|
| string | Required | |
| password | string | Required |
Response: 200 OK
{
"data": {
"token": "2|abcdefghijklmnopqrstuvwxyz",
"user": {
"id": "uuid", "name": "Adaeze Okonkwo",
"email": "adaeze@acme.ng",
"role": "finance", "is_owner": true
}
}
}/auth/logoutRevokes the current bearer token. The token becomes invalid immediately.
Response: 200 OK
{ "data": { "message": "Logged out successfully." } }Invoice Management
All routes require authentication. Write operations require an active subscription. Supplier party details are automatically derived from the authenticated organisation's taxpayer profile.
/invoicesReturns a paginated list of invoices for the authenticated organisation.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by: draft, validated, signed, transmitting, transmitted, acknowledged, reported, failed |
| search | string | Search by invoice number or buyer name |
| page | integer | Page number (default: 1) |
Response: 200 OK
{
"data": [
{
"id": "uuid",
"irn": "INV001-2741A890-20260701",
"invoice_number": "INV001",
"issue_date": "2026-07-01",
"status": "signed",
"currency_code": "NGN",
"total_amount": 150000.00,
"vat_amount": 19565.22
}
],
"meta": { "total": 42, "per_page": 15, "current_page": 1, "last_page": 3 }
}/invoicesCreates a new invoice in draft status. Supplier party details are taken from the organisation's taxpayer profile. The IRN is built and validated server-side. Per-line amounts and totals are calculated server-side.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| invoice_number | string | Required | Alphanumeric only. No special characters or spaces. Combined with the organisation's NRS Service ID and issue date to build the IRN. |
| invoice_type_code | string | Optional | NRS invoice type code. Defaults to 381 (Commercial Invoice) when left out. Other examples: 380 = Credit Note, 384 = Debit Note, 385 = Self-Billed Invoice (see Reference Data below for the full list; these are NRS's own codes, not generic UBL/PEPPOL codes). |
| invoice_kind | string | Required | B2B, B2C, or B2G. Decides which buyer details are needed (see the buyer fields below) and sets our own 24-hour reporting deadline for B2C. Also sent to NRS as invoice_kind. |
| issue_date | date | Required | YYYY-MM-DD. Used to build the IRN. |
| due_date | date | Optional | YYYY-MM-DD. NRS will not sign an invoice that has no due date, so if you leave this out we send NRS the issue_date as the due date. The invoice you saved is not changed. Set it yourself whenever the real due date is different. |
| issue_time | string | Optional | HH:MM:SS, 24-hour time |
| tax_point_date | date | Optional | YYYY-MM-DD. The date VAT becomes due, if different from issue_date. |
| actual_delivery_date | date | Optional | YYYY-MM-DD. A single delivery date. |
| delivery_period_start | date | Optional | YYYY-MM-DD. For a service covering a date range (e.g. a subscription period) rather than a single delivery date. Paired with delivery_period_end. |
| delivery_period_end | date | Optional | YYYY-MM-DD |
| document_currency_code | string | Optional | ISO 4217 currency code. Defaults to NGN. |
| tax_currency_code | string | Optional | ISO 4217 currency code. Defaults to NGN. |
| payment_means_code | string | Optional | NRS payment means code (see Reference Data above) |
| note | string | Optional | Free-text note for the whole invoice. Sent to NRS as note. Stored encrypted on our side. |
| payment_terms_note | string | Optional | For example Net 30 or Due on receipt. Sent to NRS as payment_terms_note. Stored encrypted on our side. |
| terms_conditions | string | Optional | Printed legal or boilerplate text for the invoice. Stored encrypted. This is not part of NRS's own schema, so it is never sent to NRS; it only appears on the PDF. |
| buyer_reference | string | Optional | The buyer's own purchase order or reference number. Sent to NRS as buyer_reference. |
| order_reference | string | Optional | Sent to NRS as order_reference. Many sellers send the same value as buyer_reference. |
| accounting_cost | string | Optional | An internal cost-centre or accounting code. Sent to NRS as accounting_cost. |
| billing_reference_irn | string | Optional | The IRN of a related earlier invoice, for example the original invoice a credit note refers to. Send it together with billing_reference_date. Sent to NRS as billing_reference (an array of one entry). Format: capital letters, numbers and dashes only, no spaces (see Rules NRS enforces below). |
| billing_reference_date | date | Optional | YYYY-MM-DD. The issue date of the referenced document. |
| dispatch_document_reference_irn | string | Optional | Reference of the dispatch document (for example a delivery note). Send it together with dispatch_document_reference_date. Sent to NRS as dispatch_document_reference. Same format rule as above. |
| dispatch_document_reference_date | date | Optional | YYYY-MM-DD |
| receipt_document_reference_irn | string | Optional | Reference of the receipt document. Send it together with receipt_document_reference_date. Sent to NRS as receipt_document_reference. Same format rule as above. |
| receipt_document_reference_date | date | Optional | YYYY-MM-DD |
| originator_document_reference_irn | string | Optional | Reference of the document this invoice originates from. Send it together with originator_document_reference_date. Sent to NRS as originator_document_reference. Same format rule as above. |
| originator_document_reference_date | date | Optional | YYYY-MM-DD |
| contract_document_reference_irn | string | Optional | Reference of the contract the invoice relates to. Send it together with contract_document_reference_date. Sent to NRS as contract_document_reference. Same format rule as above. |
| contract_document_reference_date | date | Optional | YYYY-MM-DD |
| additional_document_reference_irn | string | Optional | A general-purpose document reference, for anything not covered by the specific reference types above. Send it together with additional_document_reference_date. Sent to NRS as additional_document_reference (an array of one entry). Same format rule as above. |
| additional_document_reference_date | date | Optional | YYYY-MM-DD |
| invoice_adjustment_amount | decimal | Optional | One adjustment applied to the whole invoice, on top of the line totals, for example an early-payment discount or a flat processing charge. Sent to NRS as allowance_charge. Use together with invoice_adjustment_is_charge. |
| invoice_adjustment_is_charge | boolean | Required with invoice_adjustment_amount | true if the amount is a charge that adds to the total, false if it is an allowance (a discount) that takes away from it |
| buyer.party_name | string | Required if buyer is present | Buyer company or individual name |
| buyer.tin | string | Required for B2B/B2G | The buyer's TIN. For a B2B or B2G buyer you can send incorporation_number instead. A B2C buyer does not need one. |
| buyer.incorporation_number | string | Optional | The buyer's company registration number. Accepted in place of buyer.tin for B2B/B2G. When it is used instead of a TIN, NRS receives it as RN-{number}. |
| buyer.email | string | Required once a buyer is on the invoice | Not required at all for a B2C sale with no formal buyer record |
| buyer.telephone | string | Optional | |
| buyer.street_name | string | Required once a buyer is on the invoice | |
| buyer.city_name | string | Required once a buyer is on the invoice | |
| buyer.state_code | string | Optional | NRS state code (see Reference Data below). Sent to NRS as postal_address.state. Send it together with lga_code. |
| buyer.lga_code | string | Optional | NRS LGA code (see Reference Data below). Sent to NRS as postal_address.lga. |
| buyer.country_code | string | Optional | 3-letter ISO code (e.g. NGA), not the 2-letter form. Defaults to NGA. |
| buyer.postal_zone | string | Required once a buyer is on the invoice | |
| buyer.business_description | string | Optional | A short description of what the buyer's business does |
| payee.party_name | string | Required if payee is present | Only set this if payment for the invoice actually goes to a different entity than the supplier. Most invoices never need this. Sent to NRS as payee_party. If you send a payee, NRS needs its TIN and a full postal address, so the fields marked required below must all be present. |
| payee.tin | string | Required if payee is present | NRS rejects a payee with no TIN |
| payee.email | string | Optional | |
| payee.telephone | string | Optional | Sent to NRS in international format, for example +2348012345678. A Nigerian number starting 0 is converted for you. |
| payee.street_name | string | Required if payee is present | |
| payee.city_name | string | Required if payee is present | NRS rejects a party address with no city |
| payee.state_code | string | Required if payee is present | NRS state code (see Reference Data below). Sent to NRS as postal_address.state. |
| payee.lga_code | string | Required if payee is present | NRS LGA code (see Reference Data below). Sent to NRS as postal_address.lga. |
| payee.country_code | string | Optional | 3-letter ISO code (e.g. NGA), not the 2-letter form. Defaults to NGA. |
| payee.postal_zone | string | Optional | |
| bill.party_name | string | Required if bill is present | Only set this if the bill/invoice copy should go to a different contact than the buyer (for example, a separate accounts payable office). Sent to NRS as bill_party. NRS needs its TIN and a full postal address, so the fields marked required below must all be present. |
| bill.tin | string | Required if bill is present | NRS rejects a billing contact with no TIN |
| bill.email | string | Optional | |
| bill.telephone | string | Optional | Sent to NRS in international format, for example +2348012345678. A Nigerian number starting 0 is converted for you. |
| bill.street_name | string | Required if bill is present | |
| bill.city_name | string | Required if bill is present | NRS rejects a party address with no city |
| bill.state_code | string | Required if bill is present | NRS state code (see Reference Data below). Sent to NRS as postal_address.state. |
| bill.lga_code | string | Required if bill is present | NRS LGA code (see Reference Data below). Sent to NRS as postal_address.lga. |
| bill.country_code | string | Optional | 3-letter ISO code (e.g. NGA), not the 2-letter form. Defaults to NGA. |
| bill.postal_zone | string | Optional | |
| ship.party_name | string | Required if ship is present | Only set this if goods or services are actually delivered somewhere other than the buyer's own address (for example, a separate warehouse or site). Sent to NRS as ship_party. NRS needs its TIN and a full postal address, so the fields marked required below must all be present. |
| ship.tin | string | Required if ship is present | NRS rejects a delivery party with no TIN |
| ship.email | string | Optional | |
| ship.telephone | string | Optional | Sent to NRS in international format, for example +2348012345678. A Nigerian number starting 0 is converted for you. |
| ship.street_name | string | Required if ship is present | |
| ship.city_name | string | Required if ship is present | NRS rejects a party address with no city |
| ship.state_code | string | Required if ship is present | NRS state code (see Reference Data below). Sent to NRS as postal_address.state. |
| ship.lga_code | string | Required if ship is present | NRS LGA code (see Reference Data below). Sent to NRS as postal_address.lga. |
| ship.country_code | string | Optional | 3-letter ISO code (e.g. NGA), not the 2-letter form. Defaults to NGA. |
| ship.postal_zone | string | Optional | |
| lines | array | Required | At least one line item |
| lines[].item_type | string | Required | goods or services |
| lines[].item_name | string | Required | Sent to NRS as item.name |
| lines[].item_description | string | Optional | A longer description of the item. Sent to NRS as item.description. If left out, the item name is used. |
| lines[].sellers_item_identification | string | Optional | The seller's own SKU or item code |
| lines[].product_category | string | Required | Free-text category, required on every line regardless of item_type |
| lines[].hsn_code | string | Required when item_type is goods | Format: 0000.00 |
| lines[].isic_code | string | Required when item_type is services | Exactly 4 digits |
| lines[].service_category | string | Required whenever isic_code is present | |
| lines[].invoiced_quantity | decimal | Required | How many units are being invoiced |
| lines[].price_unit | string | Required | NRS unit-of-measure code (e.g. EA, DAY; see Reference Data below) |
| lines[].price_amount | decimal | Required | Price of one unit, before tax. Line amount = invoiced_quantity x price_amount. |
| lines[].tax_category_id | string | Required | NRS tax category code, e.g. STANDARD_VAT, ZERO_VAT, Exempted (see Reference Data below) |
| lines[].tax_rate_percent | decimal | Optional | If left out, the rate is looked up from tax_category_id (7.5 for STANDARD_VAT). If you send it, it must be exactly 7.5 when tax_category_id is STANDARD_VAT. |
| lines[].base_quantity | decimal | Optional | The quantity that price_amount applies to. Defaults to 1. |
| lines[].discount_rate | decimal | Optional | Percent, 0-100. The discount is taken off the line before VAT. Used to work out the discount when discount_amount is not sent. |
| lines[].discount_amount | decimal | Optional | A fixed discount for the line, taken off before VAT. If you send both this and discount_rate, this amount is used. Sent to NRS together with discount_rate. |
| lines[].charge_rate | decimal | Optional | Percent, 0-100. An extra charge on this line, separate from its own price (for example a rush or handling fee). Used to work out the charge when charge_amount is not sent. Sent to NRS as fee_rate. |
| lines[].charge_amount | decimal | Optional | A fixed extra charge for the line. If you send both this and charge_rate, this amount is used. Added to the line before VAT. Sent to NRS as fee_amount. |
Supplier details (name, TIN, email, address) are never sent in this request; they are always taken from the organisation's own taxpayer profile, set up once during onboarding. Every field in the table above that has an NRS name is sent to NRS in the signing request, so nothing you send here is dropped on the way.
Rules NRS enforces
NRS checks these when the invoice is validated and signed. The API rejects what it can straight away with a 422 and a message naming the field. Everything else comes back from NRS as the invoice's failure_reason.
| Rule | What to do |
|---|---|
| Due date | NRS will not sign an invoice with no due date. If you do not send due_date, we send NRS the issue_date as the due date. Send your real due date whenever it differs. |
| Payee, billing contact, delivery address | Each one is optional as a whole. If you send one, it needs a TIN and a full postal address: street_name, city_name, state_code and lga_code. NRS rejects it otherwise. |
| Document reference format | Every *_document_reference_irn and billing_reference_irn must be capital letters, numbers and dashes only, with no spaces. REF001-2741A890-20260601 is accepted. IRN REF001-2741A890-20260601 (with the word IRN and a space) and ref001 (lower case) are rejected. |
| Buyer on B2B and B2G invoices | Needs a name, a TIN (or an incorporation_number), and street_name, city_name and postal_zone. A B2C invoice may leave the buyer out. |
| Goods and services lines | Every line needs product_category. A goods line needs an hsn_code in the form 0000.00. A services line needs a 4-digit isic_code and a service_category. |
| Tax rate | Leave tax_rate_percent out and it is looked up from the tax category. If you send it for STANDARD_VAT it must be exactly 7.5. |
| Phone numbers | NRS wants international format (+234...). A number such as 08012345678 is converted for you. |
Example Request
{
"invoice_number": "INV001",
"invoice_type_code": "381",
"invoice_kind": "B2B",
"issue_date": "2026-07-01",
"issue_time": "14:30:00",
"due_date": "2026-07-31",
"tax_point_date": "2026-07-01",
"document_currency_code": "NGN",
"tax_currency_code": "NGN",
"payment_means_code": "30",
"payment_terms_note": "Net 30",
"buyer_reference": "PO-2026-014",
"order_reference": "PO-2026-014",
"accounting_cost": "500",
"billing_reference_irn": "REF001-2741A890-20260601",
"billing_reference_date": "2026-06-01",
"buyer": {
"party_name": "Sample Buyer Enterprises Ltd",
"tin": "98765432-0001",
"email": "accounts@samplebuyer.example",
"telephone": "08012345678",
"street_name": "10 Admiralty Way",
"city_name": "Lekki",
"state_code": "NG-LA",
"lga_code": "NG-LA-EOS",
"country_code": "NGA",
"postal_zone": "106104"
},
"bill": {
"party_name": "Sample Buyer Finance Department",
"tin": "98765432-0001",
"email": "finance@samplebuyer.example",
"street_name": "3 Bourdillon Road",
"city_name": "Ikoyi",
"state_code": "NG-LA",
"lga_code": "NG-LA-EOS",
"postal_zone": "101233"
},
"lines": [
{
"item_type": "services",
"item_name": "Invota e-invoicing platform - annual subscription",
"product_category": "Software Services",
"isic_code": "6202",
"service_category": "Computer consultancy and computer facilities management activities",
"invoiced_quantity": 1,
"price_unit": "EA",
"price_amount": 250000,
"tax_category_id": "STANDARD_VAT"
}
]
}Response: 201 Created
{
"id": "uuid",
"irn": "INV001-2741A890-20260701",
"invoice_number": "INV001",
"status": "draft",
"created_at": "2026-07-01T09:00:00+00:00"
}/invoices/{id}Returns the full details of a single invoice including all line items and party details.
/invoices/{id}Updates a draft invoice. Only allowed when status is draft. Only the account owner (is_owner = true) may edit an invoice. If the invoice number or issue date changes, the IRN is rebuilt and re-validated server-side.
Request Body
Send only the fields you want to change, using the same field names and rules as POST /invoices. To remove the payee, billing contact or delivery address, send null for it (for example "bill": null). A failed invoice that was rejected before NRS accepted it (it has no validated date) can also be edited and submitted again. Any other invoice that has left draft cannot be changed.
/invoices/{id}Soft-deletes a draft invoice. Only allowed when status is draft. Only the account owner may delete. Deleted invoices are not permanently removed from the database.
/invoices/bulkCreates up to 100 invoices in a single request, for quick imports. This dashboard bulk route accepts a shorter set of fields than a single invoice does: invoice_number, issue_date, due_date, document_currency_code, payment_means_code, note, buyer (party_name, email, telephone), and for each line item_name, item_type, invoiced_quantity, price_amount, price_unit, tax_category_id, tax_rate_percent, hsn_code, isic_code and discount_rate. For the full set of NRS fields (document references, payee, billing contact, delivery address and so on), create invoices one at a time with POST /invoices, or use the enterprise bulk route POST /b2b/invoices/bulk, which accepts the full body for every invoice.
Request Body
{ "invoices": [ { ... }, { ... } ] }Response: 207 Multi-Status
Returns a result for each submitted invoice: success or the specific validation error for that item.
Invoice Lifecycle
These endpoints move an invoice through the NRS submission pipeline. All background communication with NRS runs as queued jobs. The API returns 202 Accepted immediately and processing continues in the background.
/invoices/{id}/submitSubmits a draft invoice to NRS for validation and signing. Dispatches a background job that first validates the invoice against NRS, then signs it and generates the QR code. The invoice status moves from draft through validated to signed if both steps succeed.
If NRS rejects the invoice, its status becomes failed and the reason NRS gave is saved in failure_reason, which you can read with GET /invoices/{id}. A rejection that retrying cannot fix (for example a missing TIN or a badly formed reference) fails straight away. A temporary problem on the NRS side, such as a timeout or a "try again later" reply, is retried automatically up to three times before the invoice is marked failed. Fix the cause, then submit the invoice again.
A B2C invoice is complete once it is signed. It is registered with NRS at that point and is not transmitted to a buyer system.
Response: 202 Accepted
{ "data": { "message": "Invoice submitted. Processing in background." } }/invoice/confirm/{irn}Polls NRS to confirm the current status of an invoice by its IRN. Use this to check whether a submitted invoice has been signed or to surface any NRS-side issue.
Response: 200 OK
{ "data": { "irn": "...", "status": "signed", "nrs_message": "..." } }/invoices/{id}/transmitTransmits a signed invoice to the NRS exchange network so the buyer's system can receive it. Only available once the invoice status is signed. Runs as a background job.
Response: 202 Accepted
{ "data": { "message": "Invoice queued for transmission." } }/invoices/{id}/paymentUpdates the payment status of an invoice. When status is set to PAID or PARTIAL, the system automatically queues a VAT post-payment report to NRS and sends a receipt email to the buyer.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| payment_status | string | Required | PENDING, PARTIAL, PAID, or REJECTED |
| partial_amount_paid | decimal | Required when payment_status is PARTIAL | The amount received so far. Cleared automatically when the status moves to anything else. |
| payment_reference | string | Optional | Your own reference for the payment, for example a bank transfer reference. Up to 255 characters. |
The report to NRS only goes out once the invoice has been signed. If you mark an invoice paid before it is signed, the report is sent automatically as soon as signing completes.
/invoices/{id}/pdfStreams the invoice as a downloadable PDF. Includes the invoice header, supplier and buyer details, line items, tax totals, the IRN, and the QR code image.
Response
Content-Type: application/pdf. File is streamed as a download.
/invoices/{id}/qrReturns a time-limited pre-signed URL to the QR code image in file storage. Only available once the invoice is signed.
Response: 200 OK
{ "data": { "url": "https://storage.invota.ng/qr-codes/org-id/irn.png?token=..." } }/invoices/{id}/emailQueues an email to the buyer containing invoice details and a PDF attachment.
Response: 202 Accepted
{ "data": { "message": "Email queued for delivery." } }Customers
Customers are buyer contacts saved to the organisation's address book. They can be selected when creating an invoice to pre-fill buyer details.
/customersReturns a paginated list of customers for the authenticated organisation.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| search | string | Filter by name or email |
| page | integer | Page number (default: 1) |
/customersCreates a new customer record.
Request Body
| Field | Type | Required |
|---|---|---|
| name | string | Required |
| string | Required | |
| tin | string | Optional |
| phone | string | Optional |
| address | string | Optional |
/customers/{id}Returns the full details of a single customer.
/customers/{id}Updates a customer record. All fields are optional.
/customers/{id}Deletes a customer record.
Items
Items are saved products or services that can be selected when adding lines to an invoice. Each item stores NRS classification codes so they do not need to be entered each time.
/itemsReturns a paginated list of items for the authenticated organisation.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| search | string | Filter by name or description |
| type | string | Filter by goods or services |
| page | integer | Page number (default: 1) |
/itemsCreates a new item.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Required | |
| description | string | Optional | |
| unit_price | decimal | Required | |
| product_category | string | Required | goods or services |
| hsn_code | string | Required for goods | Format: 0000.00 |
| isic_code | string | Required for services | 4-digit ISIC code |
| tax_category_code | string | Required | NRS tax category |
/items/{id}Returns the full details of a single item.
/items/{id}Updates an item record.
/items/{id}Deletes an item record.
Reports
Reporting endpoints provide summary and export data for the authenticated organisation. Requires the auditor or finance role.
/reports/summaryReturns aggregate invoice statistics over a given date range.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| from | date (YYYY-MM-DD) | Start of date range |
| to | date (YYYY-MM-DD) | End of date range |
Response: 200 OK
{
"data": {
"total_invoices": 127,
"total_value": 4500000.00,
"total_vat": 586956.52,
"by_status": {
"draft": 5, "signed": 12,
"acknowledged": 98, "reported": 12, "failed": 0
}
}
}/reports/vatReturns VAT post-payment report records for the authenticated organisation.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| from | date | Start of period |
| to | date | End of period |
| status | string | pending, submitted, failed |
/reports/exportExports a list of invoices as a CSV file for the given date range and optional status filter.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| from | date | Start of period |
| to | date | End of period |
| status | string | Optional filter by invoice status |
| format | string | csv (default) |
Response
Content-Type: text/csv. File is streamed as a download.
B2B Enterprise API
The B2B API is for enterprise clients that connect their ERP or POS systems directly to INVOTA. It uses API key authentication instead of bearer tokens. Clients access this API only. They do not use the web dashboard.
Authentication
Include both headers on every B2B API request:
X-Api-Key: {your-api-key}
X-Api-Secret: {your-api-secret}API credentials are provisioned by a Best Teck Nigeria Limited admin. Clients can rotate their own credentials at any time using the credentials endpoint below.
/b2b/invoicesSubmits a single invoice through the B2B API. It takes exactly the same request body as POST /invoices (Section 3), with the same fields and the same rules, so every NRS field is available to you: document references, payee, billing contact, delivery address, discounts and charges, and the rest. The invoice is saved, then a background job validates and signs it with NRS. The response comes back straight away with the IRN, which you use to poll for progress.
Two differences from the dashboard route: the supplier is always your own business profile, and a B2B or B2G invoice must have a buyer with a TIN (or incorporation_number) and a full postal address. A B2C invoice may leave the buyer out. Invoices created with a sandbox key are marked sandbox and are never sent to NRS; a production key sends them for real.
Response: 202 Accepted
{
"id": "uuid",
"irn": "INV001-2741A890-20260701",
"invoice_number": "INV001",
"status": "draft",
"payable_amount": "268750.00",
"qr_url": null,
"pdf_url": null
}qr_url and pdf_url are empty at this point. The QR code is filled in once the invoice is signed; poll GET /b2b/invoices/{irn} to get it.
Errors
| Status | code | Meaning |
|---|---|---|
401 | MISSING_API_CREDENTIALS / INVALID_API_CREDENTIALS | The X-Api-Key or X-Api-Secret header is missing or wrong |
402 | SUBSCRIPTION_NOT_FOUND | No billing subscription exists for the organisation |
402 | NO_CREDITS_REMAINING / CREDIT_LIMIT_EXCEEDED | A prepaid account is out of credits, or a pay-as-you-go balance has reached its limit |
403 | KYC_REQUIRED | The business has not finished identity verification |
422 | SUPPLIER_PROFILE_INCOMPLETE | Your business profile is missing fields NRS needs. The response lists them in missing_fields. |
422 | TAXPAYER_PROFILE_MISSING | The organisation has no taxpayer profile yet |
422 | DUPLICATE_IRN | An invoice with the same number and issue date already exists |
422 | (validation) | A field is missing or wrong. The body is { message, errors } with one entry per field, for example errors.payee.tin. |
429 | (none) | More than 100 requests in a minute |
/b2b/invoices/{irn}Returns the current status of an invoice by its IRN. Poll this after submitting to track progress.
Response: 200 OK
{
"id": "uuid",
"irn": "INV001-2741A890-20260701",
"invoice_number": "INV001",
"invoice_kind": "B2B",
"status": "signed",
"payment_status": "PENDING",
"payable_amount": "268750.00",
"tax_amount": "18750.00",
"issue_date": "2026-07-01",
"due_date": "2026-07-31",
"validated_at": "2026-07-01T09:00:03+00:00",
"signed_at": "2026-07-01T09:00:06+00:00",
"transmitted_at": null,
"acknowledged_at": null,
"failure_reason": null,
"qr_url": "https://storage.invota.ng/qr-codes/...",
"pdf_url": null
}status moves through draft, validated, signed, transmitting, transmitted, acknowledged and reported, or ends at failed. When it is failed, failure_reason holds the reason NRS gave, in plain text. Fix the cause and submit a new invoice. qr_url is a temporary link that lasts 30 minutes, so request it again when you need it.
/b2b/invoices/{irn}/paymentTells us a payment status from your own system, for example when your ERP or till marks an invoice paid. Once the invoice has been signed, a PAID or PARTIAL update sends the VAT report to NRS and emails the buyer a receipt, the same as marking it paid in the dashboard.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| payment_status | string | Required | PENDING, PARTIAL, PAID, or REJECTED |
| partial_amount_paid | decimal | Required when payment_status is PARTIAL | The amount received so far |
| payment_reference | string | Optional | Your own payment reference. Up to 255 characters. |
Response: 200 OK
{
"id": "uuid",
"irn": "INV001-2741A890-20260701",
"payment_status": "PAID",
"partial_amount_paid": null,
"payment_confirmed_at": "2026-07-05T10:12:00+00:00",
"nrs_vat_report_queued": true
}/b2b/invoices/bulkSubmits up to 50 invoices in a single request. Every invoice takes the full request body, the same as a single POST /b2b/invoices. Each invoice is checked on its own: one with a mistake is reported against its position in your list and the others are still created, and the response is 207 Multi-Status. The account-level errors listed under POST /b2b/invoices (credentials, KYC, billing, supplier profile) reject the whole request, and so does a missing or empty list or more than 50 invoices.
Request Body
{ "invoices": [ { ... }, { ... } ] }Response: 207 Multi-Status
{
"created": [
{ "index": 0, "id": "uuid", "irn": "INV001-2741A890-20260701", "status": "draft" }
],
"errors": [
{
"index": 1,
"invoice_number": "INV002",
"error": "Validation failed.",
"errors": { "payee.tin": ["The payee.tin field is required when payee is present."] }
},
{ "index": 2, "irn": "INV003-2741A890-20260701", "error": "Duplicate IRN" }
],
"summary": { "total": 3, "created": 1, "failed": 2 }
}/b2b/credentialsReturns the current active credential for the authenticated B2B client (key ID and creation date). The actual key and secret values are not returned after initial provisioning.
/b2b/credentials/rotateRotates the B2B API credentials. The old key is deactivated immediately. The new key and secret are returned once only.
Response: 200 OK
{
"data": {
"api_key": "new-key-shown-once",
"api_secret": "new-secret-shown-once"
}
}api_secret is only returned in this response. It is stored encrypted and cannot be retrieved again. Rotate only when you are ready to update your system immediately.Enterprise Dashboard (Account Management)
Everything above is called by your ERP/POS system using theX-Api-Key / X-Api-Secret pair. The endpoints below are different: they use ordinary Bearer token auth (the same login as the regular dashboard) and power the /dashboard/api developer portal a human enterprise user actually logs into to manage their account: viewing usage, rotating keys, switching billing mode, buying credits, and setting a webhook URL. Your integration code should not call these; they are for the person administering the integration, not the integration itself.
/enterprise/summaryOverview stats for the enterprise dashboard home page: submission counts by outcome, the 10 most recent invoices, and the organisation's current billing state.
Response: 200 OK
{
"stats": {
"total_submitted": 812, "validated_count": 790,
"failed_count": 6, "pending_count": 16
},
"billing": {
"mode": "prepaid",
"outstanding_balance": 0,
"credit_limit_ngn": 500000,
"credits_balance": 340,
"plan_slug": "enterprise",
"per_invoice_rate_ngn": 50.00,
"currency": "NGN",
"available_packs": [
{ "id": "100", "credits": 100, "price_ngn": 5000 },
{ "id": "500", "credits": 500, "price_ngn": 22500 }
]
},
"recent_invoices": [ { "id": "uuid", "irn": "...", "status": "signed", "..." : "..." } ],
"webhook_callback_url": "https://erp.example.com/hooks/invota"
}/enterprise/credentialsReturns metadata for the active API credential (environment, created/rotated/revoked timestamps). The key and secret values are never returned here, only at the moment they are created or rotated. Credentials are issued automatically once KYC is approved; if none exist yet, credential is null.
/enterprise/credentials/rotateRotates the active credential within its current environment. The old key stops working immediately. Same one-time-reveal rule as /b2b/credentials/rotate above.
/enterprise/credentials/environmentSwitches between sandbox and production. The environment is encoded in the key prefix itself, so switching issues a brand new key pair rather than relabeling the old one: every credential in the other environment is revoked at the same time, so nothing from the previous environment keeps working afterwards.
Request Body
| Field | Type | Required |
|---|---|---|
| environment | string | Required |
/enterprise/billing/modeSwitches how invoice submissions are billed: payg (Pay As You Go, a running balance invoiced per submission) or prepaid (a credits balance that decreases per submission, topped up manually). Switching to Prepaid is blocked while an outstanding PAYG balance is unpaid.
Request Body
| Field | Type | Required |
|---|---|---|
| mode | string | Required |
/enterprise/billing/pay/initializeStarts a Paystack payment, either clearing an outstanding PAYG balance (purpose: "balance") or buying a prepaid credit pack (purpose: "credits", with a pack_id from the packs listed in GET /enterprise/summary). Returns a Paystack checkout URL to redirect the user to.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| purpose | string | Required | balance or credits |
| pack_id | string | Required if purpose is credits | Must match an available pack |
Response: 200 OK
{ "authorization_url": "https://checkout.paystack.com/...", "reference": "ent_...", "amount_ngn": 5000 }/enterprise/billing/pay/verifyVerifies a Paystack transaction reference after the user returns from checkout and applies it, clearing the balance or crediting the purchased pack onto credits_balance. Safe to call more than once for the same reference; an already-applied payment is reported back without being applied twice.
Request Body
| Field | Type | Required |
|---|---|---|
| reference | string | Required |
/enterprise/webhook-callback-urlSets the URL that receives NRS transmission events for this organisation's invoices (see Section 9). Must be https:// in production.
/enterprise/webhook/verifySends a single synthetic test event to the saved webhook URL right away, so the user can confirm it's reachable before relying on it. This is a one-off reachability check, not a real event; it is not retried and has no connection to the fire-and-forget delivery used for actual invoice events.
Response: 200 OK / 422
{ "verified": true, "reason": "ok", "status_code": 200 }Webhooks
INVOTA receives event notifications from NRS and forwards them to your registered webhook callback URL. Set the callback URL on your organisation record.
NRS Event Receiver
/webhooks/nrsThis endpoint receives event notifications from NRS. It is registered with NRS as the callback URL for all transmission events. Always returns HTTP 200 to acknowledge receipt. Duplicate events are detected using SHA-256 hash checks and ignored automatically.
Events Forwarded to Your Callback URL
| Event | Description |
|---|---|
TRANSMITTING | NRS has received the invoice on the exchange network |
TRANSMITTED | NRS confirms the invoice reached the buyer's access point |
ACKNOWLEDGED | The buyer's system has acknowledged the invoice |
FAILED | NRS has reported a failure for this invoice |
Webhook Payload
Each event is sent to your callback URL as a POST with a JSON body:
{
"event": "TRANSMITTED",
"irn": "INV001-2741A890-20260701",
"status": "transmitted",
"invoice_id": "uuid",
"timestamp": "2026-07-01T14:30:00+00:00",
"payload": { "irn": "INV001-2741A890-20260701", "message": "TRANSMITTED" }
}| Field | Description |
|---|---|
| event | TRANSMITTING, TRANSMITTED, ACKNOWLEDGED or FAILED |
| irn | The invoice's IRN |
| status | The invoice's status on our side after the event was applied |
| invoice_id | The invoice's id on our side |
| timestamp | When we forwarded the event (ISO 8601) |
| payload | The original message NRS sent us, passed through unchanged |
GET /b2b/invoices/{irn} as a fallback if your endpoint is temporarily unavailable.Reference Data
The platform caches reference data from NRS and refreshes it daily at 02:00 WAT. These lists power the dropdown menus in the invoice form and validate classification codes at submission time.
/reference/{type}Returns the cached list for the given type.
Available Types
| Type | Description |
|---|---|
invoice_types | NRS invoice type codes (e.g. 381 for commercial invoice) |
payment_means | Payment method codes |
tax_categories | VAT and tax category codes (S = Standard, Z = Zero, E = Exempt) |
currencies | ISO 4217 currency codes |
quantity_codes | Unit of measure codes for invoice lines |
hs_codes | Harmonised System codes for goods classification |
services_codes | ISIC codes for services classification |
countries | Country codes and names |
states | Nigerian state codes |
lgas | Nigerian Local Government Areas |
Response: 200 OK
{
"data": [
{ "code": "380", "name": "Credit Note", "resource_type": "invoice_types" },
{ "code": "381", "name": "Commercial Invoice", "resource_type": "invoice_types" }
]
}IRN Format
The Invoice Reference Number (IRN) is constructed by INVOTA as the System Integrator. NRS does not assign it. The platform builds and validates the IRN before saving each invoice.
Structure
{InvoiceNumber}-{ServiceID}-{YYYYMMDD}| Part | Rules | Example |
|---|---|---|
| InvoiceNumber | Alphanumeric characters only. No spaces, dashes, or special characters. | INV001 |
| ServiceID | 8-character alphanumeric NRS-assigned code for the taxpayer. Stored on the organisation record. | 2741A890 |
| YYYYMMDD | Invoice issue date. Must be a valid calendar date. | 20260701 |
Full Example
INV001-2741A890-20260701
IRN with Timestamp (used for NRS signing payload only)
The Unix timestamp is appended only in the cryptographic signing payload sent to NRS. It does not form part of the IRN printed on the invoice or stored in the database.
INV001-2741A890-20260701.1751328000
Uniqueness
No two invoices within the same organisation may share the same IRN. The platform checks for duplicates before saving. Once an IRN has been submitted to NRS and signed it cannot be reused or re-signed under a different invoice.
Error Codes
All errors follow a consistent structure. The HTTP status code indicates the general error type. The message field gives a human-readable explanation.
| HTTP Status | Meaning | Common Cause |
|---|---|---|
401 | Unauthorized | Token expired or revoked, missing Authorization header, or (B2B) a missing or wrong X-Api-Key / X-Api-Secret |
402 | Payment Required | Enterprise billing: no subscription, no prepaid credits left, or a pay-as-you-go balance at its limit. The code field says which. |
403 | Forbidden | Wrong role, editing an invoice that has left draft, a non-owner trying to edit or delete, or KYC not yet approved (code KYC_REQUIRED) |
404 | Not Found | Invalid invoice ID or IRN, cross-tenant access attempt |
422 | Unprocessable Entity | A field is missing or wrong (the errors object names each one), an invalid or duplicate IRN (code DUPLICATE_IRN), or an incomplete business profile (code SUPPLIER_PROFILE_INCOMPLETE) |
429 | Too Many Requests | More than 100 requests in a minute, on both the dashboard API and the B2B API |
500 | Internal Server Error | An unexpected error on our side. Problems reported by NRS do not appear here: they are saved on the invoice as failure_reason. |
503 | Service Unavailable | The platform is in maintenance mode |
Validation Error Response
{
"message": "The given data was invalid.",
"errors": {
"invoice_number": ["The invoice number may only contain letters and numbers."],
"lines.0.hsn_code": ["The HSN code must be in the format 0000.00."]
}
}When NRS Rejects an Invoice
NRS checks an invoice in the background, after the API has already answered 202. So a rejection does not come back as an HTTP error. The invoice's status becomes failed and the reason NRS gave is saved as failure_reason. Read it from GET /invoices/{id} or GET /b2b/invoices/{irn}. The text is NRS's own message, passed through as it was sent.
{
"irn": "INV001-2741A890-20260701",
"status": "failed",
"failure_reason": "invoicerequest.invoice.billparty.tin is required"
}KYC & Verification
Business identity verification. NRS invoice submission can be blocked for an organisation until KYC is approved, depending on the platform's current enforcement settings.
/kycReturns the authenticated organisation's overall KYC status plus the state of each individual required document.
Response: 200 OK
{
"kyc_status": "pending",
"documents": [
{
"type": "cac_certificate",
"label": "CAC Certificate of Incorporation",
"required": true,
"status": "approved",
"rejection_reason": null,
"submitted_at": "2026-07-01T09:00:00Z",
"reviewed_at": "2026-07-02T11:00:00Z"
},
{
"type": "directors_id",
"label": "Director's Government ID",
"required": true,
"status": "pending",
"rejection_reason": null,
"submitted_at": "2026-07-01T09:05:00Z",
"reviewed_at": null
}
]
}Overall Status Values
| Status | Meaning |
|---|---|
| not_submitted | No required document has been uploaded yet |
| pending | At least one required document is awaiting review |
| approved | Every required document has been approved |
| rejected | At least one required document was rejected, re-upload it |
Document Types
| Type | Label | Required |
|---|---|---|
| cac_certificate | CAC Certificate of Incorporation | Required |
| directors_id | Director's Government ID | Required |
| proof_of_address | Proof of Business Address | Required |
| tin_certificate | FIRS TIN Certificate | Optional |
/kyc/submitUploads one document for one type. Re-submitting a type that is still pending or was rejected replaces it; an already approved document cannot be overwritten by re-uploading. Send as multipart/form-data, not JSON.
Request Body (multipart/form-data)
| Field | Type | Required | Description |
|---|---|---|---|
| document_type | string | Required | One of the document types above |
| document | file | Required | PDF, JPG or PNG, max 10 MB |
Response: 201 Created
{
"message": "Document submitted for review.",
"document_type": "cac_certificate",
"status": "pending",
"submitted_at": "2026-07-01T09:00:00Z"
}Billing & Subscriptions
Subscription plan billing for ordinary business and practice accounts via Paystack. Not to be confused with Section 8's Enterprise Dashboard billing, which uses a separate Pay As You Go / Prepaid Credits model for API integrators.
/billingReturns the authenticated organisation's current subscription and payment method state.
Response: 200 OK
{
"subscription": {
"plan_slug": "growth",
"plan_name": "Growth",
"status": "active",
"billing_type": "monthly",
"monthly_fee_ngn": 15000,
"invoice_limit": 200,
"invoices_used": 47,
"cycle_start_at": "2026-07-01T00:00:00Z",
"cycle_end_at": "2026-08-01T00:00:00Z",
"next_renewal_at": "2026-08-01T00:00:00Z",
"outstanding_balance": 0,
"gateway": "paystack",
"has_payment_method": true,
"card_last4": "4081",
"card_type": "visa",
"card_expiry": "09/28",
"features": ["Up to 200 invoices/month", "Email support"],
"trial_days": 0,
"is_popular": true
}
}/billing/historyReturns a paginated list of payment events (charges, renewals, failures) for the authenticated organisation, newest first.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| per_page | integer | 5–50, default 10 |
/billing/history/exportDownloads the full payment history as a CSV file.
Response
Content-Type: text/csv. File is streamed as a download.
/billing/initializeStarts a Paystack payment to activate or renew a subscription plan. Returns a checkout URL to redirect the user to. Consent note: activating a plan this way saves the card used and authorises automatic renewal charges each billing cycle until the plan is changed or cancelled; this is disclosed to the user in the activation dialog before they are redirected.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| plan_slug | string | Required | |
| billing_type | string | Required | monthly or yearly |
Response: 200 OK
{
"authorization_url": "https://checkout.paystack.com/...",
"access_code": "...",
"reference": "inv_...",
"amount_ngn": 15000,
"plan_name": "Growth",
"billing_type": "monthly"
}/billing/verifyVerifies a Paystack reference after the user returns from checkout and activates the subscription. Safe to call more than once for the same reference.
Request Body
| Field | Type | Required |
|---|---|---|
| reference | string | Required |
Compliance & Audit Trail
Aggregate compliance reporting and the organisation's own activity trail. Distinct from Section 4's per-invoice VAT post-payment reporting: this is summary and historical view, not a write operation.
/compliance/summaryReturns a compliance score plus NRS submission and VAT reporting statistics for the authenticated organisation. The score is a weighted average: NRS acceptance rate × 0.6 + VAT submission rate × 0.4.
Response: 200 OK
{
"compliance_score": 87,
"nrs_acceptance_rate": 92.5,
"vat_submission_rate": 75.0,
"invoice_stats": { "total": 120, "transmitted": 111, "failed": 2, "pending": 7 },
"vat_stats": { "total": 8, "submitted": 6, "failed": 0, "pending": 2 },
"vat_trend": [
{ "month": "Apr", "submitted": 1, "pending": 0 },
{ "month": "May", "submitted": 2, "pending": 1 }
],
"nrs_submissions": [
{ "irn": "INV045-2741A890-20260701", "status": "acknowledged", "date": "2026-07-01", "amount": 85000.00 }
],
"breakdown": [
{ "label": "Invoices transmitted", "value": 111, "total": 120 },
{ "label": "VAT reports submitted", "value": 6, "total": 8 }
]
}/compliance/exportDownloads the full NRS submission history (IRN, status, taxable/VAT/total amounts) as a CSV file.
/audit-trailReturns the authenticated organisation's own activity trail: every tracked create/update/delete action taken within it, sourced from the platform's immutable audit log. Scoped strictly to the caller's own organisation; there is no way to see another organisation's activity through this endpoint.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| per_page | integer | 5–50, default 10 |
| search | string | Filter by actor name, action, or IP address |
| sort | string | recency (default), last_6_months, or alphabetical |
Response: 200 OK
{
"entries": [
{
"id": "225",
"timestamp": "2026-09-09T13:09:31Z",
"actor_name": "Adaeze Okonkwo",
"action": "invoice.updated",
"action_label": "Invoice Updated",
"action_tone": "neutral",
"target": "Invoice",
"ip_address": "102.89.23.4",
"status": "successful"
}
],
"meta": { "current_page": 1, "last_page": 6, "per_page": 10, "total": 59 }
}Service Level Agreement
This Service Level Agreement (SLA) outlines the terms of service between Best Teck Nigeria Limited (the Provider), operating the INVOTA e-invoicing platform, and registered taxpayers using the platform (the Client).
1. Service Description
The Provider delivers electronic invoice creation, digital signing, QR code generation, and transmission services through the NRS exchange network, accessible via the INVOTA web dashboard or the INVOTA B2B API for enterprise integrations.
2. Availability
- Target platform uptime: 99.5% per calendar month, excluding scheduled maintenance windows and NRS-side outages outside the Provider's control.
- Scheduled maintenance is communicated to affected clients in advance where practical.
3. Support and Response Times
| Severity | Acknowledged Within | Target Resolution |
|---|---|---|
| Critical (platform unavailable, transmission fully blocked) | 4 business hours | 1 business day |
| High (a specific feature broken, no safe workaround) | 1 business day | 3 business days |
| General enquiries and minor issues | 2 business days | - |
Support contact: hello@invota.ng
4. Data Security Commitments
- Sensitive data (taxpayer TIN, email, API credentials, NRS signing keys) is encrypted at rest using AES-256-CBC with a random initialisation vector per write.
- All traffic to and from the platform is encrypted in transit using TLS.
- Every create, update, and delete action is recorded in an immutable, database-enforced audit log.
- Access to client data is restricted by role-based permissions and scoped strictly to each organisation's own records.
5. Issue Resolution and Escalation
Reported issues are triaged by severity as defined in Section 3. Clients are kept informed of progress on any issue rated High priority or above until resolved.
6. Term and Review
This SLA applies for the duration of the Client's active subscription or API integration with the Provider and is reviewed periodically to reflect the platform's current capabilities.