Skip to content

Developers

REST API reference

Connect your own systems and partners to your agency: fleet, reservations, customers, payments and invoices, with an API key.

OpenAPI document (JSON)

Overview

The Orvo Rental REST API gives an agency's own systems and its partners its fleet, reservations, customers, payments and invoices. Requests and answers are JSON. Money is an integer in the currency's minor unit (18000 is 180.00); dates are YYYY-MM-DD and times HH:MM in the agency's time zone; instants are ISO 8601. Records have the same shape the back office uses.

Base URLs (the agency's own address works too; the platform address serves every agency's key):

https://orvorental.app/api/v1

Authentication

Every endpoint but the OpenAPI document needs an API key, sent as a bearer token. An agency's owner or administrator creates keys in the back office (Settings › Integrations), with a name, the scopes it may use and an optional expiry. The key is shown once; only a hash of it is stored. A key can only carry scopes its creator's own role allows. The key alone names the agency: on the agency's own address it must be that agency's key. A revoked or expired key answers 401, like a wrong one.

Authorization: Bearer orvo_<prefix>_<secret>

The API is part of the plans that include API access; without it every call answers402 FEATURE_DISABLED. Scopes:

Scopes
ScopeAllows
fleet:readRead vehicles, categories, locations and availability; price a selection
reservations:readRead reservations
reservations:writeCreate, confirm and cancel reservations
customers:readRead customer records
customers:writeCreate and change customer records
payments:readRead payments, deposits and refunds
payments:writeRecord payments and deposits received by hand
invoices:readRead invoices and credit notes, with a link to their PDF
webhooks:manageManage the webhook endpoints

A key without the operation's scope answers 403 SCOPE_REQUIRED. Changes made with a key are recorded in the agency's change log and in the reservation's history under the key's name.

Rate limits

Each key may make 600 requests a minute across the API. Over the limit the answer is429 RATE_LIMITED with a Retry-After header in seconds. Back off and retry after it.

Idempotency

Every POST that changes something takes an optional Idempotency-Key header: 8 to 100 characters of letters, digits, _ and - (a UUID works). Use one new key per operation and send the same key when you retry it. A repeat with the same key and body within 24 hours gets the first successful answer again, with Idempotent-Replayed: true, and changes nothing. The same key with another body answers422 IDEMPOTENCY_MISMATCH. A refused request is not remembered, so it can be corrected and sent again.

Errors

An error is { "error": "<English text>", "code": "<CODE>", "fields"?: { ... } }. Branch oncode, never on error. Validation codes in fields are stable (Required, TooLong?max=120, InvalidDate, ...).

Error codes
StatusCodeMeaning
401UNAUTHORIZEDNo key, or a malformed, unknown, revoked or expired one; or an agency host that is not the key's agency.
402FEATURE_DISABLEDThe agency's plan does not include API access.
402READ_ONLYA write while the agency is read-only (an expired trial, a payment past due).
403SCOPE_REQUIREDThe key lacks the operation's scope (named in `scope`).
404NOT_FOUNDNo such item, or another agency's.
409CAR_TAKENThe vehicle is not free for the window.
409STALEThe reservation changed since the `version` you sent; `version` is the one now.
409INVALID_TRANSITIONThe reservation cannot take that step in its status (`from`).
409BUSYSomeone else changed the same thing at that moment; nothing was saved, try again.
413BODY_TOO_LARGEThe body is over 32 KB.
422VALIDATION_FAILEDThe body or query does not validate; `fields` maps each field to a validation code.
422QUOTE_REFUSEDThe pricing rules refuse the selection (`reason`, `params`).
422IDEMPOTENCY_MISMATCHThe Idempotency-Key was used for another request.
429RATE_LIMITEDOver the key's limit; wait Retry-After seconds.
503AGENCY_SUSPENDEDThe agency is suspended.
500SERVER_ERRORSomething went wrong on our side; try again.

Pagination

Lists answer { "data": [...], "hasMore": true, "nextCursor": "<id>" }. Ask for the next page with?after=<nextCursor>; ?limit= takes 1 to 100 (default 25). Each list has a fixed order (reservations and customers newest first), so a cursor always names the same position.

Endpoints

The full description, with every field, is the OpenAPI 3.1 document.

Fleet

GET/api/v1/vehiclesList vehicles

Every vehicle of the agency, published or not, in the fleet's order (sort order, then oldest first).

Scope: fleet:read

