Overview
This reference covers the tenant integration API for ERP and server-to-server connections. Register an API key in the portal (Settings → API Access), then create and submit invoices, poll status, and receive webhook events on invoice lifecycle changes.
If you are having trouble with the API or need help getting started, contact support.
- Create an API key in Settings → API Access (portal admin required once).
POST /api/v1/invoices/create— create and queue a single invoice for NRS submission (HTTP 202).- Poll
GET /api/v1/invoices/{irn}/statusor listen forinvoice.certified/invoice.failedwebhook events. - Use
POST /api/v1/invoices/{id}/retryto recover from failed NRS submissions.
API version 1.0.0
Base URL
https://api.flowpoint.ng
Common headers
X-Flowpoint-Key: <api_key>— required on every authenticated requestContent-Type: application/json— required on POST/PATCH JSON bodies
Authentication
All documented tenant integration endpoints require an API key. Send your key in the X-Flowpoint-Key header on every request.
API keys
- Create keys in the portal under Settings → API Access (portal administrator required).
- Send the plaintext key in the
X-Flowpoint-Keyheader. - Revoked or expired keys return 401. Keys cannot access admin-only settings routes.
- API keys are not restricted by portal role permissions.
Admin-only (not API key)
Tenant profile, crypto keys, locations, integration connectors, and webhook subscription management require a portal admin session. API keys receive 403 on those routes.
Rate Limits
Limits use a 1-minute window and expose RateLimit-* draft-7 headers. Exceeding limits returns HTTP 429 — implement exponential backoff.
Global limits (per IP)
| Scope | Default limit |
|---|---|
| Authentication (/api/v1/auth/*) | 10 requests/minute |
| All other /api/v1/* | 120 requests/minute |
Invoice write limits (per tenant)
Keyed on tenant ID (not IP). Protects against a single compromised key flooding writes.
| Route | Limit |
|---|---|
| POST /api/v1/invoices/create | 60 requests/minute |
| POST /api/v1/invoices/upload/csv/submit | 5 requests/minute |
List pagination
Collection GETs return { "items": [], "total": 142, "limit": 50, "offset": 0 }. Query params: limit (default 50), offset (default 0), plus resource filters documented on each list endpoint.
| Endpoint | Max limit |
|---|---|
| GET /api/v1/invoices | 500 |
| GET /api/v1/customers | 200 |
| GET /api/v1/items | 200 |
GET /api/v1/invoices returns certified invoices only.
Create invoice fields
POST /api/v1/invoices/create accepts a Flowpoint invoice request shape — not raw NRS JSON. The customer object in the request is the buyer. Your supplier (seller) is always your organisation as the Flowpoint tenant — derived from your business profile and primary location, not from the JSON body. B2B (Business-to-Business) and B2G (Business-to-Government) share the same buyer requirements; B2C (Business-to-Consumer) is lighter.
Common fields (all invoice kinds)
| Field | B2B / B2G | B2C | Notes |
|---|---|---|---|
| invoice_kind | Required | Required | B2B, B2C, or B2G |
| invoice_type | Required | Required | invoice, credit_note, or debit_note |
| document_number | Required | Required | Human-readable invoice number (not a UUID). Used to generate the IRN. |
| issue_date, due_date | Required | Required | ISO dates YYYY-MM-DD |
| currency | Required | Required | Default NGN |
| customer.name | Required | Required | Buyer display name |
| lines[] | Required (≥1) | Required (≥1) | description, quantity, unit_price, hsn_code, tax_scheme, tax_rate |
B2B (Business-to-Business) / B2G (Business-to-Government) — extra customer fields
Enforced on POST /create when invoice_kind is B2B or B2G (or inferred as B2G from the buyer TIN). Missing fields return HTTP 400 with CUSTOMER_VALIDATION_FAILED.
| Field | Required | Notes |
|---|---|---|
| customer.tin | Yes | Standard TIN or RC-XXXXXXX (≥5 characters) |
| customer.email | Yes | Valid email on the buyer party |
| customer.address | Yes | Street address |
| customer.city | Yes | City |
| customer.postal_code | Yes | Postal / ZIP code |
| customer.phone | No | Optional — included on NRS buyer party when set |
| customer.country | No | Default NG |
Tenant-derived NRS fields (autofilled)
The supplier (seller) on every invoice is your organisation — the Flowpoint tenant behind the API key. You never send business_id or accounting_supplier_party in the create JSON; Flowpoint derives them from your tenant profile. If something is not yet on your profile, Flowpoint uses what is already on file from signup or onboarding (for example, the portal admin email when no business email is set). NRS submission still requires a complete Business ID, supplier TIN, email, street, city, and postal code before a request will succeed.
Validated on POST /create. Incomplete data returns HTTP 400 with SUPPLIER_VALIDATION_FAILED.
| NRS field | Where to set | Notes |
|---|---|---|
| business_id | Settings → Business profile (NRS business ID from onboarding) | - |
| accounting_supplier_party.party_name | Settings → Business profile | Seller party name on NRS |
| accounting_supplier_party.tin | Settings → Business profile | Checked on POST /create and at submit time |
| accounting_supplier_party.email | Business profile | If not set, Flowpoint uses the portal admin account email |
| accounting_supplier_party.telephone | Business profile | Optional on NRS — must start with + when set |
| accounting_supplier_party.postal_address | Settings → Locations (primary location) | Street name, city name, postal zone, state, lga, country — required for /sign |
| accounting_supplier_party.postal_address.city_name | Settings → Locations (primary location) | City text |
| accounting_supplier_party.postal_address.state | Settings → Locations (primary location) | NRS state code (e.g. NG-LA) |
| accounting_supplier_party.postal_address.lga | Settings → Locations (primary location) | NRS LGA code (e.g. NG-LA-IKO); must match state |
business_id and accounting_supplier_party appear in examples only to show what Flowpoint sends to NRS. Do not include them in your POST body — they are autofilled from your Flowpoint account (Your business profile / primary location).
Full request — B2B (Business-to-Business)
Use on POST /api/v1/invoices/create. Supplier profile must be complete before the request succeeds.
{
"business_id": "autofilled from your account",
"accounting_supplier_party": {
"party_name": "autofilled from your account",
"tin": "autofilled from your account",
"email": "autofilled from your account",
"telephone": "autofilled from your account",
"business_description": "autofilled from your account",
"postal_address": {
"street_name": "autofilled from your account",
"city_name": "autofilled from your account",
"postal_zone": "autofilled from your account",
"lga": "autofilled from your account",
"state": "autofilled from your account",
"country": "autofilled from your account"
}
},
"invoice_kind": "B2B",
"invoice_type": "invoice",
"document_number": "INV-2026-001",
"issue_date": "2026-04-25",
"due_date": "2026-05-25",
"currency": "NGN",
"customer": {
"name": "Acme Ltd",
"tin": "12345678-0001",
"email": "[email protected]",
"phone": "+2348012345678",
"address": "42 Marina Road",
"city": "Lagos",
"country": "NG",
"postal_code": "101001"
},
"lines": [
{
"description": "Consulting services",
"quantity": 1,
"unit_price": 50000,
"hsn_code": "8517.11",
"tax_scheme": "STANDARD_VAT",
"tax_rate": 7.5,
"unit_of_measure": "Each"
}
],
"note": "Thank you for your business",
"po_reference": "PO-7781"
}B2G (Business-to-Government): use the same buyer fields and autofilled business_id / accounting_supplier_party behaviour as B2B — set "invoice_kind": "B2G".
Full request — B2C (Business-to-Consumer)
Only customer.name is required. Buyer TIN, email, and address are not required for B2C retail invoices. Same body on POST /api/v1/invoices/create. B2C invoices typically reach reported when certified;
{
"business_id": "autofilled from your account",
"accounting_supplier_party": {
"party_name": "autofilled from your account",
"tin": "autofilled from your account",
"email": "autofilled from your account",
"telephone": "autofilled from your account",
"business_description": "autofilled from your account",
"postal_address": {
"street_name": "autofilled from your account",
"city_name": "autofilled from your account",
"postal_zone": "autofilled from your account",
"lga": "autofilled from your account",
"state": "autofilled from your account",
"country": "autofilled from your account"
}
},
"invoice_kind": "B2C",
"invoice_type": "invoice",
"document_number": "INV-2026-042",
"issue_date": "2026-04-25",
"due_date": "2026-04-25",
"currency": "NGN",
"customer": {
"name": "Walk-in Customer"
},
"lines": [
{
"description": "Consulting services",
"quantity": 1,
"unit_price": 50000,
"hsn_code": "8517.11",
"tax_scheme": "STANDARD_VAT",
"tax_rate": 7.5,
"unit_of_measure": "Each"
}
]
}Invoices
Create, submit, and track invoices for NRS certification. Use POST /create to submit a single invoice for NRS. Returns PENDING with invoiceId — poll GET /invoices/{id} or subscribe to webhook events. Certified invoices appear on GET /invoices.
Customers & Items
Master data for repeat customers and catalogue items. Optional — you can inline customer and line data on invoice create. Use GET /customers/search and GET /items/search for typeahead (q required, 2–100 characters). List endpoints support optional broader search filters.
Invoice Webhook Events
Flowpoint POSTs JSON to HTTPS URLs you register in Settings → Webhooks (portal admin). These are outbound events — not API endpoints you call. Register one callback URL per tenant; it can subscribe to multiple invoice lifecycle events. Use PATCH to update events on an existing URL.
- Delivery method: HTTP POST with Content-Type application/json
- Required shared secret at registration; every delivery includes an HMAC signature in the
x-flowpoint-signatureheader - Root
statusispending,certified, orfailed— matching the event lifecycle - Signed body includes
timestamp(Unix ms) andemittedAt(ISO 8601); reject deliveries older than 5 minutes for replay protection - The
sourcefield identifies which step in the invoice lifecycle emitted the event
Webhook delivery
Flowpoint POSTs each event to your callback URL. Your endpoint must accept the JSON body and return any HTTP 2xx status within 10 seconds. An empty 204 No Content response is fine. Flowpoint does not read or validate your response body on success.
Delivery is at-least-once — the same event may arrive more than once. Use the envelope id field to dedupe on your side.
| Attempt | When | On failure |
|---|---|---|
| 1 | Immediate | Retry after ~5s |
| 2 | After ~5s backoff | Retry after ~10s |
| 3 | After ~10s backoff | Retry after ~20s |
| 4 | After ~20s backoff | Retry after ~40s |
| 5 | After ~40s backoff | Endpoint auto-paused |
A failed attempt is any non-2xx HTTP status, a request timeout (>10s), or a network error. After 5 failed attempts for one delivery, Flowpoint sets the webhook endpoint to inactive and stops sending new events to that URL. Fix your endpoint, then re-enable with PATCH /api/v1/integrations/webhooks/:id and { "active": true } (portal Settings → Webhooks → Edit).
Signature verification
A signing secret is required when you register the webhook. Flowpoint sends x-flowpoint-signature — an HMAC-SHA256 hex digest of the raw request body. Verify the signature with a timing-safe equality check, then reject any delivery where Date.now() - timestamp > 300000 (5 minutes).
const crypto = require('crypto');
const MAX_AGE_MS = 300000; // 5 minutes
function verifyFlowpointWebhook(rawBody, signatureHeader, secret) {
let event;
try {
event = JSON.parse(rawBody);
} catch {
return false;
}
if (typeof event.timestamp !== 'number') return false;
if (Date.now() - event.timestamp > MAX_AGE_MS) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader, 'utf8'),
Buffer.from(expected, 'utf8'),
);
}Errors and status meanings
Failed requests return JSON with at least an error string. Many invoice routes also include reason (machine code), detail, and fields. Master-data POST routes use code instead of reason.
If you are having trouble integrating or interpreting API responses, contact support.
{
"error": "human-readable message",
"reason": "MACHINE_CODE",
"code": "machine_code",
"detail": "optional extra context",
"fields": { "customer.tin": "required" }
}Not every field appears on every error. Master-data routes use code instead of reason. Search endpoints return plain error when q is missing or out of range (2–100 characters).
HTTP status codes
| HTTP | When | Meaning | Next step |
|---|---|---|---|
| 401 | Missing or invalid X-Flowpoint-Key header | Request is not authenticated | Send a valid API key in the X-Flowpoint-Key header |
| 403 | Valid API key used on a route that does not accept API keys (e.g. tenant settings) | Route not available to API keys | Use a portal admin session for admin routes, or confirm the endpoint supports API key access |
| 400 | Validation failed, submission blocked, or bad query (e.g. search q too short) | Request cannot be processed — see error and reason/code | Fix the body or query and retry |
| 404 | Invoice, draft, batch, or IRN not found for this tenant | Resource does not exist or is not visible | Check the UUID or IRN |
| 409 | Conflict — duplicate document number, draft not editable, customer/item exists | State conflict — see reason or code | Use a different number or resolve the conflict |
| 429 | Global or per-tenant rate limit exceeded | Too many requests in the window | Backoff and retry; check RateLimit-* headers |
| 503 | GET /health when the API is unavailable | API health degraded — response body { "status": "degraded" } | Retry GET /health; contact support if persistent |
| 500 | Unexpected server error | Internal failure | Retry; contact support if persistent |
| 502 | NRS payment confirm/update failed (PATCH /invoices/{id}/payment) | Upstream NRS call failed — paymentReported stays false | Read nrsError in the body; fix and retry |
Invoice machine codes (reason)
| reason | HTTP | Meaning | Next step |
|---|---|---|---|
| CUSTOMER_VALIDATION_FAILED | 400 | B2B/B2G buyer fields missing or invalid on create or PATCH draft | Send full customer TIN, email, address, city, postal code |
| SUPPLIER_VALIDATION_FAILED | 400 | Tenant supplier profile incomplete on POST /create or submit | Complete NRS supplier fields in the portal |
| DOCUMENT_NUMBER_CONFLICT_INVOICE | 409 | A certified invoice already uses this document number | Pick a different invoice number |
| invalid_document_number | 400 | Document number failed format rules (e.g. UUID-shaped) | Use a human-readable invoice number |
| unresolved_b2b_missing_tin | 400 | B2B invoice could not resolve buyer TIN when saving | Send customer.tin on create |
| unresolved_b2b_customer_address | 400 | B2B buyer address fields could not be resolved | Send full customer address fields |
| unresolvable_hsn | 400 | Line HSN could not be resolved when saving the invoice | Send valid HSN code on each line |
| unresolvable_unit_price | 400 | Line unit price could not be resolved | Send positive Unit price on each line |
| incomplete_unpatchable | 400 | Invoice validation failed and required fixes could not be applied automatically | Fix the payload per error detail |
| ZERO_VALUE | 400 | Invoice total payable is zero | Ensure lines sum to a positive payable amount |
| NO_SERVICE_ID | 400 | Tenant NRS Service ID not configured | Complete NRS Setup in the portal |
| NO_BUSINESS_ID | 400 | Tenant NRS Business ID not configured | Complete NRS Setup in the portal |
| NO_KEYS | 400 | No active cryptographic keys on the tenant | Upload keys in Settings → Crypto Keys |
| BLOCKED_PENDING_REVIEW | 400 | Invoice held for review before NRS submission | Complete review in the portal, then retry |
| BLOCKED_PENDING_APPROVAL | 400 | Invoice awaiting approval before NRS submission | Complete approval in the portal, then retry |
| BLOCKED_CHECKLIST_FAILED | 400 | Invoice did not pass validation before NRS submission | Fix validation errors in the response detail |
| SUBMISSION_ERROR | 500/502 | NRS submission failed unexpectedly | POST /invoices/{id}/retry |
| NOT_SIGNED | 400 | Reconcile called on an invoice not yet certified | Submit to NRS first |
| NOT_ELIGIBLE | 400 | Payment report not allowed for this invoice | Invoice must be certified B2C in reported status with an IRN |
Master data codes (code)
Returned by POST /api/v1/customers and POST /api/v1/items.
| code | HTTP | Meaning | Next step |
|---|---|---|---|
| missing_field | 400 | Required field empty on POST /customers or POST /items | Send all required fields in the body |
| invalid_tin | 400 | Customer TIN too short or invalid format | Send a valid TIN (≥5 characters) |
| invalid_email | 400 | Email missing or invalid format | Send a valid email address |
| invalid_phone | 400 | Phone missing or does not start with + country code | Send phone as +2348012345678 |
| invalid_state | 400 | state is not a known NRS code | Omit state or use GET /helpers/states |
| invalid_lga | 400 | lga invalid, or state missing when lga is sent | Omit lga or pair with a valid state from GET /helpers/lgas |
| invalid_kind | 400 | customerKind is not B2B or B2G | Set customerKind to B2B or B2G |
| tin_exists | 409 | Customer with this TIN already exists | Use GET /customers/search or list |
| invalid_price | 400 | unitPriceKobo missing, zero, or negative | Send an integer kobo amount greater than zero |
| invalid_cost | 400 | costKobo negative when provided | Omit cost or send a non-negative amount |
| invalid_tax_scheme | 400 | Unrecognised taxScheme value | Use STANDARD_VAT or another supported scheme |
| item_exists | 409 | Item with this name already exists | Use GET /items/search or list |
| create_failed | 500 | Record insert succeeded but could not be read back | Retry; contact support if persistent |
Create response status
The status field on POST /invoices/create.
| status | HTTP | Meaning | Next step |
|---|---|---|---|
| PENDING | 202 | Invoice accepted for NRS processing | Poll GET /invoices/{invoiceId} until certified or failed |
| BLOCKED:… | 201 | Invoice saved but not submitted to NRS (review, approval, or validation) | Resolve the block in the portal, then retry |
Payment status fields
From PATCH /invoices/{id}/payment.
| Field | Values | Meaning |
|---|---|---|
| paymentStatus (Flowpoint) | PAID · PARTIAL · UNPAID | Local payment state after successful NRS update |
| nrsPaymentStatus (NRS) | PENDING · PARTIAL · PAID · REJECTED | Status sent to NRS on PATCH /payment |
| nrsError | string | Present on HTTP 502 when NRS confirm or update fails |
Public IRN verify
GET /api/v1/invoices/verify/{irn} — no auth.
| Outcome | HTTP | Meaning | Next step |
|---|---|---|---|
| valid: true | 200 | IRN exists and is certified for a tenant | Use returned invoice summary |
| valid: false, reason: malformed IRN | 400 | IRN string failed format check | Correct the IRN |
| valid: false, reason: IRN not found | 404 | No matching certified invoice | Verify IRN with issuer |
Helpers
Reference lookups for standard codes and labels used on invoices — invoice types, tax categories, payment means, quantity units, product and service codes, and Nigeria states, LGAs, and countries. Authenticate with your API key. Responses may be cached for a short period; stay within rate limits.
Public Endpoints
No authentication required. API health check and public IRN verification.
API health (GET /health)
GET /health is an unauthenticated uptime check. It is not rate-limited. Returns { "status": "ok" } (HTTP 200) when operational, or { "status": "degraded" } (HTTP 503) when unavailable. The status field here means API health — not invoice or batch status.
| body.status | HTTP | Meaning | Next step |
|---|---|---|---|
| ok | 200 | API is operational | None |
| degraded | 503 | API health check failed | Retry GET /health; contact support if persistent |
The portal app exposes a public status page at /status for end users. It is a frontend page (not GET /status on the API). It polls GET /health and shows plain uptime only — never dependency detail. Open status page.
If you are having trouble integrating or interpreting API responses, contact support.