Developers · REST API v1

Rovora API

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.

Base URLhttps://rovora.eu/api/v1
AuthBearer API key
FormatJSON over HTTPS
Available onFleet plan and above

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

  1. In Rovora, go to Admin → API and create a key. Pick only the permissions your integration needs.
  2. 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.
  3. 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.

Header
Authorization: Bearer rvk_live_7f2a9c31d4e8b60a5c1f93ab27de4051f6c8b3a9e2d7401c5b8f6a3d9e0c2b14

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.

ScopeGrants
drivers:read
drivers:write
Driver records, contact details, licence and document expiry dates, vehicle assignment.
vehicles:read
vehicles:write
Vehicle records, registration, make and model, mileage, insurance and road-licence expiry.
financials:read
financials:write
The 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.

Every response tells you where you stand

Response headers
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1790000160
X-RateLimit-Limit-Day: 50000
X-RateLimit-Remaining-Day: 48211
X-RateLimit-Reset-Day: 1790035200

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.

ScopeAny live keyReturns200 OK
cURL
curl https://rovora.eu/api/v1/me \
  -H "Authorization: Bearer $ROVORA_API_KEY"
Response
{
  "data": {
    "organization": { "id": "8f14…", "name": "Sciberras Fleet", "slug": "sciberras-fleet" },
    "plan": { "key": "scale", "name": "Fleet" },
    "key": {
      "id": "c0de…",
      "name": "Payroll sync",
      "prefix": "rvk_live_7f2a9c31",
      "created_at": "2026-09-22T09:14:03.221Z",
      "expires_at": null,
      "last_used_at": "2026-09-22T11:02:55.010Z"
    },
    "scopes": ["drivers:read", "financials:read"],
    "enabled_modules": ["bookkeeping", "maintenance", "settlements"],
    "rate_limit": { "per_minute": "120", "per_day": "50000" }
  }
}

Drivers

Your driver records: contact details, employment type, licence and document expiry dates, and which vehicle each one is on.

GET/api/v1/drivers

List drivers

Every driver in the fleet, filtered and paginated.

Scopedrivers:readReturns200 OK
Query parameters
statusstring

active | inactive

employment_typestring

full_time | part_time | terminated

assigned_vehicle_iduuid

Only drivers on this vehicle.

searchstring

Matches name or phone number.

updated_sincestring (ISO 8601)

Only records changed at or after this timestamp. Store the time of your last sync and pass it here to pull just the changes.

sortstring

full_name | created_at | updated_at | status. Prefix with "-" to reverse. Defaults to full_name.

limitinteger

Rows per page, 1–200. Defaults to 50.

offsetinteger

Rows to skip. Defaults to 0.

cURL
curl https://rovora.eu/api/v1/drivers?limit=25 \
  -H "Authorization: Bearer $ROVORA_API_KEY"
Response
{
  "data": [
    {
      "id": "3b1c…",
      "full_name": "Marco Vella",
      "phone": "+356 7900 1234",
      "address": "12 Triq il-Kbira, Birkirkara",
      "status": "active",
      "employment_type": "full_time",
      "user_id": "9ae2…",
      "assigned_vehicle_id": "7d40…",
      "id_card_number": "0123456M",
      "id_card_expiry_date": "2029-04-11",
      "police_conduct_expiry_date": "2027-01-30",
      "driving_license_number": "MT-448192",
      "driving_license_expiry_date": "2028-06-01",
      "tag_license_expiry_date": "2027-03-15",
      "notes": null,
      "created_at": "2026-02-11T08:00:00.000Z",
      "updated_at": "2026-09-19T15:22:41.884Z"
    }
  ],
  "meta": { "limit": 50, "offset": 0, "total": 38, "has_more": false }
}
GET/api/v1/drivers/{id}

Retrieve a driver

One driver by id. Answers 404 for an id in another fleet — ids are never confirmed across tenants.

Scopedrivers:readReturns200 OK
cURL
curl https://rovora.eu/api/v1/drivers/3b1c8f22-7a64-4c19-9e0a-51d7c2f9ab34 \
  -H "Authorization: Bearer $ROVORA_API_KEY"
POST/api/v1/drivers

Create a driver

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.

assigned_vehicle_iduuid

A vehicle in this fleet.

id_card_numberstring

National ID / residence card number.

id_card_expiry_datedate

YYYY-MM-DD. Feeds document-expiry reminders.

police_conduct_expiry_datedate

YYYY-MM-DD.

driving_license_numberstring

Licence number.

driving_license_expiry_datedate

YYYY-MM-DD.

tag_license_expiry_datedate

YYYY-MM-DD.

notesstring

Free text, max 2000 characters.

cURL
curl -X POST https://rovora.eu/api/v1/drivers \
  -H "Authorization: Bearer $ROVORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "full_name": "Marco Vella", "phone": "+356 7900 1234", "employment_type": "full_time", "driving_license_number": "MT-448192", "driving_license_expiry_date": "2028-06-01" }'
