Authentication, scopes, errors and limits
API keys
Every call to /api/v1/... needs an API key in the Authorization header:
Authorization: Bearer key_live_3f9a1c0...
Create keys in Aphra under Settings → API. You give each key a name (say “Website” or “Zapier”) and one or more scopes. The full key is shown once, right after you create it. Aphra only stores a hash, so a lost key cannot be recovered: revoke it and make a new one.
The settings page shows, per key, the first characters, its scopes, when it was made and when it was last used. A revoked key stops working immediately.
Keep keys on your server. Never put one in JavaScript that runs in a browser or in a mobile app: anyone could read it and see your customer list.
Scopes
A key can only do what its scopes allow. Give each integration the smallest set it needs.
| Scope | Allows |
|---|---|
bookings:read |
GET /api/v1/bookings |
bookings:write |
POST /api/v1/bookings |
customers:read |
GET /api/v1/customers |
invoices:read |
GET /api/v1/invoices |
bookings:write does not include bookings:read. If your integration creates bookings and also lists them, tick both.
Errors
Errors come back as JSON with a single error field and an HTTP status code:
{ "error": "slot_unavailable" }
| Status | Meaning |
|---|---|
400 |
The request is malformed: a missing or invalid field, or a bad query parameter. |
401 |
No key, an unknown key, or a revoked key. |
403 |
The key is valid but lacks the scope for this endpoint. |
404 |
The account or resource does not exist. |
422 |
The request was understood but could not be carried out, for example the time slot is taken. The error field holds a code; see Bookings. |
429 |
Too many requests. See below. |
5xx |
Something went wrong on our side. Retry later. |
Base your code on the status code and, for 422, on the error code. The human-readable messages for 400, 401, 403 and 429 may change and are not translated.
Rate limits
| Endpoints | Limit |
|---|---|
/api/v1/... |
120 requests per minute, per key and IP address |
/api/public/services |
60 requests per minute, per business and IP address |
/api/availability |
60 requests per minute, per business and IP address |
The window is fixed: it resets every 60 seconds, not on a rolling basis. Above the limit you get 429. Wait until the next minute before you try again. There is no Retry-After header yet.
Pagination
List endpoints take limit and offset:
| Parameter | Default | Maximum |
|---|---|---|
limit |
50 | 100 |
offset |
0 |
The response repeats the limit and offset it used. There is no total count. To walk through everything, keep raising offset by limit until a page comes back with fewer items than limit.
curl "https://app.aphrabooking.com/api/v1/customers?limit=100&offset=200" \
-H "Authorization: Bearer $APHRA_KEY"
Dates and times
All timestamps are ISO 8601 in UTC. When you send one, end it with Z: 2026-11-20T14:00:00Z. A time with an offset such as +02:00 is rejected. The business’s own time zone is set in Aphra; convert on your side when you show times to people.
Field names
The /api/v1 endpoints use camelCase (startsAt). The public endpoints and webhook payloads use snake_case (starts_at). That difference is historical. We will not change existing fields without a new API version; see the changelog.