Cashflow logo
Developer API

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.

One key per business
Not per user. Anyone who has it can act on that business's behalf.
Always JSON
Every response is JSON, including errors — regardless of your Accept header.
Same engine as the app
Numbering, VAT, snapshots and stock draw-down are shared with the web app, not a simplified copy.

Base URL

Every endpoint on this page is relative to:

https://cashflow.ng/api/v1

Request & 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:

Single resource
Sample response
{
    "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:

Paginated list
Sample response
{
    "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/v1 and always return JSON, regardless of your Accept header.

Errors & rate limits

Standard HTTP status codes. Validation failures return field-level detail:

422 Unprocessable Content
Sample error response
{
    "status": "Failed",
    "message": "The given data was invalid.",
    "errors": {
        "name": [
            "The name field is required."
        ]
    }
}
401 Unauthorized
Missing, invalid, or rotated-away API key.
404 Not Found
No such record, or it belongs to a different business.
422 Unprocessable Content
Validation failed, or the action isn't allowed in the record's current state.
429 Too Many Requests
Rate limit: 120 requests/minute per API key.

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.

GET /business read the profile
PUT /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.
email 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.
PUT /business
Sample request
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}'
Sample response
{
    "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).

GET /customers ?q=&per_page= — paginated
GET /customers/{id}
POST /customers
PUT /customers/{id}
DELETE /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.
email 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.
POST /customers
Sample request
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"}'
Sample response
{
    "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.

GET /vendors
GET /vendors/{id}
POST /vendors
PUT /vendors/{id}
DELETE /vendors/{id}
Field Type Description
name string Required. Vendor / supplier name.
email 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.
POST /vendors
Sample request
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"}'
Sample response
{
    "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.

GET /customers/{id}/contact-persons
POST /customers/{id}/contact-persons
GET /vendors/{id}/contact-persons
POST /vendors/{id}/contact-persons
GET /contact-persons/{id}
PUT /contact-persons/{id}
DELETE /contact-persons/{id}
Field Type Description
first_name string Required.
last_name string, nullable
salutation string, nullable Mr, Mrs, Dr…
email string (email), nullable
phone / mobile string, nullable
designation / department string, nullable
is_primary boolean Making one primary clears the flag on the others.
POST /customers/{id}/contact-persons
Sample request
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}'
Sample response
{
    "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.

GET /items ?q=&is_composite=&per_page= — paginated
GET /items/{id}
POST /items
PUT /items/{id}
DELETE /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.
POST /items
Sample request
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"
  }'
Sample response
{
    "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.

GET /categories
GET /categories/{id}
POST /categories
PUT /categories/{id}
DELETE /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.
POST /categories
Sample request
curl https://cashflow.ng/api/v1/categories \
  -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"Beverages","sort_order":1}'
Sample response
{
    "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.

GET /price-lists
GET /price-lists/{id}
POST /price-lists
PUT /price-lists/{id}
DELETE /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.
POST /price-lists
Sample request
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
  }'
Sample response
{
    "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

GET /locations
GET /locations/{id}
POST /locations
PUT /locations/{id}
DELETE /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.
POST /locations
Sample request
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"}'
Sample response
{
    "status": "Successful",
    "data": {
        "id": "b3b3b3b3-0007-4a9b-8c7d-1f2e3d4c5b6a",
        "name": "Lagos Mainland",
        "code": "LOS-1",
        "is_default": true
    }
}

Warehouses

GET /warehouses
POST /warehouses
PUT /warehouses/{id}
DELETE /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.
POST /warehouses
Sample request
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}'
Sample response
{
    "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.

GET /stock-adjustments ?item_id=&warehouse_id=
POST /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”.
POST /stock-adjustments
Sample request
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"}'
Sample response
{
    "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.

GET /documents ?type=&status= — paginated
GET /documents/{id}
POST /documents
PUT /documents/{id}
DELETE /documents/{id}
POST /documents/{id}/email email it to the customer
POST /documents/{id}/convert body {"type": "invoice"} — returns the new document (201); 422 lists convertible_to
POST /documents/{id}/submit-for-approval start the approval chain, if one applies
POST /documents/{id}/approve approve at the current level
POST /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.
POST /documents
Sample request
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}
    ]
  }'
Sample response
{
    "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
POST /documents — with shipping
Sample request
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
    }
  }'
Sample response
{
    "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.

POST /documents/{id}/email
Sample request
curl https://cashflow.ng/api/v1/documents/DOCUMENT_ID/email \
  -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{}'
Sample response — ‘to’ omitted, falls back to the customer’s own email
{
    "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.
POST /documents/{id}/submit-for-approval
Sample response
{
    "status": "Successful",
    "data": {
        "id": "e6e6e6e6-0010-4a9b-8c7d-1f2e3d4c5b6a",
        "approval_status": "pending",
        "current_approval_level": 1
    }
}
POST /documents/{id}/reject
Sample request
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"}'
Sample response
{
    "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.

GET /recurring-invoices
GET /recurring-invoices/{id}
POST /recurring-invoices
PUT /recurring-invoices/{id}
DELETE /recurring-invoices/{id} invoices already generated are kept
POST /recurring-invoices/{id}/pause
POST /recurring-invoices/{id}/resume
POST /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.
POST /recurring-invoices
Sample request
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}]
  }'
Sample response
{
    "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.

GET /payments ?document_id= — paginated, read-only
GET /payments/{id}
POST /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.
POST /documents/{id}/payments
Sample request
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"}'
Sample response
{
    "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.

POST /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.
POST /documents/{id}/whatsapp
Sample request
curl https://cashflow.ng/api/v1/documents/DOCUMENT_ID/whatsapp \
  -H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"phone":"08031234567"}'
Sample response
{
    "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.

GET /expenses
GET /expenses/{id}
POST /expenses
PUT /expenses/{id}
DELETE /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.
POST /expenses
Sample request
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"}'
Sample response
{
    "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.

GET /recurring-expenses
GET /recurring-expenses/{id}
POST /recurring-expenses
PUT /recurring-expenses/{id} partial updates allowed
DELETE /recurring-expenses/{id} expenses already recorded are kept
POST /recurring-expenses/{id}/pause
POST /recurring-expenses/{id}/resume
POST /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.
POST /recurring-expenses
Sample request
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"}'
Sample response
{
    "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.

GET /bills ?status=
GET /bills/{id}
POST /bills
PUT /bills/{id}
DELETE /bills/{id}
POST /bills/{id}/open move draft → open, posts to payables
POST /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?}.
POST /bills
Sample request
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}]
  }'
Sample response
{
    "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.

GET /vendor-payments ?bill_id= — paginated
GET /vendor-payments/{id}
GET /vendor-payments/{id}
Sample request
curl https://cashflow.ng/api/v1/vendor-payments/PAYMENT_ID \
  -H "Authorization: Bearer YOUR_API_KEY"
Sample response
{
    "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.

GET /vendor-credits
GET /vendor-credits/{id}
POST /vendor-credits
PUT /vendor-credits/{id} only while nothing is applied
DELETE /vendor-credits/{id} only while nothing is applied
POST /vendor-credits/{id}/apply {“allocations”: {“BILL_ID”: 25000}}
POST /vendor-credits/{id}/refund amount, date, bank_account_id
POST /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.
POST /vendor-credits
Sample request
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}]}'
Sample response
{
    "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.

GET /purchase-orders
GET /purchase-orders/{id}
POST /purchase-orders
PUT /purchase-orders/{id}
DELETE /purchase-orders/{id} draft only
POST /purchase-orders/{id}/send draft → sent
POST /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}.
POST /purchase-orders
Sample request
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}]
  }'
