Read and write your fleet’s drivers, vehicles and financials from your own systems. Sync drivers from your HR tool, push mileage from a tracker every night, post fuel-card spend straight into the books, or pull a month’s profit and loss into a spreadsheet.
The API is server-to-server. It sends no CORS headers on purpose, so a browser page cannot call it — an API key does not belong in front-end code where anyone can read it.
Quickstart
In Rovora, go to Admin → API and create a key. Pick only the permissions your integration needs.
Copy the key when it is shown. We store only a one-way hash of it, so that screen is genuinely the only time you can see it.
Send it on every request as Authorization: Bearer <key>.
cURL
# 1. Check your key works
curl https://rovora.eu/api/v1/me \
-H "Authorization: Bearer $ROVORA_API_KEY"
# 2. List your vehicles
curl "https://rovora.eu/api/v1/vehicles?limit=10" \
-H "Authorization: Bearer $ROVORA_API_KEY"
# 3. Record an expense against one of them
curl -X POST https://rovora.eu/api/v1/financials/transactions \
-H "Authorization: Bearer $ROVORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "category_id": "a11f…", "amount": 68.40, "description": "Diesel" }'
A small client
Everything you need is one fetch wrapper: set the header, honour Retry-After on a 429, and branch on error.code.
Node.js
const ROVORA = 'https://rovora.eu/api/v1';
async function rovora(path, init = {}) {
const res = await fetch(ROVORA + path, {
...init,
headers: {
Authorization: `Bearer ${process.env.ROVORA_API_KEY}`,
'Content-Type': 'application/json',
...init.headers,
},
});
if (res.status === 429) {
// Wait the number of seconds we're told, then try again.
const wait = Number(res.headers.get('Retry-After') ?? 5);
await new Promise((r) => setTimeout(r, wait * 1000));
return rovora(path, init);
}
const body = await res.json().catch(() => null);
if (!res.ok) throw new Error(body?.error?.code ?? `HTTP ${res.status}`);
return body;
}
const { data: drivers, meta } = await rovora('/drivers?status=active&limit=100');
console.log(`${meta.total} active drivers`);
Authentication
Every request carries an API key created by a fleet admin. A key belongs to exactly one fleet and can only ever see that fleet’s data — there is no parameter, header or id that widens it.
If your client cannot set an Authorization header, X-API-Key: <key> works too.
Scopes
Keys are least-privilege. Choose read, write, or both, per resource — a write scope implies its read scope. A key missing the scope an endpoint needs gets 403 insufficient_scope with the required scope named in the response.
vehicles:read vehicles:writeVehicle records, registration, make and model, mileage, insurance and road-licence expiry.
financials:read financials:writeThe bookkeeping ledger: income and expense transactions, categories and period totals.
Expiry, revocation and IP allow-lists
Expiry. Give a key an end date and it stops working on its own. Rotating keys yearly is a habit worth having.
Revocation. Revoking a key takes effect on the very next request. There is no cache to wait for.
IP allow-list. If your integration calls from fixed servers, list their addresses on the key. Calls from anywhere else are refused with 403 ip_not_allowed, so a stolen key is useless off your network.
Rate limits
Limits are counted per key, in two windows: a per-minute burst ceiling and a per-day total. Both are enforced in the database, so they hold no matter how many of our servers answer your calls.
PlanPer minutePer dayKeys
Fleet12050,0005
Enterprise600500,00025
The API is a paid-plan feature and is not included in the free period. If you want to build against it before you commit, tell us and we’ll open it up for you.
Go over and you get 429 rate_limited with a Retry-After header in seconds. Wait that long and carry on — retrying immediately just burns the rest of the window.
Sync efficiently. Instead of re-reading everything on a schedule, pass updated_since with the timestamp of your last successful sync. Most fleets go from thousands of calls a day to a handful.
Requests & responses
One envelope, always
Success puts the payload under data. Failure puts a stable code and a human message under error. That is the whole contract, so one handler covers every endpoint.
List response
{
"data": [ /* … up to 200 records … */ ],
"meta": {
"limit": 50,
"offset": 0,
"total": 1284,
"has_more": true
}
}
Pagination
List endpoints take limit (1–200, default 50) and offset, and report total and has_more in meta.
Sorting & filtering
Pass sort=field, or sort=-field to reverse. Each endpoint’s filters are listed in the reference below. An unknown sort field is rejected rather than ignored, so a typo shows up straight away instead of quietly returning the wrong order.
Writing
POST creates and answers 201 with the new record.
PATCH updates only the fields you send. There is no PUT: a full replacement invites an integration to blank out fields it does not know about.
DELETE answers 204 with no body.
Unknown fields are rejected, not ignored, so a misspelled key never silently does nothing.
Bodies are JSON objects, up to 256 KB.
Dates, money and ids
Calendar dates are YYYY-MM-DD. Timestamps are ISO 8601 in UTC.
Money is a decimal number with up to 2 places, always positive. Direction comes from the category, never from a minus sign.
Every id is a UUID. Ids from another fleet return 404, never 403.
Errors
Branch on error.code, never on the message — messages get reworded, codes do not. Validation failures name every bad field at once so you can fix them in one pass.
422 Unprocessable
{
"error": {
"code": "validation_failed",
"message": "One or more fields are invalid.",
"details": {
"fields": {
"driving_license_expiry_date": "must be a date in YYYY-MM-DD format"
}
}
}
}
StatusCodeWhat it means
400invalid_jsonThe body was missing, empty, or not a JSON object.
401unauthorizedNo API key was sent.
401invalid_keyThe key does not exist.
401key_revokedThe key was revoked in Rovora.
401key_expiredThe key passed its expiry date.
402plan_limitAdding this record would exceed the fleet’s plan cap. Upgrade, or remove something.
403insufficient_scopeThe key does not hold the scope this endpoint needs.
403plan_upgrade_requiredThe fleet’s plan does not include API access.
403module_disabledThe product module behind this endpoint is switched off for the fleet.
403ip_not_allowedThe key has an IP allow-list and this address is not on it.
403account_suspendedThe Rovora account is suspended or cancelled.
404not_foundNo such record in this fleet. Also returned for ids that belong to another fleet.
409conflictThe write clashes with something that already exists, or the record is protected.
413payload_too_largeThe request body exceeded 256 KB.
422validation_failedOne or more fields are invalid. `details.fields` names each one.
429rate_limitedRate limit hit. Wait for the seconds in Retry-After.
500internal_errorSomething failed on our side. Safe to retry.
Account
Confirm a key works and find out what it is allowed to do.
GET/api/v1/me
Who am I?
Returns the fleet the key belongs to, the plan in force, the scopes the key holds and which product modules are switched on. Callable with any live key regardless of scope — make this your first call when wiring up an integration.
Adds a driver record. Leave out user_id to create a record-only driver synced from your own system, or pass the id of an existing fleet member to link them to a Rovora login so they can use the driver app. Refused with 402 plan_limit when the fleet is at its plan’s driver cap.
Scopedrivers:writeReturns201 OK
Body fields
full_namestringrequired
The driver’s name. Max 200 characters.
phonestring
Contact number.
addressstring
Home address.
statusstring
active | inactive. Defaults to active.
employment_typestring
full_time | part_time | terminated.
user_iduuid
An existing member of this fleet to link the driver to. Cannot be changed later.
Changes only the fields you send; everything else is left alone. user_id cannot be changed — linking a driver to a login is an account operation and stays in the dashboard.
Removes a driver record and answers 204. Only drivers with no linked Rovora login can be deleted here: deleting someone’s account should take a person clicking a button, not a stray script, so a linked driver returns 409 conflict. To take a linked driver off the road, PATCH their status to "inactive".
Adds a vehicle. Registration numbers are unique across Rovora, so a duplicate answers 409 conflict. Refused with 402 plan_limit at the plan’s vehicle cap.
Scopevehicles:writeReturns201 OK
Body fields
registration_numberstringrequired
Number plate. Max 32 characters.
makestringrequired
e.g. Toyota.
modelstringrequired
e.g. Corolla Hybrid.
yearinteger
Model year.
mileageinteger
Current odometer reading. Defaults to 0.
statusstring
active | in_service | out_of_service. Defaults to active.
Removes a vehicle and answers 204. Money already spent on the car stays in the books — its transactions keep their amounts and simply lose the link. A vehicle with records that cannot be detached answers 409; set its status to "out_of_service" instead.
The bookkeeping ledger — one dated line per expense or income event, the same records the Financials screen reports on. Requires the Bookkeeping module to be switched on for the fleet.
GET/api/v1/financials/categories
List categories
Your chart of accounts. Every transaction needs a category_id, and the category’s kind (income or expense) is what gives a line its direction. Read this once and cache the ids.
Files one expense or income line. amount is always positive — whether it counts as money in or money out comes from the category, so there is no way to book an expense that accidentally reads as revenue. Lines created here look exactly like ones typed into Rovora and show up in the financial reports immediately.
Scopefinancials:writeReturns201 OK
Body fields
category_iduuidrequired
A category from this fleet’s chart of accounts.
amountnumberrequired
Positive, up to 2 decimal places.
txn_datedate
The day the money moved, YYYY-MM-DD. Defaults to today.
Corrects a line. Lines Rovora posted itself from a recurring cost (source "recurring") are read-only here — they are regenerated on each posting run, so an edit would be undone. Change the recurring cost in Rovora instead.
Removes a line from the books and answers 204. Any attached receipt image is deleted with it. Automatically-posted recurring lines are protected and answer 409.
Totals for any date range with a per-category breakdown — one call instead of paging the whole ledger and adding it up yourself. Both dates are required: point it at a week, a month, or your 4-week pay cycle.
An API key is a password to your fleet’s data. Treat it like one.
Server-side only. Never put a key in a browser, a mobile app or anything a customer can open. We send no CORS headers, which stops the easiest version of this mistake.
Environment variables, not source control. A key committed to a repository is a key that will eventually be public.
One key per integration. Then revoking the one that leaked doesn’t take down everything else you run.
Least privilege. A reporting job needs financials:read, not financials:write.
Lock it to your servers. Add their IP addresses to the key.
Rotate. Set an expiry. Create the replacement, move your integration across, then revoke the old one.
What we do on our side
HTTPS only. Plain HTTP is refused outright rather than answered — a key sent in the clear has already leaked, and we would rather tell you than quietly serve the request.
Your fleet, and only your fleet. The fleet a request can touch is taken from the key itself, never from the URL, a header or the body. Every single query is filtered by it, and ids belonging to another fleet come back as 404 — you cannot even confirm they exist.
Nothing internal is writable. Write endpoints accept an explicit list of fields and reject anything else, so there is no request body that can reassign a record to a different fleet.
Keys can’t be guessed. Each one is 256 bits of cryptographically random data, and we store only a one-way hash — a database leak would not hand anyone a working key. Repeated failed attempts from one address are blocked outright.
Checked every time. Scope, expiry, revocation, IP allow-list and plan are all re-checked on every request. Revoking a key takes effect on the next call.
Logged. Every call is recorded with its key, status, timing and source address — never any of your data — and fleet admins can see the recent ones in Rovora.
Found a vulnerability? Email security@rovora.eu and we’ll respond quickly.
Versioning
The version is in the path: /api/v1. Inside v1 we only make additive changes — new endpoints, new optional parameters, new fields on existing responses. Write your client so unknown fields are ignored and it will keep working.
Anything that would break an existing client gets a new version, and we will email every fleet with a live API key well before v1 stops being supported.
Ready to build?
Create a key in Rovora and make your first call in under five minutes. Stuck on something, or need an endpoint we haven’t built yet? Tell us — we prioritise what operators actually ask for.