Pronalytics Canonical Invoice Schema v1 · demo
Contents
  1. 1What you send vs. what we mint
  2. 2Root invoice record
  3. 3Party — buyer & other parties
  4. 4Line items
  5. 5The VAT model (per-line)
  6. 6Tax categories — 27 tags, 6 drivers
  7. 7Document-reference family
  8. 8Tax totals & monetary totals
  9. 9Payment status, means & charges
  10. 10Enumerations
  11. 11Validation rules
  12. 12Worked examples — Grollo & Mollo

Canonical model · STRICT ingress body

Canonical Invoice Schema

The exact JSON shape an ERP sends, one record at a time, in the invoice_data_json array of POST /api/ingest. Map your data to these fields on your side and call us. There is no per-caller transformation on the canonical leg — you send this schema, unchanged.

You send the invoice content. Pronalytics supplies the seller party and mints the IRN and QR on accept. Money is a JSON number; dates are YYYY-MM-DD; field names are snake_case; enum input is case-insensitive.

v1 · Canonical POST /api/ingest STRICT leg · full validation FIRS / NRS UBL mapping
One field, one place

Every field below carries a stable id. Hover a field row and click # to copy a deep link straight to it. Where a rule is stated once (the VAT model, the tax-category layers, the payment enum) it is stated only once — cross-references point back to that single definition rather than restating it.

1What you send vs. what we mint#

The canonical model is the customer-ingress body only. Sending a seller-owned or minted field is not an error — it is ignored.

You sendPronalytics supplies / mints
Invoice content: transaction_id, dates, invoice_kind, the buyer (non-B2C), line_items, tax, totals, references. The seller party (accounting_supplier_party) from your registration; the irn and qr_code; business_id; service_id. Never send these.

Hard requirements on the STRICT leg: transaction_id, issue_date, and line_items (at least one), plus full per-field validation. invoice_kind is inferred when omitted (see invoice_kind). Everything else is optional or conditional.

The IRN is derived, not sent: IRN = {SEED}-{SERVICE_ID}-{YYYYMMDD}, where SEED is the uppercased invoice_number (else transaction_id) stripped to A–Z 0–9. Every other character — including / and - — is removed, so INV/261 becomes INV261. Uniqueness is per (tenant, IRN); first-seen date wins.

2Root invoice record#

One Invoice object. Grouped as identity, dates, currency/mode, status, notes, references, parties, lines, and totals.

Identity

transaction_idstring Mandatory #

Your unique reference and idempotency key; it seeds the IRN when no invoice_number is present. It is also the only hard requirement on the LENIENT leg. A unique transaction_id is what guarantees a unique IRN — the seed-stripping is many-to-one.

Maps to Pronalytics-only — seeds the IRN, not a FIRS wire field.
invoice_numberstring Optional #

Human invoice number. Preferred IRN seed; falls back to transaction_id when absent (see §1 for the strip rule).

Maps to feeds irn (minted by Pronalytics)
invoice_type_codeenum Optional default 381 #

FIRS invoice-type code. Defaults to 381 (Commercial Invoice). Full list — credit note 380, debit note 384, and the rest — in Enumerations §10.

Maps to invoice_type_code
invoice_kindenum · B2B|B2C|B2G|G2B Inferred if omitted #

NRS transaction kind. If omitted, it is inferred: any buyer tax identity present (TIN / Tax ID / RC) ⇒ B2B, else B2C. An explicit business kind (B2B / B2G / G2B) is honored unconditionally — accepted even with an incomplete or absent buyer (FIRS clearance is the completeness gate). Explicit B2C must omit the customer party (consumer-PII guard — a B2C carrying a buyer is a hard reject). See rule §11. Uppercased on the wire.

Maps to invoice_kind
transaction_typestring Optional #

A data-mapping helper describing the business purpose of the invoice (e.g. loan_fees, interest_fees, late_penalty). Tenant-defined open string. Not a FIRS field and not the B2B/B2C kind (that is invoice_kind).

Maps to Pronalytics-only — data-mapping helper.

Dates & lifecycle

