Cashflow logo Cashflow
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.

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"
    }
}

Items

Your product/service catalogue. An item can be tracked (carries stock, drawn down by documents and POS) or untracked (a service line with no stock). Composite (bundled) items and image uploads aren't available over the API yet — manage those from the app.

GET /items ?q=&per_page= — paginated
GET /items/{id}
POST /items
PUT /items/{id}
DELETE /items/{id}
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
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
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. Converting a document to the next stage of the lifecycle isn't available over the API yet.

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}/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.

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.

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
    }
}

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
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"
    }
}

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"
    }
}

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"
    }
}

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 till 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
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":"Till 1"}'
Sample response
{
    "status": "Successful",
    "data": {
        "id": "b5b5b5b5-0019-4a9b-8c7d-1f2e3d4c5b6a",
        "store_id": "STORE_ID",
        "name": "Till 1",
        "is_active": true
    }
}

Bank accounts, transactions & transfers

Your business's own current, checking, savings, domiciliary, fixed-deposit, wallet or cash accounts, each with a running-balance ledger of manual credit/debit entries, categorized by type (customer payment, expense, vendor payment, and more), plus transfers between two of your own accounts.

Bank accounts

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"
        }
    }
}

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
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
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
    }
}

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
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