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."
]
}
}
Plain text only. Every field is plain text — a value containing HTML tags, a javascript: or data:text/html link, an inline event handler or invisible control characters is refused with a 422 naming the field (e.g. lines.1.description). Quotes, apostrophes and words like SELECT or DROP are fine: values are always stored as data, never run as SQL. In search parameters (?q=) % and _ match literally.
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"
}
}
Contact persons
The people you deal with at a customer or vendor. One can be primary — document emails go to them when the customer has no email of its own, and the others are offered as CC.
/customers/{id}/contact-persons
/customers/{id}/contact-persons
/vendors/{id}/contact-persons
/vendors/{id}/contact-persons
/contact-persons/{id}
/contact-persons/{id}
/contact-persons/{id}
| Field | Type | Description |
|---|---|---|
| first_name | string | Required. |
| last_name | string, nullable | |
| salutation | string, nullable | Mr, Mrs, Dr… |
| string (email), nullable | ||
| phone / mobile | string, nullable | |
| designation / department | string, nullable | |
| is_primary | boolean | Making one primary clears the flag on the others. |
curl https://cashflow.ng/api/v1/customers/CUSTOMER_ID/contact-persons \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"first_name":"Chidi","last_name":"Okafor","email":"chidi@adastores.ng","designation":"Accounts","is_primary":true}'
{
"status": "Successful",
"data": {
"id": "c1c1c1c1-0031-4a9b-8c7d-1f2e3d4c5b6a",
"first_name": "Chidi",
"last_name": "Okafor",
"email": "chidi@adastores.ng",
"is_primary": true
}
}
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). A composite (bundle) item is sold as one line but draws its components out of stock; it holds no stock of its own, and responses include buildable_quantity — how many current stock of the parts can make. Create/update accept an image upload (multipart/form-data, not JSON) — jpg, jpeg, png or svg, up to 4MB.
/items
?q=&is_composite=&per_page= — paginated
/items/{id}
/items
/items/{id}
/items/{id}
| Field | Type | Description |
|---|---|---|
| name | string | Required. Item name. |
| default_unit_price | decimal | Selling price used when no price list applies. |
| track_inventory | boolean | True to draw stock down on documents/POS. |
| opening_stock | decimal, nullable | Starting quantity — only used once, on create. |
| reorder_level | decimal, nullable | Triggers the low-stock badge/digest below this quantity. |
| category_id | uuid, nullable | Links to an item category (see below). |
| unit_id | uuid, nullable | A label-only unit of measure, e.g. “kg”, “carton”. |
| barcode | string, nullable | Scanned at the POS sell screen. |
| warehouse_id | uuid, nullable | Which warehouse opening_stock lands in, if the business uses warehouses. |
| image | file, nullable | jpg, jpeg, png or svg, up to 4MB. Send as multipart/form-data, not JSON. |
| is_composite | boolean | True for a bundle. Forces track_inventory off. |
| components | array, nullable | Bundle parts: [{"item_id": uuid, "quantity": decimal}]. Your own non-composite items only. Sending it replaces the parts. |
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
| Field | Type | Description |
|---|---|---|
| name | string | Required. Category name, e.g. “Beverages”. |
| sort_order | integer, nullable | Controls display order — lower shows first. |
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
| Field | Type | Description |
|---|---|---|
| item_id | uuid | Required. The item being adjusted. |
| warehouse_id | uuid, nullable | Move that warehouse's stock together with the item's total. |
| quantity | decimal | Required, non-zero. Positive adds stock, negative removes it. |
| reason | string | Required. Short free-text reason, e.g. “Damaged goods”. |
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. POST /documents/{id}/convert moves a document to the next stage — quotation → sales order / proforma / invoice, sales order → proforma / invoice, proforma → invoice, invoice → receipt / credit note — as a new draft with a fresh number, exactly like the app's Convert button.
/documents
?type=&status= — paginated
/documents/{id}
/documents
/documents/{id}
/documents/{id}
/documents/{id}/email
email it to the customer
/documents/{id}/convert
body {"type": "invoice"} — returns the new document (201); 422 lists convertible_to
/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.
| Field | Type | Description |
|---|---|---|
| shipping.carrier | string, nullable | A carrier slug (e.g. “gig”, “dhl”) or any free-text name. |
| shipping.carrier_name | string, nullable | Display name. Auto-filled from carrier when it matches a known slug. |
| shipping.method | string, nullable | Service level, e.g. “standard”, “express”, “same_day”, “pickup”. |
| shipping.tracking_number | string, nullable | |
| shipping.tracking_url | string (url), nullable | Link to the carrier's own tracking page. |
| shipping.recipient_name | string, nullable | |
| shipping.recipient_phone | string, nullable | |
| shipping.address_line1 | string, nullable | |
| shipping.address_line2 | string, nullable | |
| shipping.city | string, nullable | |
| shipping.state | string, nullable | |
| shipping.country | string, nullable | Defaults to “Nigeria” once any shipping field is set. |
| shipping.postal_code | string, nullable | |
| shipping.weight_kg | decimal, nullable | |
| shipping.estimated_delivery_date | date, nullable | |
| shipping.notes | string, nullable |
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.
curl https://cashflow.ng/api/v1/documents/DOCUMENT_ID/email \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{}'
{
"status": "Successful",
"message": "Invoice OINL/INV/2026/0042 emailed to ada@example.com."
}
| 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
}
}
curl https://cashflow.ng/api/v1/documents/DOCUMENT_ID/reject \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"note":"Wrong amount"}'
{
"status": "Successful",
"data": {
"id": "e6e6e6e6-0010-4a9b-8c7d-1f2e3d4c5b6a",
"approval_status": "not_submitted",
"current_approval_level": null
}
}
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
| Field | Type | Description |
|---|---|---|
| amount | decimal | Required, min 0.01. Amount received. |
| paid_on | date | Required. When the payment was received. |
| method | string, nullable | e.g. “transfer”, “cash”, “card”. |
| reference | string, nullable | Bank/transaction reference. |
| note | string, nullable | Internal note. |
| bank_account_id | uuid, nullable | Post the amount into a bank account in the same currency as the document. |
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"
}
}
Send on WhatsApp
Send a document to a customer on WhatsApp from the business's connected WhatsApp Business number (Admin console ▸ WhatsApp, paid plans). The message is queued and sent in the background. Its status moves from queued to sent, delivered, read or failed.
/documents/{id}/whatsapp
202 — queued
| Field | Type | Description |
|---|---|---|
| phone | string | The customer's WhatsApp number, e.g. 0803 123 4567 or +2348031234567. Defaults to the phone on the document's customer. |
curl https://cashflow.ng/api/v1/documents/DOCUMENT_ID/whatsapp \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"phone":"08031234567"}'
{
"status": "Successful",
"message": "Invoice ADA/INV/2026/0042 is on its way on WhatsApp to +2348031234567.",
"whatsapp_message": {
"id": "9d1c6f0e-6b8a-4a1f-9a7e-2f4b1c3d5e6f",
"phone": "2348031234567",
"status": "queued"
}
}
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"
}
}
Recurring expenses
Expenses that repeat — rent, diesel, internet. Each run records a normal expense (and a bank debit when a bank account is set) on schedule.
/recurring-expenses
/recurring-expenses/{id}
/recurring-expenses
/recurring-expenses/{id}
partial updates allowed
/recurring-expenses/{id}
expenses already recorded are kept
/recurring-expenses/{id}/pause
/recurring-expenses/{id}/resume
/recurring-expenses/{id}/run
record the next expense now
| Field | Type | Description |
|---|---|---|
| title | string | Required. |
| amount | number | Required. |
| ledger_account_id | uuid, nullable | Expense account. |
| bank_account_id | uuid, nullable | Pay from this account. |
| vendor_id | uuid, nullable | |
| frequency | string: daily|weekly|monthly|quarterly|yearly | Required. |
| interval_count | integer | Every N units. Defaults to 1. |
| start_date | date | Required. |
| end_date / max_occurrences | date / integer, nullable | When to stop. |
curl https://cashflow.ng/api/v1/recurring-expenses \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"title":"Diesel for generator","amount":150000,"ledger_account_id":"ACCOUNT_ID","bank_account_id":"BANK_ID","frequency":"weekly","start_date":"2026-10-05"}'
{
"status": "Successful",
"data": {
"id": "e2e2e2e2-0032-4a9b-8c7d-1f2e3d4c5b6a",
"title": "Diesel for generator",
"frequency": "weekly",
"status": "active",
"next_run_date": "2026-10-05"
}
}
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"
}
}
Vendor credits
Credit a supplier owes you — returns, overcharges, short deliveries. Apply it to open bills or record a cash refund.
/vendor-credits
/vendor-credits/{id}
/vendor-credits
/vendor-credits/{id}
only while nothing is applied
/vendor-credits/{id}
only while nothing is applied
/vendor-credits/{id}/apply
{“allocations”: {“BILL_ID”: 25000}}
/vendor-credits/{id}/refund
amount, date, bank_account_id
/vendor-credits/{id}/void
reverses applications and refunds
| Field | Type | Description |
|---|---|---|
| vendor_id | uuid | Required. |
| bill_id | uuid, nullable | The bill it relates to. |
| date | date | Required. |
| vendor_reference | string, nullable | The supplier’s credit note number. |
| lines | array | Required. Each: description, ledger_account_id or item_id, qty, unit_price, taxable. |
curl https://cashflow.ng/api/v1/vendor-credits \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"vendor_id":"VENDOR_ID","bill_id":"BILL_ID","date":"2026-10-02","lines":[{"description":"Short delivery — 10 bags","ledger_account_id":"ACCOUNT_ID","qty":10,"unit_price":5800}]}'
{
"status": "Successful",
"data": {
"id": "f3f3f3f3-0033-4a9b-8c7d-1f2e3d4c5b6a",
"credit_no": "VC-0001",
"total": 58000,
"status": "open"
}
}
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 register (checkout counter) 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
| Field | Type | Description |
|---|---|---|
| store_id | uuid | Required. The store this register belongs to. |
| name | string | Required. Register name, e.g. “Counter 1”. |
| allowed_user_ids | array of uuid, nullable | Restrict this register to specific team members — omit to allow everyone. |
curl https://cashflow.ng/api/v1/registers \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"store_id":"STORE_ID","name":"Counter 1"}'
{
"status": "Successful",
"data": {
"id": "b5b5b5b5-0019-4a9b-8c7d-1f2e3d4c5b6a",
"store_id": "STORE_ID",
"name": "Counter 1",
"is_active": true
}
}
POS sales
Ring up a sale exactly as the POS sell screen does: batches and stock drawn down, loyalty points, split tender across your payment methods, and a paid receipt in Sales. Line description and unit_price default from the item. Send an idempotency_key per sale — retrying with the same key returns the original sale (200) instead of charging twice. With a register_id, that register needs an open shift. Enterprise plan.
/pos-sales
?store_id=®ister_id=&status=&date= — paginated
/pos-sales/{id}
with lines, tenders and the receipt
/pos-sales
ring up a sale (201; 200 on an idempotent replay)
/pos-sales/{id}/void
reverse stock, loyalty, the receipt and its payments
| Field | Type | Description |
|---|---|---|
| store_id | uuid | Required. Your store. |
| register_id | uuid, nullable | A register in that store with an open shift. |
| customer_id | uuid, nullable | For loyalty points and the receipt. |
| lines | array | Required. [{item_id?, description?, qty?, unit_price?, discount_amount?, taxable?, item_batch_id?}] |
| payments | array | Required. [{payment_method_id | method (slug, e.g. cash), amount?, reference?}] — one entry pays the whole sale; several split it. |
| amount_tendered | decimal, nullable | Cash handed over, for change_due. |
| discount_type / discount_value | flat|percent, decimal | Whole-sale discount. |
| redeem_points | integer, nullable | Loyalty points to redeem. |
| idempotency_key | string, nullable | Up to 64 chars — strongly recommended. |
curl https://cashflow.ng/api/v1/pos-sales \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"store_id":"STORE_ID","idempotency_key":"till1-000123","lines":[{"item_id":"ITEM_ID","qty":3}],"payments":[{"method":"cash"}],"amount_tendered":2000}'
{
"status": "Successful",
"data": {
"id": "f6f6f6f6-0021-4a9b-8c7d-1f2e3d4c5b6a",
"sale_no": "POS-000123",
"status": "completed",
"total": "1612.50",
"change_due": "387.50",
"receipt": {
"id": "DOCUMENT_ID",
"document_no": "ACME/REC/2026/0042"
}
}
}
Shopify & WooCommerce
Connected Shopify and WooCommerce stores. Read each connection's status and recent runs, change what syncs, or start a sync. A store is connected from the app (POS → Shopify & WooCommerce); its keys never appear in the API.
/integrations
/integrations/{id}
includes the last 10 runs
/integrations/{id}
what syncs
/integrations/{id}/sync
queues a run — 202
| Field | Type | Description |
|---|---|---|
| sync_items, sync_prices, sync_inventory, sync_orders, sync_customers | boolean | Turn each kind of record on or off. |
| push_new_items | boolean | Also create your items in the store. |
| catalog_source | string | <code>remote</code> (names and prices come from the store) or <code>local</code> (prices set here go to the store). |
| is_active | boolean | Pause or resume syncing. |
| steps | array, sync only | Run only some of <code>items</code>, <code>customers</code>, <code>orders</code>, <code>prices</code>, <code>inventory</code>. Omit to run everything that's switched on. |
curl https://cashflow.ng/api/v1/integrations/CONNECTION_ID/sync \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"steps":["orders","inventory"]}'
{
"status": "Successful",
"message": "Sync queued for Ada Bags."
}
curl https://cashflow.ng/api/v1/integrations \
-H "Authorization: Bearer YOUR_API_KEY"
{
"status": "Successful",
"data": [
{
"id": "9d1c6f0e-6b8a-4a1f-9a7e-2f4b1c3d5e6f",
"platform": "woocommerce",
"store_url": "https://shop.example.com",
"store_name": "Ada Bags",
"store_currency": "NGN",
"catalog_source": "remote",
"sync_items": true,
"sync_prices": true,
"sync_inventory": true,
"sync_orders": true,
"sync_customers": true,
"push_new_items": false,
"is_active": true,
"webhooks_registered": true,
"last_synced_at": "2026-10-12T09:15:02+01:00",
"last_sync_status": "success",
"last_error": null
}
]
}
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"
}
}
}
Bank rules
Auto-categorise bank lines. Rules run in priority order on statement import; the first match sets the transaction type (and optionally renames the line). Lines that already have a type are never changed.
/bank-rules
/bank-rules/{id}
/bank-rules
/bank-rules/{id}
/bank-rules/{id}
/bank-rules/apply
run rules over uncategorised lines; optional bank_account_id
| Field | Type | Description |
|---|---|---|
| name | string | Required. |
| direction | string: credit|debit|any | Required. |
| match | string: all|any | Required. All conditions, or any one. |
| conditions | array | Required. Each: field (narration|reference|amount), operator (contains, not_contains, equals, starts_with, ends_with, gt, lt, between), value, value2. |
| transaction_type | string | Required. A bank transaction type, e.g. expense, customer_payment. |
| rename_to | string, nullable | Replace the narration. |
| bank_account_id | uuid, nullable | Limit to one account. |
| priority | integer | Lower runs first. |
curl https://cashflow.ng/api/v1/bank-rules \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Fuel","direction":"debit","match":"all","transaction_type":"expense","conditions":[{"field":"narration","operator":"contains","value":"filling stn"}]}'
{
"status": "Successful",
"data": {
"id": "a5a5a5a5-0034-4a9b-8c7d-1f2e3d4c5b6a",
"name": "Fuel",
"transaction_type": "expense",
"is_active": true
}
}
Fixed assets
An asset register with monthly depreciation (straight line or declining balance). With GL auto-posting on, purchases, depreciation and disposals post journals (Fixed assets 1500, Accumulated depreciation 1510, Depreciation 6700, gain 4200 / loss 6900).
/fixed-assets
/fixed-assets/{id}
includes the depreciation schedule
/fixed-assets
/fixed-assets/{id}
only descriptive fields once depreciation is recorded
/fixed-assets/{id}
only before depreciation; refunds the purchase payment
/fixed-assets/depreciate
{“through”: “2026-09-30”} — every active asset
/fixed-assets/{id}/dispose
disposed_on, proceeds, bank_account_id
| Field | Type | Description |
|---|---|---|
| name | string | Required. |
| category | string | Required. e.g. land_buildings, motor_vehicles, computer_equipment. |
| purchase_date | date | Required. |
| cost | number | Required. |
| salvage_value | number, nullable | |
| useful_life_months | integer | Required. |
| method | string: straight_line|declining_balance | Required. |
| declining_rate | number | Annual %, for declining balance. |
| bank_account_id | uuid, nullable | Paid from this account. |
curl https://cashflow.ng/api/v1/fixed-assets \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Toyota Hilux","category":"motor_vehicles","purchase_date":"2026-01-10","cost":12000000,"useful_life_months":48,"method":"straight_line","bank_account_id":"BANK_ID"}'
{
"status": "Successful",
"data": {
"id": "b6b6b6b6-0035-4a9b-8c7d-1f2e3d4c5b6a",
"asset_no": "FA-0001",
"name": "Toyota Hilux",
"status": "active"
}
}
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
| Field | Type | Description |
|---|---|---|
| code | string | Required, unique per business, e.g. “5100”. |
| name | string | Required. Account name. |
| type | string | Required. One of asset, liability, equity, income, expense. |
| subtype | string, nullable | Free-text grouping within the type. |
| parent_id | uuid, nullable | For a sub-account of another ledger account. |
| opening_balance | decimal, nullable | Starting balance. |
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
| Field | Type | Description |
|---|---|---|
| code | string (3 letters) | Required, unique. ISO-style currency code. |
| name | string, nullable | Currency name. |
| symbol | string, nullable | Symbol shown on documents, e.g. “₦”. |
| unit | string, nullable | Word used in amount-in-words, e.g. “Naira”. |
| subunit | string, nullable | Word used for the fractional part, e.g. “Kobo”. |
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
}
}
Projects & tasks
Projects for a customer, billed by a fixed fee, project rate, task rate or staff rate. Invoicing creates a draft invoice from unbilled time and locks those entries.
/projects
filter by status, customer_id
/projects/{id}
with tasks, members and hours
/projects
/projects/{id}
/projects/{id}
not once time is invoiced
/projects/{id}/unbilled
the invoice lines it would create
/projects/{id}/invoice
optional through date
/projects/{id}/tasks
/projects/{id}/tasks/{task}
/projects/{id}/tasks/{task}
| Field | Type | Description |
|---|---|---|
| name | string | Required. |
| customer_id | uuid, nullable | Needed before invoicing. |
| billing_method | string | Required. fixed | project_hours | task_hours | staff_hours | non_billable. |
| hourly_rate | number | Required for project_hours; the fallback rate otherwise. |
| fixed_amount | number | Required for fixed. |
| budget_hours / budget_amount | number, nullable | |
| status | string | active | completed | cancelled (update only). |
curl https://cashflow.ng/api/v1/projects \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"name":"Website rebuild","customer_id":"CUSTOMER_ID","billing_method":"task_hours","hourly_rate":15000}'
{
"status": "Successful",
"data": {
"id": "c7c7c7c7-0036-4a9b-8c7d-1f2e3d4c5b6a",
"code": "PRJ-0001",
"name": "Website rebuild",
"billing_method": "task_hours",
"status": "active"
}
}
Time entries
Logged time. Entries are logged as the business owner unless you pass a team member’s user_id. Invoiced entries return 409 on update or delete.
/time-entries
filter by project_id, user_id, from, to, unbilled
/time-entries/{id}
/time-entries
/time-entries/{id}
/time-entries/{id}
/time-entries/start
start a timer (stops any other running one)
/time-entries/{id}/stop
| Field | Type | Description |
|---|---|---|
| project_id | uuid | Required. |
| project_task_id | uuid, nullable | Must belong to the project. |
| date | date | Required (except when starting a timer). |
| duration | string | Required. 1:30, 1.5, 90m or 1h30m. |
| notes | string, nullable | |
| billable | boolean | Defaults to the project’s billability. |
| user_id | uuid, nullable | A team member to log for. |
curl https://cashflow.ng/api/v1/time-entries \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"project_id":"PROJECT_ID","project_task_id":"TASK_ID","date":"2026-10-02","duration":"1:30","notes":"Homepage layout"}'
{
"status": "Successful",
"data": {
"id": "d8d8d8d8-0037-4a9b-8c7d-1f2e3d4c5b6a",
"project_id": "PROJECT_ID",
"date": "2026-10-02",
"minutes": 90,
"billable": true
}
}
Custom modules
Your own record types, built in Admin console ▸ Custom modules. Address a module by its slug or id. Record values live under data, keyed by each field’s key.
/custom-modules
definitions with their fields
/custom-modules/{module}
/custom-modules
/custom-modules/{module}
send the full field list; keep each field’s key to keep its data
/custom-modules/{module}
deletes its records too
/custom-modules/{module}/records
search with q
/custom-modules/{module}/records
/custom-modules/{module}/records/{id}
/custom-modules/{module}/records/{id}
only the data keys you send change
/custom-modules/{module}/records/{id}
| Field | Type | Description |
|---|---|---|
| name | string | Module: required. Plural, e.g. Vehicles. |
| fields | array | Module: required. Each: label, type (text, textarea, number, amount, date, checkbox, select, email, phone, url, customer, vendor), required, in_list, options (select). |
| data | object | Record: values keyed by field key, validated against the field types. |
curl https://cashflow.ng/api/v1/custom-modules/vehicles/records \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"data":{"plate_number":"LAG-123-XY","status":"On the road","value":8500000}}'
{
"status": "Successful",
"data": {
"id": "e9e9e9e9-0038-4a9b-8c7d-1f2e3d4c5b6a",
"custom_module_id": "MODULE_ID",
"data": {
"plate_number": "LAG-123-XY",
"status": "On the road",
"value": 8500000
}
}
}
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
| Field | Type | Description |
|---|---|---|
| string (email) | Required. Who to invite. | |
| role | string, required without role_id | A system role slug, e.g. “accountant”. |
| role_id | uuid, required without role | A custom role's id, as an alternative to role. |
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.
https://cashflow.ng/api/v1Generate one in the app: Admin console ▸ Developer API (business owner).
Headers
Authorization (from your key) and Accept: application/json are sent for you. Add anything else your integration sends, e.g. X-Request-Id.