FieldTypeReqNotes
issue_datedatereqYYYY-MM-DD. FIRS-mandatory; not future-dated. On the STRICT leg a present-but-unparseable value is rejected (422).
issue_timestringoHH:MM:SS, 24-hour (FIRS-optional).
due_datedateoPayment deadline; defaults to issue_date.
tax_point_datedateoTax point if it differs from the issue date.
actual_delivery_datedateoDate goods/services were delivered (matters for goods sellers).
invoice_delivery_periodobjectoService/delivery window { start_date, end_date }.

Currency & mode

FieldTypeReqNotes
currencystringoISO 4217 document currency. Default NGN.
tax_currency_codestringoOptional input; defaults to NGN (always populated).
vat_modeenumoEXCLUSIVE (default) or INCLUSIVE — declares whether stated amounts include VAT. See §5.

Status & notes

FieldTypeReqNotes
payment_statusenumoOn the invoice record this is the fiscal status PENDING (default) / PAID / PARTIAL. The full six-value lifecycle enum used to update a payment lives in §9.
notestringoFIRS note 1 — free-form (stored encrypted at rest).
payment_terms_notestringoFIRS note 2 — payment terms (stored encrypted at rest).

Conveniences

FieldTypeReqNotes
branchstringoIssuing office branch. Pronalytics convenience — not a FIRS field (FIRS encodes branch in the TIN suffix).
accounting_coststringoCost-centre / accounting category.
order_referencestringoPurchase-order number.
buyer_referencestringoBuyer's own reference for the invoice.

Parties (§3), the document-reference family (§7), line items (§4) and the totals (§8) each have their own section below.

3Party — buyer & other parties#

The customer (buyer) and every optional party share one Party shape. The seller party is never sent — Pronalytics supplies it.

customerParty Conditional #

Required for B2B / B2G / G2B; omit for B2C. For a B2B buyer, FIRS clearance additionally requires postal_address.postal_zone — lga/state codes do not substitute. See rule §11.

Maps to accounting_customer_party

Party fields

At least one identity form is required; everything else is optional. The old JTB tin is the wire id used for clearance and transmit routing.

FieldTypeReqNotes
party_namestringreqLegal name. Friendly input key name also accepted.
tinstring1 of 3Old JTB TIN NNNNNNNN-NNNN. The wire id (clearance + transmit).
tax_idstring1 of 3New NRS Tax ID (12–13 digit). Carried alongside tin.
rc_numberstring1 of 3CAC RC number. Third accepted identity form.
emailstringoAP email. Validated as an email address.
telephonestringoMust start with + (country code) — FIRS rejects otherwise.
business_descriptionstringoIf present, at least 5 characters (FIRS).
postal_addressPostalAddressoBuyer address (friendly input key address). See below.

PostalAddress

FieldTypeReqNotes
street_namestringoStreet line. Friendly input key street.
city_namestringoCity. Friendly input key city.
postal_zonestringcondPostcode. Optional for B2C/no-party, but FIRS requires it on a B2B buyer for clearance — lga/state codes do not substitute. Any plausible area postcode clears.
lgastringoLGA code (e.g. 70 / NG-AB-ASO).
statestringoState code (e.g. 4 / NG-LA).
countrystringoISO 3166-1 alpha-2. Default NG.

Other parties

All optional; each takes the same Party shape above.

FieldPurpose
payee_partyPayment recipient if different from the supplier.
bill_partyBill-to party if different from the buyer.
ship_partyShip-to party if different from the buyer.
tax_representative_partyTax agent acting for the seller.

4Line items#

line_items is an array of at least one LineItem. Each line carries its own classification pair and its own VAT treatment — VAT is per-line (see §5).

descriptionstring Mandatory #

What was sold. Becomes the item name and description (e.g. "Portland cement"). The unit of measure is a separate coded field — see unit_code below.

quantitynumber > 0 Mandatory #

The true count of units invoiced, greater than zero. The unit those are counted in is the separate coded unit_code field below.

unit_pricenumber ≥ 0 Mandatory #

Unit price magnitude only (net when vat_mode=EXCLUSIVE). For a credit/debit note, express the reversal via invoice_type_code 380/384 plus absolute amounts — never a negative price (rule §11).

unit_codestring · coded Optional default C62 ("each") #

The unit of measure for this line's quantity — a coded FIRS invoice-quantity-code (UN/ECE Rec 20), never free text. FIRS requires it and its own sample sends a code (e.g. "XBG" = bag).

