Connect your software to Raahi Flow
The Raahi Flow API lets your own tools read your company's invoices, delivery tickets, runs and trucks. It is read only, so nothing in Raahi Flow can be changed through it.
What you can build
- Send invoices and supplier bills into your accounts package or a spreadsheet.
- Check delivery tickets, tonnes and miles against your own records.
- Build a dashboard of runs, shifts and loads per truck.
- Keep registration, insurance and inspection dates in step with a maintenance tool.
Get started in 3 steps
- Open Settings > Integrations in the Raahi Flow app. Only the company owner can see this page.
- Create a key. Give it a name, for example "Accounts sync", tick the data it may read, and copy the key. It starts with
rf_live_. - Make your first call. Save the key in an environment variable called
RAAHI_KEY, then send it in theAuthorizationheader:
curl -H "Authorization: Bearer $RAAHI_KEY" \
"https://app.raahiflow.com/api/v1/invoices?limit=50"
You get back JSON with a data list and a next_cursor.
Authentication
Every request needs this header:
Authorization: Bearer rf_live_your_key_here
- Each key belongs to one company and can only read that company's data.
- The full key is shown once, when you create it. Raahi Flow keeps only a hash of it, so it cannot be shown again. If you lose a key, revoke it and create a new one.
- Revoke a key in Settings > Integrations at any time. It stops working straight away.
- The Integrations page shows when each key was last used.
- Keep keys on your server. Never put a key in a web page or a phone app.
Scopes
When you create a key you choose what it can read. Pick at least one. A call outside the key's scopes gets a 403.
| Scope | Lets the key read |
|---|---|
read:invoices | GET /invoices |
read:tickets | GET /tickets |
read:runs | GET /runs |
read:trucks | GET /trucks |
Base URL and paging
https://app.raahiflow.com/api/v1
Every endpoint is a GET and takes the same 2 query parameters:
| Parameter | What it does |
|---|---|
limit | How many records per page, 1 to 100. Default 50. |
cursor | The next_cursor from the page before. Leave it out for the first page. |
Each reply looks like { "data": [...], "next_cursor": "..." }. Keep passing next_cursor until it is null, which means you have the last page.
Endpoints
Dates are ISO 8601 strings in UTC. Amounts are numbers.
GET /invoices
Sales invoices and supplier bills, with their lines. Needs the read:invoices scope. Newest first, by when the record was added.
| Field | What it is |
|---|---|
id | Unique ID. Also used as the paging cursor. |
direction | sales (your invoice to a customer) or purchase (a supplier bill). |
number, party, reference | Invoice number, customer or supplier name, and PO or job reference. Empty text when not set. |
issueDate, dueDate | Dates in ISO 8601 (UTC). dueDate can be null. |
netAmount, vatAmount, grossAmount | Amounts in pounds, as numbers. |
status, paidAt | paid or awaiting_payment. paidAt is null until it is paid. |
truck | { "plateNo": ... }, or null when no truck is linked. |
items[] | Lines in order: description, quantity, unitPrice, vatRate (%), amount, ticketNo, lineDate, depot. An amount of 0 means quantity times unit price. |
createdAt | When the record was added (ISO 8601). |
Sample response (made-up data):
{
"data": [
{
"id": "clx0inv000example01",
"direction": "sales",
"number": "INV-1042",
"party": "Example Aggregates Ltd",
"reference": "PO-5531",
"issueDate": "2026-10-05T00:00:00.000Z",
"dueDate": "2026-11-04T00:00:00.000Z",
"netAmount": 1250,
"vatAmount": 250,
"grossAmount": 1500,
"status": "awaiting_payment",
"paidAt": null,
"createdAt": "2026-10-05T09:12:44.120Z",
"truck": { "plateNo": "AB00 CDE" },
"items": [
{
"description": "Haulage, Plant A to Site B",
"quantity": 5,
"unitPrice": 250,
"vatRate": 20,
"amount": 1250,
"ticketNo": "T-100231",
"lineDate": "2026-10-03T00:00:00.000Z",
"depot": "Plant A"
}
]
}
],
"next_cursor": "clx0inv000example01"
}
GET /tickets
Delivery tickets: every load on a run that has a ticket number. Loads with no ticket number are left out. Needs the read:tickets scope. Sorted by ID, newest ID first, which is roughly the order they were added.
| Field | What it is |
|---|---|
id | Unique ID of the load. Also used as the paging cursor. |
ticketNo, jobNo | Delivery ticket number and the office job number. |
pickup, dropoff | Where the load was collected and delivered. |
product, grade | Material, for example Type 1, and its grade. |
status | PENDING, DONE or CANCELLED. |
ticketMiles, loadTonnes | Miles and tonnes from the ticket, or null if not known. |
run | The run it belongs to: id, date, shift, truck plate and driver name. |
Sample response (made-up data):
{
"data": [
{
"id": "clx0load00example07",
"ticketNo": "T-100231",
"jobNo": "J-778",
"pickup": "Plant A",
"dropoff": "Site B",
"product": "Type 1",
"grade": "DRY",
"status": "DONE",
"ticketMiles": 18.5,
"loadTonnes": 19.9,
"run": {
"id": "clx0run000example03",
"date": "2026-10-03T00:00:00.000Z",
"shift": "DAY",
"truck": { "plateNo": "AB00 CDE" },
"driver": { "name": "Alex Example" }
}
}
],
"next_cursor": null
}
GET /runs
Runs: one driver's planned work for one shift. Needs the read:runs scope. Newest first, by when the record was added.
| Field | What it is |
|---|---|
id | Unique ID. Also used as the paging cursor. |
date, shift | Run date (ISO 8601) and DAY or NIGHT. |
status | PLANNED, SENT or DONE. |
acceptedAt | When the driver accepted the run, or null. |
truck, driver | Truck plate (or null) and the driver's name. |
loads | How many loads the run has. |
createdAt | When the record was added (ISO 8601). |
Sample response (made-up data):
{
"data": [
{
"id": "clx0run000example03",
"date": "2026-10-03T00:00:00.000Z",
"shift": "DAY",
"status": "DONE",
"acceptedAt": "2026-10-03T05:41:10.000Z",
"createdAt": "2026-10-02T16:20:00.000Z",
"truck": { "plateNo": "AB00 CDE" },
"driver": { "name": "Alex Example" },
"loads": 4
}
],
"next_cursor": null
}
GET /trucks
Your fleet, including trucks you have archived. Needs the read:trucks scope. Newest first, by when the record was added.
| Field | What it is |
|---|---|
id | Unique ID. Also used as the paging cursor. |
plateNo, make, model, year, vehicleType | The vehicle. year can be null. |
status | ACTIVE, MAINTENANCE or INACTIVE. |
vor, archived | true when the truck is off the road, or archived. |
odometerKm | Last odometer reading, in kilometres. |
registrationExpiry, insuranceExpiry, inspectionExpiry | Due dates (ISO 8601), or null if not set. |
createdAt | When the record was added (ISO 8601). |
Sample response (made-up data):
{
"data": [
{
"id": "clx0trk000example02",
"plateNo": "AB00 CDE",
"make": "Example Motors",
"model": "T32",
"year": 2021,
"vehicleType": "HGV",
"status": "ACTIVE",
"vor": false,
"archived": false,
"odometerKm": 184250,
"registrationExpiry": "2027-03-31T00:00:00.000Z",
"insuranceExpiry": "2027-01-15T00:00:00.000Z",
"inspectionExpiry": "2026-12-01T00:00:00.000Z",
"createdAt": "2025-04-10T08:00:00.000Z"
}
],
"next_cursor": null
}
Code examples
Each example reads every page. Set your key in an environment variable called RAAHI_KEY first.
curl
curl -H "Authorization: Bearer $RAAHI_KEY" \
"https://app.raahiflow.com/api/v1/invoices?limit=50"
# next page
curl -H "Authorization: Bearer $RAAHI_KEY" \
"https://app.raahiflow.com/api/v1/invoices?limit=50&cursor=NEXT_CURSOR_FROM_LAST_REPLY"
JavaScript
// Node 18 or later. Keep the key on your server, never in a web page.
const KEY = process.env.RAAHI_KEY;
const BASE = "https://app.raahiflow.com/api/v1";
async function getAll(resource) {
const rows = [];
let cursor = null;
do {
const url = new URL(`${BASE}/${resource}`);
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
if (!res.ok) {
const err = await res.json();
throw new Error(`${res.status} ${err.error}: ${err.message}`);
}
const page = await res.json();
rows.push(...page.data);
cursor = page.next_cursor;
} while (cursor);
return rows;
}
getAll("trucks").then((trucks) => console.log(trucks.length, "trucks"));
Python
# pip install requests
import os, requests
KEY = os.environ["RAAHI_KEY"]
BASE = "https://app.raahiflow.com/api/v1"
def get_all(resource):
rows, cursor = [], None
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
r = requests.get(f"{BASE}/{resource}", params=params,
headers={"Authorization": f"Bearer {KEY}"}, timeout=30)
if r.status_code != 200:
body = r.json()
raise RuntimeError(f"{r.status_code} {body['error']}: {body['message']}")
page = r.json()
rows += page["data"]
cursor = page["next_cursor"]
if not cursor:
return rows
for run in get_all("runs"):
print(run["date"][:10], run["shift"], run["loads"], "loads")
Rate limits
Keep to 120 requests a minute for each key. Above that, the API answers 429 until the minute is up. Fetching 100 records a page keeps you well inside the limit.
Errors
Errors come back as JSON with a short code and a plain message:
{ "error": "forbidden", "message": "This key does not have read:runs." }
| Status | error | When |
|---|---|---|
400 | bad_cursor | The cursor is not valid. |
401 | unauthorized | No key, a wrong key, or a revoked key. |
403 | forbidden | The key does not have the scope for this endpoint. |
404 | not_found | Unknown endpoint. Use invoices, tickets, runs or trucks. |
429 | rate_limited | More than 120 requests in a minute for one key. |
What the API never shares
Drivers appear by name only. The API never returns a driver's licence, phone number, email, NI number, address, pay or live location. Uploaded photos and files are not returned either.
Contact
Questions, or need data the API does not have yet? Email raahiflow@gmail.com.