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, .ics calendars).
  • 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 mostly camelCase too (startDate, locationId), with page and per_page for 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 (in GET /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.