Parameters of listVehicles
ParameterInDescription
categoryIdqueryOnly this category's vehicles
statusqueryOnly vehicles in this status
limitqueryItems per page, 1 to 100 (default 25)
afterqueryThe `nextCursor` of the page before
Example request
curl -s "https://orvorental.app/api/v1/vehicles" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

A page of vehicles

GET/api/v1/vehicles/{id}Get a vehicle

One vehicle by its id.

Scope: fleet:read

Parameters of getVehicle
ParameterInDescription
idrequiredpathThe vehicle's id
Example request
curl -s "https://orvorental.app/api/v1/vehicles/cm4car0000000000000000001" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

The vehicle

GET/api/v1/categoriesList vehicle categories

The agency's vehicle categories, inactive ones too, in their order. One page holds them all.

Scope: fleet:read

Example request
curl -s "https://orvorental.app/api/v1/categories" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

The categories

GET/api/v1/locationsList pick-up points

The agency's pick-up and return points, inactive ones too, in their order. `address` is the name a reservation takes. One page holds them all.

Scope: fleet:read

Example request
curl -s "https://orvorental.app/api/v1/locations" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

The points

GET/api/v1/availabilityVehicles free for a window

The published vehicles free on every day from `from` to `to` (both included, at most 90 days). Creating the reservation checks the exact times again, with the agency's turnaround buffer.

Scope: fleet:read

Parameters of getAvailability
ParameterInDescription
fromrequiredqueryThe first day
torequiredqueryThe last day
categoryIdqueryOnly this category's vehicles
Example request
curl -s "https://orvorental.app/api/v1/availability" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

The free vehicles

POST/api/v1/quotePrice a selection

The price of a selection exactly as POST /reservations prices it: the agency's rate plans, seasons, extras, protection, promo codes and one-way fees, with every active extra and protection on offer and no minimum rental. Changes nothing.

Scope: fleet:read

Body fields: carId*, location*, returnLocation, pickUpDate*, pickUpTime, dropOffDate*, dropOffTime, extras, protectionCode, promoCode, driverAge (* required)

Example request
curl -s -X POST "https://orvorental.app/api/v1/quote" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"carId":"cm4car0000000000000000001","location":"Airport","pickUpDate":"2026-12-10","pickUpTime":"10:00","dropOffDate":"2026-12-14","dropOffTime":"10:00"}'
Answer 200

The quote: lines and totals in minor units

{
  "quote": {
    "ok": true,
    "days": 4,
    "currency": "EUR",
    "totalMinor": 18000,
    "lines": []
  },
  "currency": "EUR"
}

Reservations

GET/api/v1/reservationsList reservations

The agency's reservations, newest first. Every filter is optional; a value the API does not know reads as no filter, except `updatedSince`.

Scope: reservations:read

Parameters of listReservations
ParameterInDescription
statusqueryOne status
sourcequeryOne source
fromqueryPick-up on or after this day
toqueryPick-up on or before this day
updatedSincequeryChanged at or after this instant (ISO 8601 with its offset)
externalRefqueryYour own reference, exactly
qquerySearch: the reference, the name, phone or e-mail, the plate
limitqueryItems per page, 1 to 100 (default 25)
afterqueryThe `nextCursor` of the page before
Example request
curl -s "https://orvorental.app/api/v1/reservations" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

A page of reservations

GET/api/v1/reservations/{ref}Get a reservation

One reservation by its reference, in any case.

Scope: reservations:read

Parameters of getReservation
ParameterInDescription
refrequiredpathThe reservation's reference
Example request
curl -s "https://orvorental.app/api/v1/reservations/RES-2026-00042" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

The reservation

{
  "id": "cm4bok0000000000000000001",
  "reference": "RES-2026-00042",
  "externalRef": "PARTNER-778812",
  "status": "PENDING",
  "source": "PARTNER",
  "userName": "Ana Hoxha",
  "contactNumber": "+355 69 485 2518",
  "email": "[email protected]",
  "location": "Airport",
  "pickUpDate": "2026-12-10",
  "pickUpTime": "10:00",
  "dropOffDate": "2026-12-14",
  "dropOffTime": "10:00",
  "pickupAt": "2026-12-10T09:00:00.000Z",
  "returnAt": "2026-12-14T09:00:00.000Z",
  "carId": "cm4car0000000000000000001",
  "carSnapshot": "Volkswagen Golf (2021)",
  "version": 1,
  "totals": {
    "totalMinor": 18000,
    "currency": "EUR"
  },
  "ledger": {
    "paidMinor": 0,
    "refundedMinor": 0,
    "heldMinor": 0,
    "balanceMinor": 18000,
    "paymentStatus": "UNPAID"
  },
  "createdAt": "2026-11-02T08:30:00.000Z",
  "updatedAt": "2026-11-02T08:30:00.000Z"
}

