Every route is under https://rndez.com/api/v1/open. Answers are JSON. Money is in cents. Dates are YYYY-MM-DD; times are minutes after midnight in the shop’s time zone, and also ISO times in UTC.
Send the key in every call: Authorization: Bearer rndez_live_…. Keep it on your server, never in a web page or an app people download: anyone with the key reads what it may read.
shop:read: See the shop (details, services, team, hours, days closed, free times)
bookings:read: See bookings (with the client’s name only)
clients:read: See clients (emails, phones, notes, visits; also the client’s contact on bookings)
money:read: See money (prices, deposits, paid, tips)
A staff member can make a key for themselves only. It sees the shop and their own bookings, never the client list or money.
The owner sees when each key was last used and every call in the last 30 days, and can turn a key off in one tap. A key that is turned off stops at once.
Test mode
A key that starts with rndez_test_ reaches a private copy of the demo shop, made for your shop: made-up team, services, bookings and clients. It never reaches real data and never sends an email or a text. Switch to a rndez_live_ key when your code works.
Limits
Free: 60 calls a minute per key.
Solo: 300 a minute.
Shop: 1,000 a minute, per location.
Every answer says where you are: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Over the limit you get 429 and a Retry-After header with the seconds to wait. The shop keeps working.
Errors
Every refusal has the same shape, in English and French, with the parameter that is wrong:
{ "error": { "code": "invalid", "message": "limit must be a number from 1 to 500.", "messageFr": "Demande invalide.", "field": "limit" } }
400 invalid: a bad parameter.
401 unauthorized: no key, a wrong key, or a key that is turned off.
403 scope: the key does not have that permission.
404 not_found: nothing with that id in this shop.
429 slow_down: over the limit. Wait Retry-After seconds.
Paging and sync
Lists answer { "data": [...], "next": "…" }. Pass next as ?cursor= to get the next page; it is null on the last one. 100 a page; ?limit= up to 500.
To keep a copy in sync, save the time of your last call and ask only for what changed: ?updatedSince=2026-10-13T09:00:00Z. Cancelled bookings stay in the list with status: "cancelled".
let cursor = null;
const all = [];
do {
const url = new URL("https://rndez.com/api/v1/open/bookings");
url.searchParams.set("updatedSince", lastSync);
if (cursor) url.searchParams.set("cursor", cursor);
const answer = await fetch(url, { headers: { Authorization: `Bearer ${process.env.RNDEZ_KEY}` } });
if (answer.status === 429) { await new Promise((r) => setTimeout(r, Number(answer.headers.get("retry-after")) * 1000)); continue; }
const page = await answer.json();
all.push(...page.data);
cursor = page.next;
} while (cursor);
Every route
All of them are GET for now. Making and changing bookings and clients, and webhooks, come next.