JSON-API Conventions
The anny API follows the JSON-API v1.1 specification. This guide covers the conventions you need to know when making requests.
Content Type
Write operations use the JSON-API media type:
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
Standard application/json is not accepted for POST and PATCH operations.
For GET requests, the API also accepts:
Accept: application/json
Some read-oriented and UI-oriented endpoints return normalized application/json payloads instead of JSON-API resources. Webhook deliveries are always sent as application/json.
Resource Objects
Every API response wraps data in a JSON-API resource object:
{
"data": {
"type": "bookings",
"id": "a1b2c3d4-...",
"attributes": {
"start_date": "2026-04-01T10:00:00+02:00",
"end_date": "2026-04-01T11:00:00+02:00",
"status": "accepted"
},
"relationships": {
"resource": {
"data": { "type": "resources", "id": "r1" }
},
"service": {
"data": { "type": "services", "id": "s1" }
}
}
}
}
Collection responses return "data": [...] as an array.
Common Headers
| Header | Required | Description |
|---|---|---|
Authorization |
Yes (admin/customer APIs) | Bearer {access_token} — see Authentication |
Content-Type |
Yes (POST/PATCH) | application/vnd.api+json |
Accept |
Recommended | application/vnd.api+json |
Accept-Language |
Optional | Locale code (e.g. de-DE, en-US). Controls translated fields and error messages. |
Tenant Context
Admin API requests require an organization context. Pass it as a query parameter:
GET /api/v1/bookings?o={organization_id}
The backend resolves the active organization from:
- The
tenantclaim in the JWT (if present), or - The
oquery parameter
For explicit control, always include ?o={organization_id}.
Including Related Resources
Use the include parameter to sideload related resources in a single request, avoiding N+1 round-trips:
GET /api/v1/bookings?include=resource,service,order.customer
Nested relationships use dot notation. The response adds an included array:
{
"data": { "type": "bookings", "id": "b1", "..." : "..." },
"included": [
{ "type": "resources", "id": "r1", "attributes": { "..." : "..." } },
{ "type": "services", "id": "s1", "attributes": { "..." : "..." } }
]
}
Note: Each resource type defines its own set of allowed include paths. Requesting an unsupported path returns a
400error.
Sparse Fieldsets
Request only the attributes you need to reduce payload size:
GET /api/v1/bookings?fields[bookings]=start_date,end_date,status&fields[resources]=name
This returns only the listed attributes for each resource type.
Filtering
Filter collections using the filter query parameter.
Simple Filters
GET /api/v1/bookings?filter[status]=accepted
Multiple Values
Comma-separated values act as an OR condition:
GET /api/v1/bookings?filter[status]=accepted,requested
Date Range Filters
Range filters use nested from and to keys:
GET /api/v1/bookings?filter[start_date_range][from]=2026-01-01&filter[start_date_range][to]=2026-03-31
Search
Full-text search across indexed fields:
GET /api/v1/customers?filter[search]=jane
Custom Field Filters
Filter by custom field values using the field's UUID:
GET /api/v1/customers?filter[custom_fields][{FIELD_UUID}][f_id]={FIELD_UUID}&filter[custom_fields][{FIELD_UUID}][o]==&filter[custom_fields][{FIELD_UUID}][v]=Premium
Each custom field filter entry requires:
| Key | Description |
|---|---|
f_id |
Custom field UUID |
o |
Operator: =, <>, <, >, <=, >=, in, range |
v |
Value to match (comma-separated for in, colon-separated for range) |
Note: Available filters vary per resource. Requesting an unsupported filter returns a
400error.
Sorting
Sort results with the sort parameter. Prefix with - for descending:
GET /api/v1/bookings?sort=-start_date
Multiple sort fields are comma-separated:
GET /api/v1/bookings?sort=status,-start_date
Note: Each resource type defines its allowed sort parameters. The default sort varies per resource.
Pagination
The API uses offset-based pagination:
GET /api/v1/bookings?page[number]=1&page[size]=25
| Parameter | Default | Description |
|---|---|---|
page[number] |
1 |
Page index (1-based) |
page[size] |
15 |
Items per page |
Response Metadata
Paginated responses include navigation links:
{
"data": [ "..." ],
"meta": {
"page": {
"current-page": 1,
"per-page": 15,
"from": 1,
"to": 15,
"total": 142,
"last-page": 10
}
},
"links": {
"first": "/api/v1/bookings?page[number]=1&page[size]=15",
"last": "/api/v1/bookings?page[number]=10&page[size]=15",
"next": "/api/v1/bookings?page[number]=2&page[size]=15",
"prev": null
}
}
Creating and Updating Resources
Create (POST)
POST /api/v1/bookings
{
"data": {
"type": "bookings",
"attributes": {
"start_date": "2026-04-01T10:00:00+02:00",
"end_date": "2026-04-01T11:00:00+02:00"
},
"relationships": {
"resource": {
"data": { "type": "resources", "id": "r1" }
},
"service": {
"data": { "type": "services", "id": "s1" }
}
}
}
}
Update (PATCH)
Include the id in both the URL and the body:
PATCH /api/v1/bookings/b1
{
"data": {
"type": "bookings",
"id": "b1",
"attributes": {
"status": "canceled"
}
}
}
Only include attributes that are changing — omitted attributes are left unchanged.
Error Responses
Errors follow the JSON-API error format:
{
"errors": [
{
"status": "422",
"title": "Unprocessable Entity",
"detail": "The start date must be before the end date.",
"source": {
"pointer": "/data/attributes/start_date"
}
}
]
}
Common Status Codes
| Code | Meaning |
|---|---|
200 |
Success |
201 |
Resource created |
204 |
Success (no content) |
400 |
Bad request (invalid query parameters, unsupported include/filter) |
401 |
Unauthorized (missing or invalid token) |
403 |
Forbidden (insufficient permissions) |
404 |
Resource not found |
409 |
Conflict (e.g. duplicate, concurrent edit) |
422 |
Validation error (check errors[].detail for specifics) |
429 |
Rate limited |
Combining Parameters
All query parameters can be combined in a single request:
GET /api/v1/bookings?o=42&include=resource,service&filter[status]=accepted&filter[start_date_range][from]=2026-04-01&sort=-start_date&page[size]=25&fields[bookings]=start_date,status