Conventions
Money, times and timezones, ids, pagination, errors and languages across the Plek API.
Requests
- Base URL:
https://api.plekbooking.com. Every route starts with/api, except the MCP server (/mcp) and OAuth (/oauth,/.well-known). - Bodies are JSON: send
Content-Type: application/json. Responses are JSON unless the route says otherwise (CSV/XLSX exports, PDF receipts,.icscalendars). - Ids are UUIDs. Public and customer routes name the organisation by its slug (
/api/public/demo/…), staff routes by its id. - Fields are
camelCase; query parameters of lists are mostlycamelCasetoo (startDate,locationId), withpageandper_pagefor paging.
Money
Amounts are integers in the smallest currency unit, always with their currency:
{ "amount": 1500, "currency": "EUR" }
is €15.00. Some responses add a formatted string for display; never parse it. A few older responses send the amount as a numeric string ("1500"): read it as an integer.
Plek never converts currencies. An organisation sells in its default currency, a location may use another, and totals are always per currency.
Times and timezones
- Date-times are ISO 8601 with a UTC offset, for example
2026-11-02T17:30:00+00:00. Compare them as instants; show them in the organisation's timezone (inGET /api/public/{org_slug}/organisation, and on the organisation in the staff API). - Dates without a time (
2026-11-02) are local days in the organisation's timezone: a session "on 2 November" is on that day where it takes place, also around daylight-saving changes. - Date filters on lists (
startDate,endDate) take a date or an ISO 8601 date-time.
Pagination
Lists that can grow are paged with page (from 1) and per_page:
{
"sessions": [ … ],
"pagination": { "total": 87, "per_page": 20, "current_page": 1, "total_pages": 5, "has_more": true }
}
Keep asking for the next page while has_more is true. Sort with sort and direction (asc, desc) where a list offers it; unknown sort keys answer 400. Short lists (locations, activities, roles) come whole.
The MCP server pages differently: a result with nextCursor continues with cursor.
Errors
Errors are JSON with a human-readable error and, where your code can act on it, a stable code:
{ "error": "This session is full.", "code": "no_seats" }
| Status | Means |
|---|---|
400 |
The request is malformed: a missing or wrongly typed field, an unknown sort key. |
401 |
No credentials, or they are invalid, revoked or expired (WWW-Authenticate: Bearer). |
403 |
Authenticated, but the role lacks the permission, or the organisation is not the key's. |
404 |
Not found, or not visible to you (a member limited to some locations gets 404 for the others). |
409 |
The state doesn't allow it now: full, already booked, already cancelled, a space taken. Often worth showing to a person. |
422 |
Understood, but a rule refuses it: invalid input per field (errors or details), a currency mismatch, a party too large. |
429 |
Too many requests: see Rate limits. |
5xx |
Our fault. Retry later with backoff; writes that create something are safe to retry only when the route says so. |
Branch on code, not on error: messages change and may be translated. Each operation in the reference lists the codes it returns. Codes are added over time, so treat an unknown code like its status.
Languages
Send Accept-Language (en, nl, fr, de) to get messages and translated content (activity names, descriptions) in that language, when the organisation offers it. Otherwise you get the organisation's default language.
Permissions
Every staff operation needs a permission (resource:action, for example booking:read); the reference names it. What a key may see can depend on it: prices and payment status only come with payment:read, customer contact details with customer:read. A field you may not see is left out or null rather than refusing the request.