Leave it blank and it defaults to C62 ("one / each") — correct for most service, subscription and fee lines. Send the code directly, or a friendly word we map for you: EACH→C62, MONTH→MON, HOUR→HUR, DAY→DAY, YEAR→ANN, KG→KGM, LITRE→LTR, BOX→XBX, BAG→XBG, PACK→XPK. Friendly input keys unit_code, price_unit, uom, unit_of_measure are all accepted.

A real unit we cannot map is HELD — we never guess a code (that once caused a FIRS rejection). If you see "not a FIRS invoice-quantity-code", send a valid code or a known word above, or just leave it blank for C62.

line_extension_amountnumber Optional #

Line net. Computed from quantity × unit_price (less any discount) when absent.

line_item_typeenum · GOODS|SERVICE Optional default GOODS #

Picks the classification pair. GOODS uses hsn_code + product_category; SERVICE uses isic_code + service_category (rule §11).

Classification pair

FieldTypeUsed forNotes
hsn_codestringGOODSHS code, pattern NNNN.NN (e.g. 8471.30).
product_categorystringGOODSGoods category (at least 2 chars). Pairs with hsn_code.
isic_codestringSERVICEISIC code, pattern NNNN (4-digit, no decimal).
service_categorystringSERVICEService category (at least 2 chars). Pairs with isic_code.

Unknown hsn_code/isic_code are accepted with a warning while the shipped reference lists are seed subsets — the warning rides the feedback webhook, never a reject on our own incomplete data. When a full official list is loaded, unknowns become hard rejects with no change to your integration (rule §11).

Per-line VAT & adjustments

FieldTypeReqNotes
vat_ratenumberoPer-line VAT %. Default 7.5. Forced to 0 for zero-rate categories (see §5).
vat_typeenumoPer-line VAT treatment. Default STANDARD. Full driver set + FIRS codes in §6. Legacy input key tax_category_id also accepted.
discount_ratenumberoPer-line discount %.
discount_amountnumberoPer-line discount value. May not exceed line gross (unit_price × quantity).
fee_ratenumberoPer-line fee % (FIRS-optional).
fee_amountnumberoPer-line fee value (FIRS-optional).
sellers_item_identificationstringoSeller's SKU / item code.
customer_skustringoCustomer's own SKU.

5The VAT model (per-line)#

The single VAT rule for the whole schema, stated once here. Everything else references back to it.

One VAT rule

VAT is computed per line, driven by each line's vat_type + vat_rate. EXEMPT and ZERO lines contribute 0 VAT regardless of vat_mode. vat_mode only declares whether stated amounts are tax-inclusive or tax-exclusive. There is no unconditional divide-by-1.075, and there is no invoice-level fee-amount VAT source.

How each mode computes

vat_modeMeaningPer-line math
EXCLUSIVE (default)unit_price is net; VAT is added on top.net = qty × unit_price; vat = net × rate/100.
INCLUSIVEStated amount already includes VAT.net = charged / (1 + rate/100); vat = charged − net.

How each category rates

CategoryRate appliedVAT contributed
STANDARDvat_rate (default 7.5%)computed
REDUCED / LOCAL_SALESthe vat_rate you givecomputed
EXEMPTforced 00
ZEROforced 00
WHTforced 0 (own subtotal)0

Caller-supplied VAT. A header vat_amount is honored verbatim when it cannot be computed from the payload (e.g. an out-of-band commission VAT). See §8.

Withholding tax is informational: it is emitted as its own WITHHOLDING_TAX subtotal and does not change payable_amount — it reduces only the cash actually remitted to the seller. A WHT-category line's amount is the withholding base; WHT = base × 7.5% (rule §11).

6Tax categories — 27 tags, 6 drivers#

Two layers, not a contradiction. Layer 1 is the pinned FIRS master list of 27 tax categories — all 27 are valid tags. Layer 2 is the 6 VAT-family codes that actually drive Nigerian VAT computation.

Layer 1 — the 27-category FIRS master list

FIRS pins a master list of 27 tax categories (GST variants, excise, duty, stamp duty, and more). Every one of the 27 is a valid tag. Pronalytics serves the live list, verbatim and cached, from the public reference resource GET /api/firs-resources/tax-categories so you can render a picker. On the wire a subtotal's category is tax_subtotal[].tax_category = { id, percent }.

Layer 2 — the 6 VAT-family drivers

