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:

  1. The tenant claim in the JWT (if present), or
  2. The o query parameter

For explicit control, always include ?o={organization_id}.


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 400 error.


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

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 400 error.


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