POST/api/v1/reservationsCreate a reservation

A reservation, created as the back office creates one: priced by the agency's rules, the vehicle checked free for the exact window (with the turnaround buffer) in a serializable transaction, its history written as the API key. `source` defaults to PARTNER and `status` to PENDING (confirm it with POST /reservations/{ref}/confirm). `externalRef` is your own reference, stored and searchable. No mail is sent on creation.

Scope: reservations:write · takes an Idempotency-Key

Body fields: carId*, location*, returnLocation, pickUpDate*, pickUpTime, dropOffDate*, dropOffTime, extras, protectionCode, promoCode, driverAge, userName*, contactNumber*, email, locale, customerId, rentalType, deliveryAddress, customerNotes, internalNotes, notes, source, status, externalRef (* required)

Example request
curl -s -X POST "https://orvorental.app/api/v1/reservations" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"carId":"cm4car0000000000000000001","userName":"Ana Hoxha","contactNumber":"+355 69 485 2518","email":"[email protected]","location":"Airport","pickUpDate":"2026-12-10","pickUpTime":"10:00","dropOffDate":"2026-12-14","dropOffTime":"10:00","source":"PARTNER","externalRef":"PARTNER-778812"}'
Answer 201

The reservation

{
  "id": "cm4bok0000000000000000001",
  "reference": "RES-2026-00042",
  "externalRef": "PARTNER-778812",
  "status": "PENDING",
  "source": "PARTNER",
  "userName": "Ana Hoxha",
  "contactNumber": "+355 69 485 2518",
  "email": "[email protected]",
  "location": "Airport",
  "pickUpDate": "2026-12-10",
  "pickUpTime": "10:00",
  "dropOffDate": "2026-12-14",
  "dropOffTime": "10:00",
  "pickupAt": "2026-12-10T09:00:00.000Z",
  "returnAt": "2026-12-14T09:00:00.000Z",
  "carId": "cm4car0000000000000000001",
  "carSnapshot": "Volkswagen Golf (2021)",
  "version": 1,
  "totals": {
    "totalMinor": 18000,
    "currency": "EUR"
  },
  "ledger": {
    "paidMinor": 0,
    "refundedMinor": 0,
    "heldMinor": 0,
    "balanceMinor": 18000,
    "paymentStatus": "UNPAID"
  },
  "createdAt": "2026-11-02T08:30:00.000Z",
  "updatedAt": "2026-11-02T08:30:00.000Z"
}

POST/api/v1/reservations/{ref}/confirmConfirm a reservation

Confirms a QUOTE or PENDING reservation (the vehicle checked free again). `version` is the version you read: when the reservation changed since, the answer is 409 STALE with the version now; without it, the version stored now. The customer gets the confirmation mail when an e-mail is on file.

Scope: reservations:write · takes an Idempotency-Key

Parameters of confirmReservation
ParameterInDescription
refrequiredpathThe reservation's reference

Body fields: version

Example request
curl -s -X POST "https://orvorental.app/api/v1/reservations/RES-2026-00042/confirm" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"version":1}'
Answer 200

The reservation after the step

POST/api/v1/reservations/{ref}/cancelCancel a reservation

Cancels the reservation, with an optional reason (stored). `version` as for confirm. The customer gets the cancellation mail when an e-mail is on file.

Scope: reservations:write · takes an Idempotency-Key

Parameters of cancelReservation
ParameterInDescription
refrequiredpathThe reservation's reference

Body fields: version, reason

Example request
curl -s -X POST "https://orvorental.app/api/v1/reservations/RES-2026-00042/cancel" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"version":2,"reason":"Cancelled by the customer on the partner's site"}'
Answer 200

The reservation after the step

Customers

GET/api/v1/customersList customers

The agency's customer records, newest first. A record never carries the date of birth, licence number or street address through the API: the licence is reduced to `licenceMasked` and `licenceExpired`.

Scope: customers:read

Parameters of listCustomers
ParameterInDescription
qqueryThe names, the e-mail or the phone digits
typequeryINDIVIDUAL or BUSINESS
blockedquerytrue: blocked only; false: not blocked only
limitqueryItems per page, 1 to 100 (default 25)
afterqueryThe `nextCursor` of the page before
Example request
curl -s "https://orvorental.app/api/v1/customers" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