These six codes are what the serializer computes and emits for Nigerian VAT. Supply any of them as vat_type — the friendly name or the FIRS code.

vat_type (friendly)FIRS wire codeMeaningRate
STANDARDSTANDARD_VATNigeria standard VAT7.5%
ZEROZERO_VATZero-rated (e.g. exports)0
REDUCEDREDUCED_VATReduced raterate given
EXEMPTEXEMPTEDVAT-exempt supply0
WHTWITHHOLDING_TAXWithholding tax (informational)0
LOCAL_SALESLOCAL_SALES_TAXLocal sales taxrate given
Accepted input spellings

Each driver is accepted as its friendly name (STANDARD), its FIRS code (STANDARD_VAT), either in any case (standard), and via the legacy key tax_category_id. All resolve to the six drivers above. A subtotal on the wire carries tax_category:{ id, percent }; the six drivers are the only categories the canonical serializer computes today — a value outside the six is not silently mapped. If you need a category from the wider 27, raise it with Pronalytics so it can be added to the mapping.

TaxSubtotal shape

{ taxable_amount, tax_amount, tax_category: { id, percent } }. percent is the rate (7.5 for STANDARD). Zero-rate drivers carry percent = 0. WHT is a subtotal with the WITHHOLDING_TAX category.

7Document-reference family#

Every reference here is a DocumentReference object { irn, issue_date } (or a list of them where noted). All optional; each links the invoice to a related prior document for the audit trail.

Correction pointers (credit / debit notes)

FieldTypeNotes
reference_invoice_numberstringOriginal IRN / number a 380/384 corrects. Feeds billing_reference.
reference_invoice_datedateIssue date of the referenced original.
billing_referencearray<DocRef>Explicit prior-doc links. Built from the two fields above when not given explicitly.

Related-document pointers

FieldTypeLinks to
dispatch_document_referenceDocRefDespatch advice.
receipt_document_referenceDocRefReceipt advice.
originator_document_referenceDocRefOriginating document.
contract_document_referenceDocRefGoverning contract.
additional_document_referencearray<DocRef>Any other related documents.

DocumentReference shape: { "irn": string, "issue_date": "YYYY-MM-DD" }. A credit/debit note (380/384) normally carries one of these back to the original; a standalone note with no reference is allowed for aggregate adjustments (rule §11).

8Tax totals & monetary totals#

All optional overrides — omit them and we compute from the lines. Supply them to pin an exact breakdown (e.g. an out-of-band VAT figure not computable from the payload).

Convenience header amounts

FieldTypeNotes
vat_amountnumberComputed per-line if omitted. Supplying it overrides the computed VAT (used for out-of-band figures).
wht_amountnumberSerialized as a WITHHOLDING_TAX subtotal. Informational — does not reduce payable_amount.
total_amountnumberApproximately payable_amount. Computed if omitted.

tax_total · array of TaxTotal

tax_totalarray · TaxTotal Optional #

Explicit tax breakdown; computed from the lines when absent. A supplied tax_total is honored as-is, but any WHT still rides through as its own subtotal.

TaxTotal = { tax_amount, tax_subtotal: [ TaxSubtotal ] }, where each TaxSubtotal is { taxable_amount, tax_amount, tax_category:{id,percent} } (see §6).

legal_monetary_total · object

9Payment status, means & charges#

Two distinct things share the name "payment status" — keep them apart.

One payment enum — six values

The canonical payment-status enum used to update an invoice (POST /api/payment/update) has exactly six values, validated identically on every payment surface. Case-insensitive on the wire, echoed UPPER. An unrecognized value is a validation failure.

PENDINGPARTIALPAID OVERDUECANCELLEDREFUNDED

The payment_status field on the invoice record itself is the narrower fiscal status set at creation — PENDING (default) / PAID / PARTIAL. It records where the invoice sits at issue. The six-value enum above is the vocabulary for later lifecycle updates to that payment. They are not the same list; do not conflate them.

payment_meansarray · PaymentMeans Optional #

Each entry { payment_means_code, payment_due_date? }. Codes in Enumerations §10.

Maps to payment_means
allowance_chargearray · AllowanceCharge Optional #

Each entry { charge_indicator (bool), amount, reason? }. charge_indicator true = charge, false = allowance. A document-level charge raises, an allowance lowers, the standard-rated base. Freight is a charge here, never a line.

