Build on Cashflow
One API key per business. Full read/write access to your contacts, sales documents, inventory, purchases, banking and team — plain REST, JSON in and out, no SDK required.
curl https://cashflow.ng/api/v1/documents \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"invoice","issue_date":"2026-09-13","lines":[{"description":"Design work","qty":1,"unit_price":50000}]}'
Introduction
The Cashflow API is a plain REST, JSON-in/JSON-out interface to the same data and business rules that power the Cashflow web app. There is no SDK, no GraphQL layer, no webhooks yet — just HTTP requests with a bearer token. If you can create an invoice by hand in Cashflow, you can create it exactly the same way over the API.
Accept header.Base URL
Every endpoint on this page is relative to:
https://cashflow.ng/api/v1Request & response format
Send Content-Type: application/json with a JSON body on POST/PUT
requests. Every response — success or failure — is a JSON object with a top-level
status field ("Successful" or "Failed"), so you can branch on
that alone without inspecting the HTTP status code. There's no "Pending" here — every
endpoint on this page is synchronous, so a request has resolved one way or the other by the time it
answers. A single resource (e.g. GET /items/{id}) comes back nested under data:
{
"status": "Successful",
"data": {
"id": "«uuid»",
"« …the resource's own fields »": null
}
}
A list endpoint (e.g. GET /items) is paginated — status merges straight
into that same pagination envelope rather than nesting it a second level deeper:
{
"status": "Successful",
"current_page": 1,
"data": [
"« one object per row »"
],
"first_page_url": "https://cashflow.ng/api/v1/items?page=1",
"last_page": 4,
"last_page_url": "https://cashflow.ng/api/v1/items?page=4",
"next_page_url": "https://cashflow.ng/api/v1/items?page=2",
"prev_page_url": null,
"per_page": 20,
"total": 78
}
Every list endpoint accepts ?per_page= (max 100, default 20) and most accept a filter
or two documented under that resource (e.g. ?status=, ?q=). An action that
doesn't return a resource (e.g. a delete) answers with {"status": "Successful", "message": "…"}
instead of a data key — see each resource's own examples below for the exact shape.
Authentication
Every business gets exactly one API key, generated and rotated from Admin console ▸ Developer API — visible only to the business owner. Send it as a bearer token on every request:
Authorization: Bearer cf_live_••••••••••••••••••••••••••••••••••••
- • The key is shown in full only once, right after you generate or rotate it — Cashflow never stores the plaintext.
- • Rotating a key immediately invalidates the previous one. There's no overlap window, so update every integration before you rotate.
- • A key grants full read/write access to that business's data. Treat it like a password — never commit it, never expose it in client-side code.
- • All endpoints are namespaced under
https://cashflow.ng/api/v1and always return JSON, regardless of yourAcceptheader.
Errors & rate limits
Standard HTTP status codes. Validation failures return field-level detail:
{
"status": "Failed",
"message": "The given data was invalid.",
"errors": {
"name": [
"The name field is required."
]
}
}
Business profile
The business your key belongs to — its legal name, TIN, VAT configuration, base currency, bank details and document defaults. Every other resource on this page is scoped underneath this one business; there is no "list businesses" endpoint since a key only ever sees its own.
/business
read the profile /business
update it — send only the fields you want to change | Field | Type | Description |
|---|---|---|
| name | string | Legal / trading name shown on documents. |
| tin | string, nullable | Tax Identification Number. |
| rc_no | string, nullable | Corporate Affairs Commission registration number. |
| address | string, nullable | Postal address, printed on documents. |
| phone | string, nullable | Business phone number. |
| string (email), nullable | Business contact email. | |
| vat_enabled | boolean | Whether VAT is applied to documents. |
| vat_rate | decimal, nullable | VAT percentage, e.g. 7.5. |
| base_currency | string (3 letters) | ISO code documents convert to for reporting, e.g. NGN. |
| bank_name | string, nullable | Bank name shown on documents. |
| bank_account_number | string, nullable | NUBAN shown on documents. |
| bank_account_name | string, nullable | Account name shown on documents. |
curl -X PUT https://cashflow.ng/api/v1/business \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Obi Trading Co","tin":"12345678-0001","vat_enabled":true}'
{
"status": "Successful",
"data": {
"id": "b1e2c3d4-0001-4a9b-8c7d-1f2e3d4c5b6a",
"name": "Obi Trading Co",
"code": "OTC",
"tin": "12345678-0001",
"vat_enabled": true,
"vat_rate": "7.50",
"base_currency": "NGN"
}
}
Customers
The people and companies you sell to. A customer can be attached to any sales document and to a price list, and can't be deleted once it has documents against it (edit or archive it instead).
/customers
?q=&per_page= — paginated /customers/{id}
/customers
/customers/{id}
/customers/{id}
blocked once documents exist | Field | Type | Description |
|---|---|---|
| display_name | string, nullable* | Name shown everywhere. *One of display_name / first_name+last_name / company_name is required. |
| first_name | string, nullable | Contact first name. |
| last_name | string, nullable | Contact last name. |
| company_name | string, nullable | Company name, if a business customer. |
| string (email), nullable | Used to prefill the email form when sending documents. | |
| phone | string, nullable | Primary phone number. |
| tin | string, nullable | Customer's Tax Identification Number. |
| industry | string, nullable | Free-text industry label. |
| address | string, nullable | Postal address. |
| country | string, nullable | Country name. |
| price_list_id | uuid, nullable | A price list this customer's documents should default to. |
curl https://cashflow.ng/api/v1/customers \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"display_name":"Ada Okafor","email":"ada@example.com","phone":"+2348012345678"}'
{
"status": "Successful",
"data": {
"id": "c1c2c3c4-0002-4a9b-8c7d-1f2e3d4c5b6a",
"display_name": "Ada Okafor",
"email": "ada@example.com",
"phone": "+2348012345678",
"price_list_id": null,
"created_at": "2026-09-13T10:04:00Z"
}
}
Vendors
The people and companies you buy from — attach a vendor to an expense, bill or purchase order.
/vendors
/vendors/{id}
/vendors
/vendors/{id}
/vendors/{id}
| Field | Type | Description |
|---|---|---|
| name | string | Required. Vendor / supplier name. |
| string (email), nullable | Vendor contact email. | |
| phone | string, nullable | Vendor phone number. |
| tin | string, nullable | Vendor's Tax Identification Number. |
| industry | string, nullable | Free-text industry label. |
| address | string, nullable | Postal address. |
| notes | string, nullable | Internal notes, not shown to the vendor. |
curl https://cashflow.ng/api/v1/vendors \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Kaduna Feed Mills Ltd","phone":"+2348098765432"}'
{
"status": "Successful",
"data": {
"id": "d1d2d3d4-0003-4a9b-8c7d-1f2e3d4c5b6a",
"name": "Kaduna Feed Mills Ltd",
"phone": "+2348098765432",
"created_at": "2026-09-13T10:05:00Z"
}
}
Items
Your product/service catalogue. An item can be tracked (carries stock, drawn down by documents and POS) or untracked (a service line with no stock). Composite (bundled) items and image uploads aren't available over the API yet — manage those from the app.
/items
?q=&per_page= — paginated /items/{id}
/items
/items/{id}
/items/{id}
curl https://cashflow.ng/api/v1/items \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"name": "Bag of rice (50kg)",
"default_unit_price": 65000,
"track_inventory": true,
"opening_stock": 20,
"barcode": "615600001234"
}'
{
"status": "Successful",
"data": {
"id": "e1e2e3e4-0004-4a9b-8c7d-1f2e3d4c5b6a",
"name": "Bag of rice (50kg)",
"default_unit_price": "65000.00",
"track_inventory": true,
"stock_on_hand": "20.00",
"barcode": "615600001234",
"category": null
}
}
Item categories
Simple named groups for items — powers the category tabs on the POS sell screen and filtering in the app.
/categories
/categories/{id}
/categories
/categories/{id}
/categories/{id}
blocked while items still use it curl https://cashflow.ng/api/v1/categories \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Beverages","sort_order":1}'
{
"status": "Successful",
"data": {
"id": "f1f2f3f4-0005-4a9b-8c7d-1f2e3d4c5b6a",
"name": "Beverages",
"sort_order": 1
}
}
Price lists
A named set of prices you assign to one or more customers: either fixed per-item
overrides, or a flat percentage off (or on top of) each item's default price. Assigning
one to a customer pre-fills their documents automatically.
/price-lists
/price-lists/{id}
/price-lists
/price-lists/{id}
/price-lists/{id}
unassigns it from any customer first | Field | Type | Description |
|---|---|---|
| name | string | Required. Price list name. |
| type | string: fixed|percentage | Required. Fixed per-item prices, or a percentage adjustment. |
| currency | string (3 letters), nullable | Currency the fixed prices are in. Defaults to the business's base currency. |
| adjustment | decimal, nullable | For type=percentage: -100 to 1000, e.g. -10 for 10% off. |
| is_active | boolean, nullable | Inactive lists are hidden from the customer picker. |
| prices | array, nullable | For type=fixed: a list of {item_id, unit_price} overrides. |
curl https://cashflow.ng/api/v1/price-lists \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"name": "Wholesale",
"type": "percentage",
"currency": "NGN",
"adjustment": -12
}'
{
"status": "Successful",
"data": {
"id": "a2a2a2a2-0006-4a9b-8c7d-1f2e3d4c5b6a",
"name": "Wholesale",
"type": "percentage",
"currency": "NGN",
"adjustment": "-12.00",
"is_active": true
}
}
Locations & warehouses
For businesses on the Business/Enterprise plan tracking stock per branch: a location is a branch/site, and each location holds one or more warehouses that items keep a separate stock-on-hand quantity in.
Locations
/locations
/locations/{id}
/locations
/locations/{id}
/locations/{id}
blocked while it has warehouses | Field | Type | Description |
|---|---|---|
| name | string | Required. Branch/location name. |
| code | string, nullable | Short internal code. |
| address | string, nullable | Physical address. |
| is_default | boolean, nullable | The location new warehouses default to. |
| is_active | boolean, nullable | Inactive locations are hidden from pickers. |
curl https://cashflow.ng/api/v1/locations \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Lagos Mainland","code":"LOS-1"}'
{
"status": "Successful",
"data": {
"id": "b3b3b3b3-0007-4a9b-8c7d-1f2e3d4c5b6a",
"name": "Lagos Mainland",
"code": "LOS-1",
"is_default": true
}
}
Warehouses
/warehouses
/warehouses
/warehouses/{id}
/warehouses/{id}
blocked while it still holds stock | Field | Type | Description |
|---|---|---|
| name | string | Required. Warehouse name. |
| location_id | uuid, nullable | Which location this warehouse belongs to. |
| code | string, nullable | Short internal code. |
| is_default | boolean, nullable | The warehouse opening stock lands in when a document/item doesn't say otherwise. |
| is_active | boolean, nullable | Inactive warehouses are hidden from pickers. |
curl https://cashflow.ng/api/v1/warehouses \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Main store","location_id":"LOCATION_ID","is_default":true}'
{
"status": "Successful",
"data": {
"id": "c4c4c4c4-0008-4a9b-8c7d-1f2e3d4c5b6a",
"name": "Main store",
"location_id": "b3b3b3b3-0007-4a9b-8c7d-1f2e3d4c5b6a",
"is_default": true
}
}
Stock adjustments
Manually correct an item's quantity — the same feed and action behind Inventory Management in the app. Every adjustment is logged with a reason and shows on the item's movement history.
/stock-adjustments
?item_id=&warehouse_id= /stock-adjustments
curl https://cashflow.ng/api/v1/stock-adjustments \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"item_id":"ITEM_ID","quantity":-3,"reason":"Damaged goods"}'
{
"status": "Successful",
"data": {
"id": "d5d5d5d5-0009-4a9b-8c7d-1f2e3d4c5b6a",
"item_id": "ITEM_ID",
"quantity": "-3.00",
"reason": "Damaged goods",
"created_at": "2026-09-13T10:12:00Z"
}
}
Sales documents
One endpoint covers all seven document types in the sales lifecycle — set type when
creating: quotation, sales_order, proforma_invoice,
invoice, receipt, credit_note, retainer_invoice.
VAT, discounts, multi-currency, numbering, shipping and stock draw-down all follow the exact same
rules as creating a document in the app. Converting a document to the next stage of the lifecycle
isn't available over the API yet.
/documents
?type=&status= — paginated /documents/{id}
/documents
/documents/{id}
/documents/{id}
/documents/{id}/email
email it to the customer /documents/{id}/submit-for-approval
start the approval chain, if one applies /documents/{id}/approve
approve at the current level /documents/{id}/reject
reject with a reason, back to draft | Field | Type | Description |
|---|---|---|
| type | string | Required on create. One of the seven document types listed above. |
| customer_id | uuid, nullable | The customer this document is for. |
| issue_date | date | Required. YYYY-MM-DD. |
| due_date | date, nullable | For invoices/proforma — used by payment reminders. |
| currency | string (3 letters), nullable | Defaults to the business's base currency. |
| exchange_rate | decimal, nullable | Required if currency ≠ base currency. |
| lines | array | Required, at least one. Each: {item_id?, description, qty, unit_price, taxable?}. |
| discount_type | string: percent|flat, nullable | How discount_value is applied. |
| discount_value | decimal, nullable | The discount amount or percentage. |
| shipping_fee | decimal, nullable | Added to the total after VAT. Optional — omit or send 0 for no shipping charge. |
| shipping | object, nullable | Optional shipping details — see the dedicated example below. Sent as a whole, stored as one JSON object; omit entirely for a document with no shipping. |
curl https://cashflow.ng/api/v1/documents \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"type": "invoice",
"customer_id": "CUSTOMER_ID",
"issue_date": "2026-09-13",
"due_date": "2026-10-13",
"lines": [
{"description": "Website design", "qty": 1, "unit_price": 250000},
{"description": "Hosting (annual)", "qty": 1, "unit_price": 40000, "taxable": false}
]
}'
{
"status": "Successful",
"data": {
"id": "e6e6e6e6-0010-4a9b-8c7d-1f2e3d4c5b6a",
"type": "invoice",
"document_no": "OINL/INV/2026/0042",
"status": "draft",
"subtotal": "290000.00",
"vat_amount": "18750.00",
"shipping_fee": "0.00",
"total": "308750.00",
"currency": "NGN"
}
}
Shipping
For a merchant that ships physical items — entirely optional, and unrelated to whether the document
gets emailed or approved. shipping_fee is a real money field that adds to the total;
everything else is free-form shipment detail with no effect on totals. carrier is a
free string, not a fixed enum — the app's own picker suggests
GIG Logistics, Libmot Express, Red Star Express, ABC Cargo Express, Kwik Delivery, Sendbox, DHL Express, UPS, FedEx, Bolt, Konga Express, Speedaf, but any name works.
curl https://cashflow.ng/api/v1/documents \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"type": "invoice",
"customer_id": "CUSTOMER_ID",
"issue_date": "2026-09-15",
"lines": [{"description": "Bag of rice (50kg)", "qty": 1, "unit_price": 65000}],
"shipping_fee": 3500,
"shipping": {
"carrier": "gig",
"method": "express",
"tracking_number": "GIG-90234",
"recipient_name": "Ada Okafor",
"recipient_phone": "+2348012345678",
"address_line1": "12 Allen Avenue",
"city": "Ikeja",
"state": "Lagos",
"weight_kg": 5
}
}'
{
"status": "Successful",
"data": {
"id": "a1a1a1a1-0025-4a9b-8c7d-1f2e3d4c5b6a",
"type": "invoice",
"document_no": "OINL/INV/2026/0043",
"subtotal": "65000.00",
"shipping_fee": "3500.00",
"total": "68500.00",
"currency": "NGN",
"shipping_details": {
"carrier": "gig",
"carrier_name": "GIG Logistics",
"method": "express",
"tracking_number": "GIG-90234",
"tracking_url": null,
"recipient_name": "Ada Okafor",
"recipient_phone": "+2348012345678",
"address_line1": "12 Allen Avenue",
"address_line2": null,
"city": "Ikeja",
"state": "Lagos",
"country": "Nigeria",
"postal_code": null,
"weight_kg": 5,
"estimated_delivery_date": null,
"notes": null
}
}
}
Email, approve, reject
/email is refused with a 422 until the document is approved (or no approval rule
applies to it) — same rule as the app. /approve and /reject act as the
API key's business owner, so they only succeed if the owner is actually named as an approver at the
document's current level; a business that routes approvals to other team members has to use the app
for those two.
| Field | Type | Description |
|---|---|---|
| to | string (email), nullable | Defaults to the customer on the document, if any. |
| cc | array of email, nullable | |
| message | string, nullable | A short note included in the email body. |
{
"status": "Successful",
"data": {
"id": "e6e6e6e6-0010-4a9b-8c7d-1f2e3d4c5b6a",
"approval_status": "pending",
"current_approval_level": 1
}
}
Recurring invoices
A schedule that issues a real invoice on a cadence — weekly, monthly, quarterly or yearly — from a saved line-item template, rather than a document type of its own. The daily scheduler backfills any missed run and stops itself once the end condition is reached.
/recurring-invoices
/recurring-invoices/{id}
/recurring-invoices
/recurring-invoices/{id}
/recurring-invoices/{id}
invoices already generated are kept /recurring-invoices/{id}/pause
/recurring-invoices/{id}/resume
/recurring-invoices/{id}/run
generate the next invoice immediately | Field | Type | Description |
|---|---|---|
| title | string | Required. Internal name for the schedule. |
| customer_id | uuid, nullable | Customer each generated invoice is billed to. |
| frequency | string: weekly|monthly|quarterly|yearly | Required. |
| interval_count | integer (1-60) | Required. Every N units of frequency. |
| start_date | date | Required. First run date. |
| end_mode | string: never|on_date|after | Required. How the schedule ends. |
| end_date | date, nullable | Required if end_mode=on_date. |
| max_occurrences | integer, nullable | Required if end_mode=after. |
| auto_email | boolean, nullable | Email the customer automatically on each run. |
| lines | array | Required, at least one described line — same shape as a document line. |
curl https://cashflow.ng/api/v1/recurring-invoices \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"title": "Monthly retainer — Ada Okafor",
"customer_id": "CUSTOMER_ID",
"frequency": "monthly",
"interval_count": 1,
"start_date": "2026-10-01",
"end_mode": "never",
"lines": [{"description": "Retainer", "qty": 1, "unit_price": 150000}]
}'
{
"status": "Successful",
"data": {
"id": "f7f7f7f7-0011-4a9b-8c7d-1f2e3d4c5b6a",
"title": "Monthly retainer — Ada Okafor",
"frequency": "monthly",
"interval_count": 1,
"next_run_date": "2026-10-01",
"status": "active",
"occurrences_generated": 0
}
}
Payments
A payment recorded against a sales document. Recording one recomputes that document's status
(draft → sent → partial → paid) automatically — there's no separate "mark as paid" call.
/payments
?document_id= — paginated, read-only /payments/{id}
/documents/{id}/payments
record a new payment curl https://cashflow.ng/api/v1/documents/DOCUMENT_ID/payments \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"amount":308750,"paid_on":"2026-09-13","method":"transfer","reference":"TRX-9284"}'
{
"status": "Successful",
"data": {
"id": "a8a8a8a8-0012-4a9b-8c7d-1f2e3d4c5b6a",
"document_id": "DOCUMENT_ID",
"amount": "308750.00",
"method": "transfer",
"reference": "TRX-9284",
"paid_on": "2026-09-13"
}
}
Expenses
A quick single-line spend against an expense account — optionally paid straight from a bank account.
/expenses
/expenses/{id}
/expenses
/expenses/{id}
/expenses/{id}
| Field | Type | Description |
|---|---|---|
| date | date | Required. Expense date. |
| ledger_account_id | uuid | Required. Which expense account this hits. |
| amount | decimal | Required. Amount spent. |
| description | string, nullable | What the expense was for. |
| vendor_id | uuid, nullable | Vendor this was paid to. |
| bank_account_id | uuid, nullable | Pay it straight from this account — marks it paid immediately. |
curl https://cashflow.ng/api/v1/expenses \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"date":"2026-09-13","ledger_account_id":"ACCOUNT_ID","amount":15000,"description":"Fuel"}'
{
"status": "Successful",
"data": {
"id": "b9b9b9b9-0013-4a9b-8c7d-1f2e3d4c5b6a",
"reference": "EXP/2026/0031",
"amount": "15000.00",
"description": "Fuel",
"status": "paid"
}
}
Bills
A multi-line vendor invoice. A bill only hits payables once opened — create it as a draft first, then open it, then record payments against it as they happen.
/bills
?status= /bills/{id}
/bills
/bills/{id}
/bills/{id}
/bills/{id}/open
move draft → open, posts to payables /bills/{id}/payments
record a payment | Field | Type | Description |
|---|---|---|
| vendor_id | uuid | Required. |
| date | date | Required. Bill date. |
| due_date | date, nullable | Payment due date. |
| lines | array | Required, at least one: {ledger_account_id, description, qty, unit_price, taxable?}. |
curl https://cashflow.ng/api/v1/bills \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"vendor_id": "VENDOR_ID",
"date": "2026-09-13",
"lines": [{"description": "Generator servicing", "qty": 1, "unit_price": 45000}]
}'
{
"status": "Successful",
"data": {
"id": "c0c0c0c0-0014-4a9b-8c7d-1f2e3d4c5b6a",
"reference": "BILL/2026/0018",
"status": "draft",
"total": "45000.00"
}
}
Vendor payments
A read-only list of payments recorded against bills. To record a new one, use POST /bills/{id}/payments above.
/vendor-payments
?bill_id= — paginated /vendor-payments/{id}
curl https://cashflow.ng/api/v1/vendor-payments/PAYMENT_ID \
-H "Authorization: Bearer YOUR_API_KEY"
{
"status": "Successful",
"data": {
"id": "d1d1d1d1-0015-4a9b-8c7d-1f2e3d4c5b6a",
"bill_id": "BILL_ID",
"amount": "45000.00",
"method": "transfer",
"paid_on": "2026-09-14"
}
}
Purchase orders
Order stock from a vendor, then receive it — partially or fully — into a warehouse or main stock. A receiving line can optionally capture a batch number, expiry date and cost price, which flows straight into POS's expiry-aware stock draw-down.
/purchase-orders
/purchase-orders/{id}
/purchase-orders
/purchase-orders/{id}
/purchase-orders/{id}
draft only /purchase-orders/{id}/send
draft → sent /purchase-orders/{id}/receive
record quantities received | Field | Type | Description |
|---|---|---|
| vendor_id | uuid | Required. |
| date | date | Required. Order date. |
| expected_date | date, nullable | Expected delivery date. |
| currency | string (3 letters), nullable | Defaults to the business's base currency. |
| exchange_rate | decimal, nullable | Rate to the base currency. Defaults to 1 (ignored when currency is the base currency). |
| lines | array | Required: {item_id, description, qty, unit_price}. |
curl https://cashflow.ng/api/v1/purchase-orders \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"vendor_id": "VENDOR_ID", "date": "2026-09-13", "currency": "NGN", "exchange_rate": 1,
"lines": [{"item_id": "ITEM_ID", "description": "Bag of rice (50kg)", "qty": 100, "unit_price": 58000}]
}'
{
"status": "Successful",
"data": {
"id": "e2e2e2e2-0016-4a9b-8c7d-1f2e3d4c5b6a",
"reference": "PO/2026/0009",
"status": "draft",
"total": "5800000.00"
}
}
/receive optionally accepts batch_no, expiry_date and cost_price per line.
Purchase returns
Send stock back to a vendor — draft, then complete it to reduce stock and post the accounting entries, or cancel it.
/purchase-returns
/purchase-returns/{id}
/purchase-returns
/purchase-returns/{id}
draft only /purchase-returns/{id}
/purchase-returns/{id}/complete
reduces stock, posts to the ledger /purchase-returns/{id}/cancel
restores stock if already completed | Field | Type | Description |
|---|---|---|
| vendor_id | uuid | Required. |
| date | date | Required. |
| bill_id | uuid, nullable | The bill this return relates to, if any. |
| purchase_order_id | uuid, nullable | The purchase order this return relates to, if any. |
| reason | string, nullable | Why the stock is being returned. |
curl https://cashflow.ng/api/v1/purchase-returns \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"vendor_id":"VENDOR_ID","date":"2026-09-14","reason":"Wrong item delivered"}'
{
"status": "Successful",
"data": {
"id": "f3f3f3f3-0017-4a9b-8c7d-1f2e3d4c5b6a",
"reference": "PRET/2026/0004",
"status": "draft"
}
}
Recurring bills
Mirrors recurring invoices, for bills — rent, subscriptions or any vendor charge that repeats on a schedule.
/recurring-bills
/recurring-bills/{id}
/recurring-bills
/recurring-bills/{id}
/recurring-bills/{id}
bills already generated are kept /recurring-bills/{id}/pause
/recurring-bills/{id}/resume
/recurring-bills/{id}/run
generate the next bill immediately | Field | Type | Description |
|---|---|---|
| vendor_id | uuid | Required. |
| title | string | Required. Internal name for the schedule. |
| frequency | string: weekly|monthly|quarterly|yearly | Required. |
| interval_count | integer | Required. Every N units of frequency. |
| start_date | date | Required. |
| auto_open | boolean, nullable | Open each generated bill automatically. |
| due_days | integer, nullable | Days after issue the bill is due. |
curl https://cashflow.ng/api/v1/recurring-bills \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"vendor_id":"VENDOR_ID","title":"Office rent","frequency":"monthly","interval_count":1,"start_date":"2026-10-01","auto_open":true}'
{
"status": "Successful",
"data": {
"id": "a4a4a4a4-0018-4a9b-8c7d-1f2e3d4c5b6a",
"title": "Office rent",
"frequency": "monthly",
"status": "active",
"next_run_date": "2026-10-01"
}
}
Registers
A POS till attached to a store. Optionally restrict which team members may open a shift or check out on it.
/registers
/registers/{id}
/registers
/registers/{id}
/registers/{id}
blocked once it has shift history curl https://cashflow.ng/api/v1/registers \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"store_id":"STORE_ID","name":"Till 1"}'
{
"status": "Successful",
"data": {
"id": "b5b5b5b5-0019-4a9b-8c7d-1f2e3d4c5b6a",
"store_id": "STORE_ID",
"name": "Till 1",
"is_active": true
}
}
Bank accounts, transactions & transfers
Your business's own current, checking, savings, domiciliary, fixed-deposit, wallet or cash accounts, each with a running-balance ledger of manual credit/debit entries, categorized by type (customer payment, expense, vendor payment, and more), plus transfers between two of your own accounts.
Bank accounts
/bank-accounts
/bank-accounts/{id}
/bank-accounts
/bank-accounts/{id}
/bank-accounts/{id}
blocked once it has transactions — mark inactive instead | Field | Type | Description |
|---|---|---|
| name | string | Required. Account label. |
| account_type | string | Required. One of current, checking, savings, domiciliary, fixed_deposit, wallet, cash. |
| bank_name | string, nullable | Bank name. |
| account_number | string, nullable | 10-digit NUBAN (not required/validated for checking, wallet or cash). |
| currency | string (3 letters), nullable | Defaults to the business's base currency. |
| opening_balance | decimal, nullable | Starting balance. |
| opening_balance_date | date, nullable | Date the opening balance is as of. |
| sms_alerts | boolean, nullable | Auto-post the ₦4 SMS-alert fee per transaction (NGN accounts). Defaults true. |
| emtl_charges | boolean, nullable | Auto-post the ₦50 EMTL stamp duty on debit transfers of ₦10,000+ (NGN accounts). Defaults true. |
| is_active | boolean, nullable | Defaults true. |
curl https://cashflow.ng/api/v1/bank-accounts \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"GTBank — Main","account_type":"current","currency":"NGN","account_number":"0123456789"}'
{
"status": "Successful",
"data": {
"id": "c6c6c6c6-0020-4a9b-8c7d-1f2e3d4c5b6a",
"name": "GTBank — Main",
"account_type": "current",
"currency": "NGN",
"account_number": "0123456789",
"current_balance": "0.00"
}
}
Transactions
/bank-transactions
?bank_account_id=&direction=&transaction_type=&from=&to= /bank-transactions/{id}
/bank-transactions
/bank-transactions/{id}
blocked for transfer/payment/charge-linked entries | Field | Type | Description |
|---|---|---|
| bank_account_id | uuid | Required. |
| transaction_type | string, nullable | One of customer_payment, sales_without_document, interest_income, other_income, owner_contribution, loan_received, refund_received, expense, vendor_advance, vendor_payment, sales_return, employee_reimbursement, owner_drawings, loan_repayment, tax_payment, bank_charge, other. Most of these fix the direction for you — see below. |
| direction | string: credit|debit, nullable | Required unless transaction_type already implies a direction (only customer_payment, expense, vendor_payment, etc. do — other and an omitted transaction_type both need it explicit). |
| amount | decimal | Required, min 0.01. |
| transaction_date | date | Required. |
| narration | string | Required. What the entry was for. |
| channel | string, nullable | e.g. transfer, pos, atm, ussd, cheque, cash, web. |
| reference | string, nullable | Bank reference. |
curl https://cashflow.ng/api/v1/bank-transactions \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"bank_account_id":"ACCOUNT_ID","transaction_type":"customer_payment","amount":50000,"transaction_date":"2026-09-13","narration":"Ada Ltd payment"}'
{
"status": "Successful",
"data": {
"id": "d7d7d7d7-0021-4a9b-8c7d-1f2e3d4c5b6a",
"direction": "credit",
"transaction_type": "customer_payment",
"amount": "50000.00",
"balance_after": "50000.00",
"narration": "Cash deposit"
}
}
Transfers
Moves money between two of the business's own accounts as one linked pair of entries.
/bank-transfers
/bank-transfers/{transfer_id}
reverses both legs (and any charges they triggered) | Field | Type | Description |
|---|---|---|
| from_account_id | uuid | Required. Must differ from to_account_id. |
| to_account_id | uuid | Required. |
| amount | decimal | Required, min 0.01. Amount sent, in the from account's currency. |
| date | date | Required. |
| converted_amount | decimal, nullable | Required if the two accounts use different currencies — amount received, in the to account's currency. |
| narration | string, nullable | |
| reference | string, nullable |
curl https://cashflow.ng/api/v1/bank-transfers \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"from_account_id":"FROM_ACCOUNT_ID","to_account_id":"TO_ACCOUNT_ID","amount":100000,"date":"2026-09-13","narration":"Fund payroll account"}'
{
"status": "Successful",
"data": {
"transfer_id": "e8e8e8e8-0022-4a9b-8c7d-1f2e3d4c5b6a",
"from_account": {
"id": "c6c6c6c6-0020-4a9b-8c7d-1f2e3d4c5b6a",
"current_balance": "150000.00"
},
"to_account": {
"id": "f9f9f9f9-0023-4a9b-8c7d-1f2e3d4c5b6a",
"current_balance": "50000.00"
}
}
}
Chart of accounts
The general ledger's account list — seeded with a Nigerian default on business creation. Core accounts (VAT, AR, sales) can't be deleted, only deactivated.
/chart-of-accounts
/chart-of-accounts/{id}
/chart-of-accounts/trial-balance
?as_of= /chart-of-accounts
/chart-of-accounts/{id}
/chart-of-accounts/{id}
blocked for core or used accounts curl https://cashflow.ng/api/v1/chart-of-accounts \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"code":"5310","name":"Fuel & transport","type":"expense"}'
{
"status": "Successful",
"data": {
"id": "e8e8e8e8-0022-4a9b-8c7d-1f2e3d4c5b6a",
"code": "5310",
"name": "Fuel & transport",
"type": "expense",
"is_active": true
}
}
Journals
A manual double-entry journal — must balance, saved as a draft then posted. A posted journal is immutable and can only be reversed (posts a mirror entry).
/journals
/journals/{id}
/journals
/journals/{id}
draft only /journals/{id}
draft only /journals/{id}/post
lock it in — must balance /journals/{id}/reverse
posted only — creates a mirror journal | Field | Type | Description |
|---|---|---|
| date | date | Required. |
| narration | string, nullable | What this journal records. |
| reference | string, nullable | Auto-generated if omitted. |
| lines | array | Required, at least 2, must balance: {ledger_account_id, debit, credit, description?}. |
curl https://cashflow.ng/api/v1/journals \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{
"date": "2026-09-13", "narration": "Owner capital injection",
"lines": [
{"ledger_account_id": "BANK_ACCOUNT_LEDGER_ID", "debit": 500000, "credit": 0},
{"ledger_account_id": "CAPITAL_ACCOUNT_ID", "debit": 0, "credit": 500000}
]
}'
{
"status": "Successful",
"data": {
"id": "f9f9f9f9-0023-4a9b-8c7d-1f2e3d4c5b6a",
"reference": "JN/2026/0067",
"status": "draft",
"narration": "Owner capital injection"
}
}
Currency adjustments
A guided FX revaluation — give the old and new exchange rate for a foreign-currency balance and a gain/loss journal is posted automatically.
/currency-adjustments
/currency-adjustments/{id}
/currency-adjustments
/currency-adjustments/{id}
| Field | Type | Description |
|---|---|---|
| ledger_account_id | uuid | Required. The foreign-currency asset/liability account. |
| date | date | Required. |
| currency | string (3 letters) | Required. The foreign currency being revalued. |
| foreign_balance | decimal | Required. The balance in that foreign currency. |
| old_rate | decimal | Required, greater than 0. The rate it was booked at. |
| new_rate | decimal | Required, greater than 0. Today's rate. |
curl https://cashflow.ng/api/v1/currency-adjustments \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"ledger_account_id":"ACCOUNT_ID","date":"2026-09-13","currency":"USD","foreign_balance":10000,"old_rate":1500,"new_rate":1620}'
{
"status": "Successful",
"data": {
"id": "a0a0a0a0-0024-4a9b-8c7d-1f2e3d4c5b6a",
"currency": "USD",
"old_rate": "1500.00",
"new_rate": "1620.00",
"gain_loss": "1200000.00"
}
}
Currencies
NGN, USD, EUR, GBP, GHS and XOF are built in. Add your own if a customer needs one that isn't.
/currencies
/currencies
add a custom currency /currencies/{code}
custom currencies only, and never the base currency curl https://cashflow.ng/api/v1/currencies \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"code":"ZAR","name":"South African Rand","symbol":"R","unit":"Rand","subunit":"Cent"}'
{
"status": "Successful",
"data": {
"code": "ZAR",
"name": "South African Rand",
"symbol": "R",
"is_base": false,
"is_custom": true
}
}
Team members
List, invite, re-role and remove teammates on this business — the same actions available from the Team page in the app.
/users
lists members + pending invitations /users
invite an existing user or a new email /users/{id}
change a member's role /users/{id}
remove a member — a business must keep at least one owner curl https://cashflow.ng/api/v1/users \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"email":"new-hire@example.com","role":"accountant"}'
{
"status": "Successful",
"message": "Invitation sent to new-hire@example.com."
}
Seat limits still apply — inviting past your plan's user cap returns a 422.