Sample response
{
    "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.

GET /purchase-returns
GET /purchase-returns/{id}
POST /purchase-returns
PUT /purchase-returns/{id} draft only
DELETE /purchase-returns/{id}
POST /purchase-returns/{id}/complete reduces stock, posts to the ledger
POST /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.
POST /purchase-returns
Sample request
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"}'
Sample response
{
    "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.

GET /recurring-bills
GET /recurring-bills/{id}
POST /recurring-bills
PUT /recurring-bills/{id}
DELETE /recurring-bills/{id} bills already generated are kept
POST /recurring-bills/{id}/pause
POST /recurring-bills/{id}/resume
POST /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.
POST /recurring-bills
Sample request
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}'
Sample response
{
    "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.

GET /registers
GET /registers/{id}
POST /registers
PUT /registers/{id}
DELETE /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.
POST /registers
Sample request
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"}'
Sample response
{
    "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.

GET /pos-sales ?store_id=&register_id=&status=&date= — paginated
GET /pos-sales/{id} with lines, tenders and the receipt
POST /pos-sales ring up a sale (201; 200 on an idempotent replay)
POST /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.
POST /pos-sales
Sample request
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}'
Sample response
{
    "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.

GET /integrations
GET /integrations/{id} includes the last 10 runs
PATCH /integrations/{id} what syncs
POST /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.
POST /integrations/{id}/sync
Sample request
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"]}'
Sample response
{
    "status": "Successful",
    "message": "Sync queued for Ada Bags."
}
GET /integrations
Sample request
curl https://cashflow.ng/api/v1/integrations \
  -H "Authorization: Bearer YOUR_API_KEY"
Sample response
{
    "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

GET /bank-accounts
GET /bank-accounts/{id}
POST /bank-accounts
PUT /bank-accounts/{id}
DELETE /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.
POST /bank-accounts
Sample request
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"}'
Sample response
{
    "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

GET /bank-transactions ?bank_account_id=&direction=&transaction_type=&from=&to=
GET /bank-transactions/{id}
POST /bank-transactions
DELETE /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.
POST /bank-transactions
Sample request
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"}'
Sample response
{
    "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.

POST /bank-transfers
DELETE /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
POST /bank-transfers
Sample request
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"}'
Sample response
{
    "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.

GET /bank-rules
GET /bank-rules/{id}
POST /bank-rules
PUT /bank-rules/{id}
DELETE /bank-rules/{id}
POST /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.
POST /bank-rules
Sample request
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"}]}'
Sample response
{
    "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).

GET /fixed-assets
GET /fixed-assets/{id} includes the depreciation schedule
POST /fixed-assets
PUT /fixed-assets/{id} only descriptive fields once depreciation is recorded
DELETE /fixed-assets/{id} only before depreciation; refunds the purchase payment
POST /fixed-assets/depreciate {“through”: “2026-09-30”} — every active asset
POST /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.
POST /fixed-assets
Sample request
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"}'
Sample response
{
    "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.

GET /chart-of-accounts
GET /chart-of-accounts/{id}
GET /chart-of-accounts/trial-balance ?as_of=
POST /chart-of-accounts
PUT /chart-of-accounts/{id}
DELETE /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.
POST /chart-of-accounts
Sample request
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"}'
Sample response
{
    "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).

GET /journals
GET /journals/{id}
POST /journals
PUT /journals/{id} draft only
DELETE /journals/{id} draft only
POST /journals/{id}/post lock it in — must balance
POST /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?}.
POST /journals
Sample request
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}
    ]
  }'
Sample response
{
    "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.

GET /currency-adjustments
GET /currency-adjustments/{id}
POST /currency-adjustments
DELETE /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.
POST /currency-adjustments
Sample request
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}'
Sample response
{
    "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.

GET /currencies
POST /currencies add a custom currency
DELETE /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”.
POST /currencies
Sample request
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"}'
Sample response
{
    "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.

GET /projects filter by status, customer_id
GET /projects/{id} with tasks, members and hours
POST /projects
PUT /projects/{id}
DELETE /projects/{id} not once time is invoiced
GET /projects/{id}/unbilled the invoice lines it would create
POST /projects/{id}/invoice optional through date
POST /projects/{id}/tasks
PUT /projects/{id}/tasks/{task}
DELETE /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).
POST /projects
Sample request
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}'
Sample response
{
    "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.

GET /time-entries filter by project_id, user_id, from, to, unbilled
GET /time-entries/{id}
POST /time-entries
PUT /time-entries/{id}
DELETE /time-entries/{id}
POST /time-entries/start start a timer (stops any other running one)
POST /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.
POST /time-entries
Sample request
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"}'
Sample response
{
    "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.

GET /custom-modules definitions with their fields
GET /custom-modules/{module}
POST /custom-modules
PUT /custom-modules/{module} send the full field list; keep each field’s key to keep its data
DELETE /custom-modules/{module} deletes its records too
GET /custom-modules/{module}/records search with q
POST /custom-modules/{module}/records
GET /custom-modules/{module}/records/{id}
PUT /custom-modules/{module}/records/{id} only the data keys you send change
DELETE /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.
POST /custom-modules/{module}/records
Sample request
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}}'
Sample response
{
    "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.

GET /users lists members + pending invitations
POST /users invite an existing user or a new email
PUT /users/{id} change a member's role
DELETE /users/{id} remove a member — a business must keep at least one owner
Field Type Description
email 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.
POST /users
Sample request
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"}'
Sample response
{
    "status": "Successful",
    "message": "Invitation sent to new-hire@example.com."
}

Seat limits still apply — inviting past your plan's user cap returns a 422.

Ready to integrate?

Sign in, open Admin console ▸ Developer API, and generate your key.

Sign in