Maps to allowance_charge

10Enumerations#

FIRS / NRS resource-backed value sets. Tax categories are in §6; the payment-update enum is in §9.

invoice_kind — NRS transaction kind

B2BB2CB2GG2B

invoice_type_code — FIRS invoice-types

CodeTypeCodeType
380Credit Note394Self-Billed Invoice
381Commercial Invoice (default)395Credit Note Request
384Debit Note396Invoice Request
385Self-Billed Invoice397Final Settlement
386Factored Invoice399Bill of Lading
388Statement of Account400Waybill
389Purchase Order402Shipping Instructions
390Proforma Invoice404Certificate of Origin
392Consignment Invoice406Customs Declaration
393Self-Billed Credit Note408Packing List

payment_status — invoice fiscal status

PENDING · defaultPAIDPARTIAL

This is the status on the record. The six-value lifecycle enum for updating a payment is in §9.

vat_mode

EXCLUSIVE · defaultINCLUSIVE

line_item_type

GOODS · defaultSERVICE

vat_type — the 6 drivers

STANDARD · defaultREDUCEDLOCAL_SALES ZEROEXEMPTWHT

Green drivers rate to 0. Full mapping and the 27-tag master list in §6.

payment_means_code — FIRS payment-means

CodeMeansCodeMeans
10Cash31Debit Transfer
20Cheque42ACH Credit
30Credit Transfer43ACH Debit

payment_means_code is a free string on the model; the codes above are the common FIRS values. The full list is served by GET /api/firs-resources.

Reference resources

Public, no-auth, cached 24h: GET /api/firs-resources/{name} for states, lgas, countries, tax-categories (the 27-tag master list), hs-codes, service-codes.

11Validation rules#

Baked into the canonical model. These explain most accept/reject outcomes — read them before you map.

Kind-aware buyer party #

invoice_kind ∈ {B2B, B2G, G2B} ⇒ customer expected (identity + email + address, with postal_zone for clearance). An explicit business kind with no/partial buyer is accepted at ingress and flagged on the feedback webhook — it will not clear at FIRS until the buyer is complete. B2C ⇒ customer must be omitted (no consumer PII); an explicit B2C carrying a buyer is a hard reject. Omitting invoice_kind infers it from buyer identity.

One classification pair per line #

Goods = hsn_code + product_category; service = isic_code + service_category; matching line_item_type. A discount_amount may not exceed the line gross.

Credit / debit notes #

Use invoice_type_code 380 / 384 with positive (abs) amounts; express the reversal via the type plus reference_invoice_number / reference_invoice_date. Never send a negative unit_price. A standalone 380 (no reference) is allowed for aggregate adjustments.

vat_mode net-backout #

EXCLUSIVE (default): unit_price is net, VAT added on top. INCLUSIVE: the amount already includes VAT, so net is backed out as charged / (1 + rate/100) (never × 0.925) and tax = charged − net. Full model in §5.

Withholding tax #

Mirrors FIRS via a WITHHOLDING_TAX subtotal plus the wht_amount convenience. A WHT-category line's amount is treated as the withholding base (WHT = base × 7.5%), not as tax on the full line. WHT does not reduce payable_amount.

Identity precedence #

The old JTB tin goes on the wire (clearance + transmit routing); tax_id and rc_number are carried alongside. A buyer with only a Tax ID or RC clears at FIRS but will not transmit until an old-format TIN is supplied.

HS / ISIC value-set warnings #

hsn_code / isic_code are checked against the loaded reference set as well as the format. While the shipped lists are seed subsets, an unknown code is accepted with a warning on the feedback webhook — never hard-rejected on our own incomplete data. When a full official list is loaded, unknowns become rejects with no change to your integration.

issue_date on the STRICT leg #

On the canonical (STRICT) leg a present-but-unparseable issue_date is rejected (422) — it never silently falls back to today. It must be a real YYYY-MM-DD and not future-dated.

12Worked examples — Grollo & Mollo#

Fictional. Seller Grollo Consulting (supplied by Pronalytics from registration — never sent); buyer Mollo Industries. Amounts in NGN.

A · Minimal B2C — no buyer, one goods line

