What the API is for
The API has two kinds of token. Each is issued by someone else and each may do something else.
- A client token is issued by a client in their account and acts only for them: free slots, creating, listing, reading, moving and cancelling their own bookings — under the same rules as in the application, including the cancellation deadline set by the company. It is free and the company's API module does not switch it off.
- A company token is issued by the company in the administration and acts for the company: it creates the company's clients, for example when moving from another system. It has no access to bookings or free slots — those always belong to a particular person.
What the API cannot do yet: list services, payments, webhooks, bulk operations and deleting bookings (a cancelled booking stays in the system).
How to get a token
Company token
- Sign in to the administration as the business administrator.
- In the Clients menu, open Firemní přístup přes API (company API access).
- In the Popis (description) field, write what the token is for (for example “Client import from Excel”) and click Vydat (issue).
- The token is shown only once, right after it is issued. Copy it: we store only its hash and cannot show it again.
The list below the form then shows the description, the beginning of the token and when it was last used. The Zneplatnit (revoke) button stops a token immediately; you issue a new one the same way as the first.
The page is available while the company has the API module switched on (it is on by default). Switching the module off stops all company tokens, not client tokens.
Open token management in the administration
Client token
A client issues a token themselves: Account → API access → Issue token. The same applies: it is shown once and can be revoked at any time.
Authentication
Every request carries the token in the Authorization header with the word Bearer. The token starts with rzv_live_.
Authorization: Bearer rzv_live_…
Always call over https. The base address of the interface:
https://reservuj.cz/api/v1
A missing, invalid or revoked token returns 401 unauthorized. A client token also stops working when the company blocks the client's account.
First call
Start with the GET /me endpoint. It changes nothing, it just tells you who the token belongs to: type, company, scopes and limits. It works with both kinds of token.
curl -s "https://reservuj.cz/api/v1/me" \
-H "Authorization: Bearer $RESERVUJ_TOKEN"
const response = await fetch('https://reservuj.cz/api/v1/me', {
headers: { Authorization: `Bearer ${process.env.RESERVUJ_TOKEN}` },
});
const body = await response.json();
if (!response.ok) {
throw new Error(`${response.status} ${body.error.code}: ${body.error.message}`);
}
console.log(body.data.type, body.data.scopes);
The answer for a company token:
{
"data": {
"type": "company",
"organization_id": 42,
"client_id": null,
"scopes": [
"clients"
],
"rate_limit": {
"limit_per_minute": 60,
"availability_cost": 5
}
}
}
Company token: creating a client
POST /clients with an application/json body. E-mail and surname are required, first name, phone and note are optional (e-mail, first name and surname at most 50 characters, phone 20, note 255).
curl -s -X POST "https://reservuj.cz/api/v1/clients" \
-H "Authorization: Bearer $RESERVUJ_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: import-2026-jana-novakova" \
-d '{"email":"jana.novakova@example.com","surname":"Nováková","name":"Jana","phone":"+420777123456"}'
import { randomUUID } from 'node:crypto';
const idempotencyKey = randomUUID();
const response = await fetch('https://reservuj.cz/api/v1/clients', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RESERVUJ_TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({ email: 'jana.novakova@example.com', surname: 'Nováková', name: 'Jana' }),
});
const body = await response.json();
console.log(response.status, body);
The answer is 201. A known e-mail does not create a second client, it only fills in missing details; the created field tells you what happened.
{
"data": {
"email": "jana.novakova@example.com",
"created": true
}
}
A created client receives no e-mail. Let your clients know in advance that you are moving their details to a new system.
Client token: free slots and bookings
You search free slots for one service, at most 31 days at a time. The service ID (service_id) is the ID of the price list row: the company sees it in the administration in the price list (Ceník) in the ID column and gives it to the client. The API does not list services yet.
curl -s "https://reservuj.cz/api/v1/availability?service_id=123&date_from=2026-11-16&date_to=2026-11-22" \
-H "Authorization: Bearer $RESERVUJ_TOKEN"
{
"data": [
{
"start": "2026-11-17T10:00:00+01:00",
"end": "2026-11-17T11:00:00+01:00",
"service_id": 123,
"provider_id": 7,
"branch_id": 3
}
]
}
You create the booking at a start from the answer. With provider_id and branch_id from the free slot it is made exactly with that worker at that branch; without them the company assigns anyone who is free.
curl -s -X POST "https://reservuj.cz/api/v1/reservations" \
-H "Authorization: Bearer $RESERVUJ_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8d0c6a52-5d0e-4f4e-9a49-2f6b0c3e7a11" \
-d '{"service_id":123,"start":"2026-11-17T10:00:00+01:00","provider_id":7,"branch_id":3,"note":"Poprvé"}'
Times are ISO 8601 with a time offset; a time with another offset is converted to the server time (Europe/Prague). A slot can disappear between the availability query and the booking — then you get 409.
Pagination
GET /reservations returns the client's own bookings ordered by start, page by page. The limit parameter is 1 to 200 (default 50). If the answer contains next_cursor, send it in the cursor parameter of the next request with the same filter (from, to, status); null means the last page. A cursor from another filter returns 400 invalid_cursor.
let cursor = null;
do {
const url = new URL('https://reservuj.cz/api/v1/reservations');
url.searchParams.set('from', '2026-11-16T00:00:00Z');
url.searchParams.set('limit', '200');
if (cursor) {
url.searchParams.set('cursor', cursor);
}
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.RESERVUJ_TOKEN}` },
});
const page = await response.json();
for (const reservation of page.data) {
console.log(reservation.id, reservation.status, reservation.start);
}
cursor = page.next_cursor;
} while (cursor);
Reading bookings returns an ETag header. Send it back in If-None-Match and unchanged data come back as 304 without a body.
Errors and codes
An error always has the same shape. You can rely on the code (error.code); the text (error.message) is for people and may change. Judge a code you do not know by its HTTP status.
{
"error": {
"code": "conflict",
"message": "Služba se platí zálohou online; objednejte ji v rezervačním portálu."
}
}
| Code | HTTP | When it comes |
|---|---|---|
unauthorized |
401 | The token is missing, invalid or revoked, has no scope for this operation, or the company switched the API module off (company token). |
bad_request |
400 | A wrong or missing parameter, a malformed Idempotency-Key. |
invalid_json |
400 | The write body is missing, is not valid JSON or is not an object. |
invalid_cursor |
400 | The cursor is invalid or belongs to another filter. |
idempotency_key_required |
400 | Moving a booking without the Idempotency-Key header. |
not_found |
404 | The record does not exist or the token has no access to it; an unknown path. |
conflict |
409 | The slot is taken, it is too late to cancel or move, the booking cannot be moved, the company is suspended or does not accept bookings yet, the service is paid by an online deposit. |
idempotency_in_progress |
409 | A request with the same Idempotency-Key is still being processed. |
unsupported_media_type |
415 | The write body is not application/json. |
unprocessable |
422 | The input is fine but cannot be accepted: the service is not offered to the client, the time is outside opening hours, too soon or not offered for a move, the worker or branch does not belong to the service. |
idempotency_key_reused |
422 | The same Idempotency-Key with a different request body. |
rate_limited |
429 | The rate limit was exceeded. |
method_not_allowed |
405 | The path does not know this method. |
server_error |
500 | An error on our side; error.details.incident holds a reference for support. |
409 for a service with a deposit. The API cannot book a service the company charges with an online deposit: it answers 409 conflict and writes nothing. Send the client to the company's booking portal, where they pay the deposit.
409 is also returned for a taken slot (someone else took it in the meantime — load the free slots again), a late cancellation or move, a suspended company and a company that does not accept bookings yet.
Someone else's record answers as non-existent (404), not 403 — the answer must not reveal what exists.
With 500 server_error, give support the reference from error.details.incident. You can repeat a write with the same Idempotency-Key; if it already went through, it is not written twice.
Rate limits
Each token has 60 points per sliding minute. An ordinary request costs one point, searching free slots 5 points, because it is computationally demanding.
Every answer carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset. Over the limit you get 429 rate_limited with a Retry-After header: wait the given number of seconds and then continue.
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 60
RateLimit-Remaining: 0
RateLimit-Reset: 17
Retry-After: 17
Requests without a valid token are counted by IP address, so that guessing tokens is not free.
Safe retries (Idempotency-Key)
You can send writes (POST and DELETE) with an Idempotency-Key header — a unique value for one operation, such as a UUID. When the connection drops and you do not know whether the write went through, send the same request again with the same key: you get the stored answer with the Idempotency-Replayed: true header and nothing is written twice.
- The key has 1 to 100 visible ASCII characters, is case-sensitive and is valid for 24 hours, only for the same token.
- The same key with a different body returns 422 idempotency_key_reused, a retry while the first request is running 409 idempotency_in_progress.
- Moving a booking (POST /reservations/{id}/move) requires the header; without it you get 400 idempotency_key_required.
- Success and 409 or 422 refusals are stored. Invalid input and refusals due to the company's current state are not stored, so a corrected or later attempt goes through.
Versions and changes
The interface version is in the address (/api/v1). New fields, enum values or endpoints are added without a new version in the address, so ignore unknown fields and values and treat an unknown booking status as a finished booking that did not take place. Removing something or changing its meaning would mean /api/v2.
Security
- A token does not belong in the browser. Anyone who opens a web page or an app can read its code and then act in your name with the token. That is why the API sends no CORS headers and the browser blocks calls from a foreign web page. Call it from your server.
- Store the token as a secret (an environment variable, a password vault), not in source code or a repository.
- Issue a separate token for each integration with a clear description; if one leaks, you revoke only that one.
- If you suspect a leak, revoke the token at once. Revocation takes effect immediately.
- Do not give the company token to clients — they issue their own token in their account.
Specification (Swagger UI) · Website widget · Getting started