Home › Developers
Developers

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

  1. Open Settings > Integrations in the Raahi Flow app. Only the company owner can see this page.
  2. 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_.
  3. Make your first call. Save the key in an environment variable called RAAHI_KEY, then send it in the Authorization header:
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.

ScopeLets the key read
read:invoicesGET /invoices
read:ticketsGET /tickets
read:runsGET /runs
read:trucksGET /trucks

Base URL and paging

https://app.raahiflow.com/api/v1

Every endpoint is a GET and takes the same 2 query parameters:

ParameterWhat it does
limitHow many records per page, 1 to 100. Default 50.
cursorThe 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.

FieldWhat it is
idUnique ID. Also used as the paging cursor.
directionsales (your invoice to a customer) or purchase (a supplier bill).
number, party, referenceInvoice number, customer or supplier name, and PO or job reference. Empty text when not set.
issueDate, dueDateDates in ISO 8601 (UTC). dueDate can be null.
netAmount, vatAmount, grossAmountAmounts in pounds, as numbers.
status, paidAtpaid 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.
createdAtWhen 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.

FieldWhat it is
idUnique ID of the load. Also used as the paging cursor.
ticketNo, jobNoDelivery ticket number and the office job number.
pickup, dropoffWhere the load was collected and delivered.
product, gradeMaterial, for example Type 1, and its grade.
statusPENDING, DONE or CANCELLED.
ticketMiles, loadTonnesMiles and tonnes from the ticket, or null if not known.
runThe 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.

FieldWhat it is
idUnique ID. Also used as the paging cursor.
date, shiftRun date (ISO 8601) and DAY or NIGHT.
statusPLANNED, SENT or DONE.
acceptedAtWhen the driver accepted the run, or null.
truck, driverTruck plate (or null) and the driver's name.
loadsHow many loads the run has.
createdAtWhen 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.

FieldWhat it is
idUnique ID. Also used as the paging cursor.
plateNo, make, model, year, vehicleTypeThe vehicle. year can be null.
statusACTIVE, MAINTENANCE or INACTIVE.
vor, archivedtrue when the truck is off the road, or archived.
odometerKmLast odometer reading, in kilometres.
registrationExpiry, insuranceExpiry, inspectionExpiryDue dates (ISO 8601), or null if not set.
createdAtWhen 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." }
StatuserrorWhen
400bad_cursorThe cursor is not valid.
401unauthorizedNo key, a wrong key, or a revoked key.
403forbiddenThe key does not have the scope for this endpoint.
404not_foundUnknown endpoint. Use invoices, tickets, runs or trucks.
429rate_limitedMore 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.