EXCLUSIVE · STANDARD 7.5% · buyer omitted
{
  "transaction_id": "POS-000817",
  "invoice_kind": "B2C",
  "issue_date": "2026-07-08",
  "line_items": [
    {
      "description": "Engine oil, 5 L can",
      "quantity": 2,
      "unit_price": 12000,
      "line_item_type": "GOODS",
      "hsn_code": "2710.19",
      "product_category": "Lubricants",
      "vat_type": "STANDARD",
      "vat_rate": 7.5
    }
  ]
}
LineNetRateVATGross
Engine oil ×224,000.007.5%1,800.0025,800.00
payable_amount24,000.00—1,800.0025,800.00

B · Full B2B — complete buyer, service line, mixed VAT

explicit B2B · one STANDARD line + one EXEMPT line
{
  "transaction_id": "GRC-SINV-42",
  "invoice_number": "GRC-SINV-42",
  "invoice_kind": "B2B",
  "invoice_type_code": "381",
  "issue_date": "2026-07-08",
  "due_date": "2026-08-07",
  "payment_status": "PENDING",
  "customer": {
    "party_name": "Mollo Industries",
    "tin": "12345678-0001",
    "tax_id": "2522500000001",
    "email": "[email protected]",
    "telephone": "+2348012345678",
    "postal_address": {
      "street_name": "12 Marina Road",
      "city_name": "Lagos",
      "postal_zone": "101241",
      "state": "NG-LA",
      "country": "NG"
    }
  },
  "line_items": [
    {
      "description": "Advisory retainer, July",
      "quantity": 1,
      "unit_price": 250000,
      "line_item_type": "SERVICE",
      "isic_code": "7020",
      "service_category": "Management consultancy",
      "vat_type": "STANDARD"
    },
    {
      "description": "Statutory filing fee (pass-through)",
      "quantity": 1,
      "unit_price": 40000,
      "line_item_type": "SERVICE",
      "isic_code": "8411",
      "service_category": "Public administration",
      "vat_type": "EXEMPT"
    }
  ],
  "payment_terms_note": "Payment due within 30 days."
}
Linevat_typeNetRateVAT
Advisory retainerSTANDARD250,000.007.5%18,750.00
Statutory filing feeEXEMPT40,000.0000.00
payable_amount—290,000.00—18,750.00

Payable = 290,000 net + 18,750 VAT = 308,750.00. The EXEMPT line contributes 0 VAT regardless of vat_mode — the single VAT rule from §5.

C · INCLUSIVE mode — VAT backed out of a gross price

vat_mode INCLUSIVE · net = charged / 1.075
{
  "transaction_id": "GRC-INC-09",
  "invoice_kind": "B2C",
  "issue_date": "2026-07-08",
  "vat_mode": "INCLUSIVE",
  "line_items": [
    {
      "description": "Diagnostic session (price shown incl. VAT)",
      "quantity": 1,
      "unit_price": 10750,
      "line_item_type": "SERVICE",
      "isic_code": "7020",
      "service_category": "Technical services",
      "vat_type": "STANDARD"
    }
  ]
}
Charged (incl.)NetVAT
10,750.0010,000.00750.00

Net = 10,750 / 1.075 = 10,000; VAT = 10,750 − 10,000 = 750. Never × 0.925 (rule §11).

D · Credit note (380) — corrects invoice B, abs amounts

type 380 · positive amount · reference points back to GRC-SINV-42
{
  "transaction_id": "GRC-CN-07",
  "invoice_type_code": "380",
  "invoice_kind": "B2B",
  "issue_date": "2026-07-08",
  "reference_invoice_number": "GRC-SINV-42",
  "reference_invoice_date": "2026-07-08",
  "customer": {
    "party_name": "Mollo Industries",
    "tin": "12345678-0001",
    "email": "[email protected]",
    "postal_address": { "street_name": "12 Marina Road", "city_name": "Lagos", "postal_zone": "101241" }
  },
  "line_items": [
    {
      "description": "Advisory retainer adjustment",
      "quantity": 1,
      "unit_price": 50000,
      "line_item_type": "SERVICE",
      "isic_code": "7020",
      "service_category": "Management consultancy",
      "vat_type": "STANDARD"
    }
  ]
}

The reversal is carried by invoice_type_code: "380" plus the reference — the amount stays positive. A negative unit_price is never sent (rule §11).

Link copied