A page of customers

GET/api/v1/customers/{id}Get a customer

One customer record by its id.

Scope: customers:read

Parameters of getCustomer
ParameterInDescription
idrequiredpathThe customer's id
Example request
curl -s "https://orvorental.app/api/v1/customers/cm4cus0000000000000000001" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

The customer

POST/api/v1/customersCreate a customer

A new customer record: a first name, the rest optional. E-mail addresses are not unique among records.

Scope: customers:write · takes an Idempotency-Key

Body fields: type, firstName*, lastName, companyName, taxId, email, phone, dateOfBirth, country, preferredLanguage, addressLine1, addressLine2, city, postalCode, region, licenceNumber, licenceCountry, licenceIssuedOn, licenceExpiresOn, consentEmail, consentWhatsapp, consentSms, consentMarketing, tags, notes (* required)

Example request
curl -s -X POST "https://orvorental.app/api/v1/customers" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"firstName":"Ana","lastName":"Hoxha","email":"[email protected]","phone":"+355 69 485 2518","country":"AL"}'
Answer 201

The customer

PATCH/api/v1/customers/{id}Change a customer

Only the fields sent change; null clears an optional one. An anonymised record is not changed (409 ANONYMISED).

Scope: customers:write

Parameters of updateCustomer
ParameterInDescription
idrequiredpathThe customer's id

Body fields: type, firstName, lastName, companyName, taxId, email, phone, dateOfBirth, country, preferredLanguage, addressLine1, addressLine2, city, postalCode, region, licenceNumber, licenceCountry, licenceIssuedOn, licenceExpiresOn, consentEmail, consentWhatsapp, consentSms, consentMarketing, tags, notes

Example request
curl -s -X PATCH "https://orvorental.app/api/v1/customers/cm4cus0000000000000000001" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+355 69 000 0000","tags":["partner"]}'
Answer 200

The customer

Payments

POST/api/v1/reservations/{ref}/paymentsRecord a payment received

Money received outside the platform, recorded on the reservation: a PAYMENT gets its receipt number (and PDF) at once, a DEPOSIT is held. The ledger on the reservation moves in the same transaction. The customer's mail goes out unless the agency turned it off.

Scope: payments:write · takes an Idempotency-Key

Parameters of recordPayment
ParameterInDescription
refrequiredpathThe reservation's reference

Body fields: kind*, method*, amountMinor*, currency, receivedAt, reference, note (* required)

Example request
curl -s -X POST "https://orvorental.app/api/v1/reservations/RES-2026-00042/payments" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"kind":"PAYMENT","method":"BANK_TRANSFER","amountMinor":18000,"currency":"EUR","reference":"SEPA-20261102-01"}'
Answer 201

The payment and the reservation's ledger

GET/api/v1/paymentsList payments, deposits or refunds

The Payments page's tabs: payments and deposits by when received, refunds by when issued, newest first.

Scope: payments:read