PATCH/api/v1/drivers/{id}

Update a driver

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.

Scopedrivers:writeReturns200 OK
Body fields
…any create field

Same fields as Create a driver, except user_id.

cURL
curl -X PATCH https://rovora.eu/api/v1/drivers/3b1c8f22-7a64-4c19-9e0a-51d7c2f9ab34 \
  -H "Authorization: Bearer $ROVORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "inactive", "notes": "On leave until October" }'
DELETE/api/v1/drivers/{id}

Delete a driver

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

Scopedrivers:writeReturns204 No Content
cURL
curl -X DELETE https://rovora.eu/api/v1/drivers/3b1c8f22-7a64-4c19-9e0a-51d7c2f9ab34 \
  -H "Authorization: Bearer $ROVORA_API_KEY"

Vehicles

Your cars: registration, make and model, mileage, status and document expiry dates.

GET/api/v1/vehicles

List vehicles

Every vehicle in the fleet, filtered and paginated.

Scopevehicles:readReturns200 OK
Query parameters
statusstring

active | in_service | out_of_service

assigned_driver_iduuid

Only vehicles assigned to this driver.

searchstring

Matches registration, make or model.

updated_sincestring (ISO 8601)

Only records changed at or after this timestamp. Store the time of your last sync and pass it here to pull just the changes.

sortstring

registration_number | make | mileage | created_at | updated_at | status. Prefix with "-" to reverse.

limitinteger

Rows per page, 1–200. Defaults to 50.

offsetinteger

Rows to skip. Defaults to 0.

cURL
curl https://rovora.eu/api/v1/vehicles?limit=25 \
  -H "Authorization: Bearer $ROVORA_API_KEY"
Response
{
  "data": [
    {
      "id": "7d40…",
      "registration_number": "JKL 442",
      "make": "Toyota",
      "model": "Corolla Hybrid",
      "year": 2023,
      "mileage": 84210,
      "status": "active",
      "assigned_driver_id": "3b1c…",
      "insurance_expiry_date": "2027-02-28",
      "road_license_expiry_date": "2027-01-15",
      "color": "White",
      "notes": null,
      "created_at": "2026-01-08T10:31:02.000Z",
      "updated_at": "2026-09-20T06:11:19.440Z"
    }
  ],
  "meta": { "limit": 50, "offset": 0, "total": 41, "has_more": false }
}
GET/api/v1/vehicles/{id}

Retrieve a vehicle

One vehicle by id.

Scopevehicles:readReturns200 OK
cURL
curl https://rovora.eu/api/v1/vehicles/3b1c8f22-7a64-4c19-9e0a-51d7c2f9ab34 \
  -H "Authorization: Bearer $ROVORA_API_KEY"
POST/api/v1/vehicles

Create a vehicle

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.

assigned_driver_iduuid

A driver in this fleet.

insurance_expiry_datedate

YYYY-MM-DD. Feeds document-expiry reminders.

road_license_expiry_datedate

YYYY-MM-DD.

colorstring

Body colour.

notesstring

Free text, max 2000 characters.

cURL
curl -X POST https://rovora.eu/api/v1/vehicles \
  -H "Authorization: Bearer $ROVORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "registration_number": "JKL 442", "make": "Toyota", "model": "Corolla Hybrid", "year": 2023, "mileage": 84210, "insurance_expiry_date": "2027-02-28" }'
PATCH/api/v1/vehicles/{id}

Update a vehicle

Changes only the fields you send. The usual use is a nightly mileage push from a tracker: send just { "mileage": 84900 }.

Scopevehicles:writeReturns200 OK
Body fields
…any create field

Same fields as Create a vehicle.

cURL
curl -X PATCH https://rovora.eu/api/v1/vehicles/3b1c8f22-7a64-4c19-9e0a-51d7c2f9ab34 \
  -H "Authorization: Bearer $ROVORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mileage": 84900 }'
DELETE/api/v1/vehicles/{id}

Delete a vehicle

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.

Scopevehicles:writeReturns204 No Content
cURL
curl -X DELETE https://rovora.eu/api/v1/vehicles/3b1c8f22-7a64-4c19-9e0a-51d7c2f9ab34 \
  -H "Authorization: Bearer $ROVORA_API_KEY"

Financials

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.

Scopefinancials:readReturns200 OK
Query parameters
kindstring

income | expense

include_inactiveboolean

true also returns retired categories.

cURL
curl https://rovora.eu/api/v1/financials/categories?limit=25 \
  -H "Authorization: Bearer $ROVORA_API_KEY"
Response
{
  "data": [
    { "id": "a11f…", "key": "fuel", "name": "Fuel", "kind": "expense", "is_active": true, "sort_order": 1 },
    { "id": "b22e…", "key": "uber", "name": "Uber payouts", "kind": "income", "is_active": true, "sort_order": 2 }
  ]
}
GET/api/v1/financials/transactions

List transactions

The ledger, newest first. Every report is a date-range query, so from and to are the parameters you will use most.

Scopefinancials:readReturns200 OK
Query parameters
fromdate

YYYY-MM-DD, inclusive.

todate

YYYY-MM-DD, inclusive.

directionstring

income | expense

category_iduuid

One category.

vehicle_iduuid

Only lines filed against this vehicle.

driver_iduuid

Only lines filed against this driver.

payment_methodstring

cash | card | bank | other

searchstring

Matches description or counterparty.

sortstring

txn_date | amount | created_at. Prefix with "-" to reverse. Defaults to -txn_date.

limitinteger

Rows per page, 1–200. Defaults to 50.

offsetinteger

Rows to skip. Defaults to 0.

cURL
curl https://rovora.eu/api/v1/financials/transactions?limit=25 \
  -H "Authorization: Bearer $ROVORA_API_KEY"
Response
{
  "data": [
    {
      "id": "e5c7…",
      "txn_date": "2026-09-19",
      "amount": 68.40,
      "direction": "expense",
      "category_id": "a11f…",
      "category": { "id": "a11f…", "key": "fuel", "name": "Fuel", "kind": "expense" },
      "description": "Diesel — JKL 442",
      "counterparty": "Enemed Birkirkara",
      "payment_method": "card",
      "vehicle_id": "7d40…",
      "driver_id": null,
      "source": "manual",
      "has_receipt": true,
      "created_at": "2026-09-19T17:44:02.113Z",
      "updated_at": "2026-09-19T17:44:02.113Z"
    }
  ],
  "meta": { "limit": 50, "offset": 0, "total": 1284, "has_more": true }
}
GET/api/v1/financials/transactions/{id}

Retrieve a transaction

One ledger line by id.

Scopefinancials:readReturns200 OK
cURL
curl https://rovora.eu/api/v1/financials/transactions/3b1c8f22-7a64-4c19-9e0a-51d7c2f9ab34 \
  -H "Authorization: Bearer $ROVORA_API_KEY"
POST/api/v1/financials/transactions

Record a transaction

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.

descriptionstring

What it was, e.g. "Diesel — JKL 442".

counterpartystring

Who it was paid to or received from.

payment_methodstring

cash | card | bank | other. Defaults to card.

vehicle_iduuid

File the cost against a vehicle.

driver_iduuid

File the cost against a driver.

cURL
curl -X POST https://rovora.eu/api/v1/financials/transactions \
  -H "Authorization: Bearer $ROVORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "txn_date": "2026-09-19", "category_id": "a11f…", "amount": 68.40, "description": "Diesel — JKL 442", "counterparty": "Enemed Birkirkara", "payment_method": "card", "vehicle_id": "7d40…" }'
PATCH/api/v1/financials/transactions/{id}

Update a transaction

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.

Scopefinancials:writeReturns200 OK
Body fields
…any create field

Same fields as Record a transaction.

cURL
curl -X PATCH https://rovora.eu/api/v1/financials/transactions/3b1c8f22-7a64-4c19-9e0a-51d7c2f9ab34 \
  -H "Authorization: Bearer $ROVORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 70.00, "description": "Diesel — JKL 442 (corrected)" }'
DELETE/api/v1/financials/transactions/{id}

Delete a transaction

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.

Scopefinancials:writeReturns204 No Content
cURL
curl -X DELETE https://rovora.eu/api/v1/financials/transactions/3b1c8f22-7a64-4c19-9e0a-51d7c2f9ab34 \
  -H "Authorization: Bearer $ROVORA_API_KEY"
GET/api/v1/financials/summary

Income, expenses and profit

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.

Scopefinancials:readReturns200 OK
Query parameters
fromdaterequired

YYYY-MM-DD, inclusive.

todaterequired

YYYY-MM-DD, inclusive.

vehicle_iduuid

Restrict the totals to one vehicle.

driver_iduuid

Restrict the totals to one driver.

cURL
curl https://rovora.eu/api/v1/financials/summary?limit=25 \
  -H "Authorization: Bearer $ROVORA_API_KEY"
Response
{
  "data": {
    "from": "2026-09-01",
    "to": "2026-09-28",
    "income": 18420.50,
    "expenses": 11038.77,
    "net": 7381.73,
    "transaction_count": 214,
    "by_category": [
      { "category_id": "b22e…", "key": "uber", "name": "Uber payouts", "kind": "income", "total": 12900.00, "count": 4 },
      { "category_id": "a11f…", "key": "fuel", "name": "Fuel", "kind": "expense", "total": 3184.20, "count": 96 }
    ]
  },
  "meta": { "truncated": false, "max_transactions": 20000 }
}

Keeping keys safe

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.

More in our security overview.

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.