Parameters of listPayments
ParameterInDescription
tabquerypayments (default), deposits or refunds
statusqueryA payment status, a deposit's hold status or a refund status
methodqueryThe method (refunds: the refunded payment's)
fromqueryOn or after this day, in the agency's time zone
toqueryOn or before this day, in the agency's time zone
qqueryThe receipt number, the bank reference, the reservation or the customer
limitqueryItems per page, 1 to 100 (default 25)
afterqueryThe `nextCursor` of the page before
Example request
curl -s "https://orvorental.app/api/v1/payments" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

A page of the tab

Invoices

GET/api/v1/invoicesList invoices and credit notes

Newest issued first.

Scope: invoices:read

Parameters of listInvoices
ParameterInDescription
typequeryall (default), invoice or credit
statusqueryISSUED, PARTIALLY_PAID, PAID or VOID
fromqueryIssued on or after this day, in the agency's time zone
toqueryIssued on or before this day, in the agency's time zone
qqueryThe number, the bill-to name or company, the reservation
limitqueryItems per page, 1 to 100 (default 25)
afterqueryThe `nextCursor` of the page before
Example request
curl -s "https://orvorental.app/api/v1/invoices" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

A page of invoices

GET/api/v1/invoices/{number}Get an invoice

One invoice or credit note by its number, with `pdfUrl`: a link to its PDF that works for 7 days without a key (null for a void one). Ask again for a fresh link.

Scope: invoices:read

Parameters of getInvoice
ParameterInDescription
numberrequiredpathThe invoice's number
Example request
curl -s "https://orvorental.app/api/v1/invoices/INV-2026-00007" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

The invoice

Webhooks

GET/api/v1/webhook-endpointsList webhook endpoints

Every webhook endpoint of the agency (at most 20, one page), with its events, state and last answer; never its secret.

Scope: webhooks:manage

Example request
curl -s "https://orvorental.app/api/v1/webhook-endpoints" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

The endpoints

{
  "data": [
    {
      "id": "cm4whk0000000000000000001",
      "url": "https://erp.example.com/hooks/orvo",
      "description": "ERP",
      "events": [
        "reservation.created",
        "payment.received"
      ],
      "isActive": true,
      "failureStreak": 0,
      "disabledAt": null,
      "lastDeliveryAt": "2026-11-02T08:31:00.000Z",
      "lastStatus": 200,
      "createdByName": "ERP sync",
      "createdAt": "2026-11-02T08:30:00.000Z",
      "updatedAt": "2026-11-02T08:30:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}

POST/api/v1/webhook-endpointsCreate a webhook endpoint

A new endpoint for the events named. The URL must be https and its host must resolve to public addresses only (422 URL_REFUSED { reason }). The answer carries the signing `secret` this once: every delivery is signed `X-Orvo-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<body>")>`. A repeat with the same Idempotency-Key replays the endpoint with `secret: null`. At most 20 endpoints (409 ENDPOINT_LIMIT).

Scope: webhooks:manage · takes an Idempotency-Key

Body fields: url*, description, events* (* required)

Example request
curl -s -X POST "https://orvorental.app/api/v1/webhook-endpoints" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"url":"https://erp.example.com/hooks/orvo","description":"ERP","events":["reservation.created","payment.received"]}'
Answer 201

The endpoint and its secret (null on a replay)

{
  "id": "cm4whk0000000000000000001",
  "url": "https://erp.example.com/hooks/orvo",
  "description": "ERP",
  "events": [
    "reservation.created",
    "payment.received"
  ],
  "isActive": true,
  "failureStreak": 0,
  "disabledAt": null,
  "lastDeliveryAt": "2026-11-02T08:31:00.000Z",
  "lastStatus": 200,
  "createdByName": "ERP sync",
  "createdAt": "2026-11-02T08:30:00.000Z",
  "updatedAt": "2026-11-02T08:30:00.000Z",
  "secret": "whsec_3q2-7wEXAMPLEsecretDoNotUse0000000000000"
}

DELETE/api/v1/webhook-endpoints/{id}Delete a webhook endpoint

The endpoint and its delivery log; nothing more is sent to it.

Scope: webhooks:manage

Parameters of deleteWebhookEndpoint
ParameterInDescription
idrequiredpathThe endpoint's id
Example request
curl -s -X DELETE "https://orvorental.app/api/v1/webhook-endpoints/cm4whk0000000000000000001" \
  -H "Authorization: Bearer $API_KEY"
Answer 200

Deleted

{
  "id": "cm4whk0000000000000000001",
  "deleted": true
}

Meta

GET/api/v1/openapi.jsonThis document

The OpenAPI 3.1 document of the API. No key needed; cached for 5 minutes.

Scope: none (no key)

Example request
curl -s "https://orvorental.app/api/v1/openapi.json"
Answer 200

The document

Webhooks

Delivered by the webhooks feature: the agency registers HTTPS endpoints in the back office and chooses the events each receives. Every delivery is a POST with a JSON body{ "id", "event", "createdAt", "agency": { "slug" }, "data" } and the headersX-Orvo-Event, X-Orvo-Delivery and X-Orvo-Signature. A 2xx answer within 10 seconds counts as delivered; otherwise it is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours. After 20 failures in a row the endpoint is switched off and the agency told.

Events:

reservation.created
reservation.updated
reservation.status_changed
reservation.cancelled
payment.received
payment.refunded
deposit.updated
invoice.issued
invoice.voided
customer.created
customer.updated
message.received
fine.recharged

Verify every delivery before trusting it. The signature header is t=<unix seconds>,v1=<hex>, where the hex is the HMAC-SHA256, keyed with the endpoint's secret, of the timestamp, a dot and the raw body. Compare in constant time and refuse a timestamp more than five minutes old:

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length / 2 && timingSafeEqual(given, Buffer.from(expected, "hex"));
}