Introduction
CairnCMS exposes the platform through two parallel APIs: a REST API and a GraphQL API. They overlap heavily and most everyday CRUD work the same way through either surface. They are not strictly equivalent, though: a small number of operator-side surfaces (the /schema/* and /config/* endpoints, and a handful of utility routes) are REST-only.
For application code, treat the choice as one of ergonomics. For operator and CI tooling, prefer REST since some of what you need is only there.
This page covers the shape both APIs share: URL conventions, the response envelope, authentication, errors, and the system-versus-user split. Subsequent pages cover individual surfaces in depth.
A schema-driven API
Section titled “A schema-driven API”The API is generated from your data model rather than hand-coded for each project. When you add a collection in the app, a new /items/<collection> REST endpoint and corresponding GraphQL types appear immediately. When you add a field, that field becomes available as a query parameter, in fields[], and as a GraphQL selection. There is no build step and no client code generation required server-side.
Two consequences:
- The API surface differs between deployments. Two CairnCMS instances with different schemas have different APIs. The reference pages describe the conventions that apply to every deployment; the specifics come from your project.
- Permissions filter every request. What an authenticated request can read or write depends on the role’s permissions. Two clients hitting the same endpoint with different tokens may see different fields, different rows, or no access at all. The permission layer is part of the API’s correctness, not a layer in front of it.
Base URL
Section titled “Base URL”Every API path is relative to the deployment’s PUBLIC_URL. For a CairnCMS instance reachable at https://cms.example.com:
https://cms.example.com/items/articleshttps://cms.example.com/auth/loginhttps://cms.example.com/graphqlThe reference pages omit the base URL and use just the path. Substitute your deployment’s URL when invoking the endpoints.
REST or GraphQL
Section titled “REST or GraphQL”For data access, both surfaces cover the same ground: every collection, system or user-defined, can be read and written through either. The right choice for an application client depends on the shape of its queries:
- REST suits straightforward CRUD against a single resource, server-to-server scripts, and tooling that benefits from familiar HTTP semantics (curl, OpenAPI consumers, HTTP-based caches). Most operator scripts and automation use REST.
- GraphQL suits clients that compose deep queries across many relations, fetch only the fields they need, or want a typed schema for code generation. Application frontends often benefit, especially when combined with a query-aware client library.
The operator-side surfaces (/schema/snapshot, /schema/diff, /schema/apply, /config/snapshot, /config/apply) are REST-only. So is asset serving (/assets/<id>) and a handful of utility routes. CI scripts that drive a CairnCMS instance through its lifecycle generally end up on REST for that reason.
You can mix the two in the same project. The official SDK supports both behind the same client, so a single application can use REST for some flows and GraphQL for others without managing two transports.
The response envelope
Section titled “The response envelope”Successful REST responses follow a consistent envelope:
{ "data": { ... }}For collection endpoints that return multiple items, data is an array:
{ "data": [ { "id": 1, "title": "First" }, { "id": 2, "title": "Second" } ]}When the request includes meta=* or a specific meta key, the envelope adds a meta object alongside data:
{ "data": [ ... ], "meta": { "total_count": 42, "filter_count": 7 }}A few endpoints return raw data without the envelope, i.e. the asset-serving routes return file bytes with the appropriate Content-Type, and a small number of utility endpoints return plain JSON for backwards compatibility. Those exceptions are called out on each surface’s reference page.
GraphQL responses follow the standard GraphQL shape:
{ "data": { ... }, "errors": [ ... ]}GraphQL collects partial errors alongside any successful selections rather than failing the entire response. REST returns a single error envelope on any failure.
Authentication
Section titled “Authentication”By default, every request must carry an access token. Two ways to attach one:
Authorizationheader —Authorization: Bearer <token>. Preferred for server-to-server calls and any context where you control the request.access_tokenquery parameter —?access_token=<token>. Supported for compatibility with clients that cannot set headers. Avoid when practical because URLs are routinely captured by browser history, server and CDN logs, andRefererheaders.
Same-origin browser clients accessing /assets/* do not need to attach a token: the refresh-token cookie set during login is used to look up the session and authorize the asset request. The first-party admin app uses this path for protected asset loads.
Endpoints that map to permissions configured for the Public role can be reached without a token. Anything outside the Public role’s permitted set returns 403 FORBIDDEN.
For full coverage of login, refresh, OAuth/OIDC/SAML providers, static tokens, and the refresh-cookie flow, see Authentication.
Errors
Section titled “Errors”REST returns a consistent envelope on failure:
{ "errors": [ { "message": "You don't have permission to access this.", "extensions": { "code": "FORBIDDEN" } } ]}errors[] always exists; multiple errors can be returned in a single response when validation collects more than one issue. Each error has a human-readable message and a machine-readable extensions.code. Treat extensions.code as the authoritative signal for what went wrong — the HTTP status code on the response is informative but not the right thing to switch on, especially when an error array contains entries with different statuses.
Errors from internal failures (uncaught exceptions, database connection problems) sanitize their messages for non-admin requesters to avoid leaking stack traces or implementation details. Admins see the full error.
The codes used across the API:
| Code | HTTP status | Meaning |
|---|---|---|
FAILED_VALIDATION | 400 | Field-level validation rejected the payload. |
INVALID_PAYLOAD | 400 | The request body was unparseable or structurally wrong. |
INVALID_QUERY | 400 | Query parameters cannot be combined as requested. |
INVALID_CREDENTIALS | 401 | Login failed, or a token is missing/invalid. |
INVALID_OTP | 401 | TFA code rejected. |
INVALID_IP | 401 | The role’s IP allow list rejected the source IP. |
TOKEN_EXPIRED | 401 | Token was valid but has aged out. |
INVALID_TOKEN | 403 | Token is malformed or signed with a different secret. |
FORBIDDEN | 403 | The role does not have permission for this action, or the item does not exist. |
ROUTE_NOT_FOUND | 404 | No such endpoint. |
UNSUPPORTED_MEDIA_TYPE | 415 | Content-Type not accepted by this endpoint. |
UNPROCESSABLE_ENTITY | 422 | The request was syntactically valid but semantically wrong (a constraint violation, for example). |
REQUESTS_EXCEEDED | 429 | Hit the rate limit. |
SERVICE_UNAVAILABLE | 503 | An external dependency (cache, mail, storage) failed. |
INTERNAL_SERVER_ERROR | 500 | An unexpected error. |
To prevent leaking information about which items exist, requests for specific items the caller cannot access return 403 FORBIDDEN rather than 404 NOT FOUND. A 404 only comes back for genuinely unmapped routes.
System data and user data
Section titled “System data and user data”Every CairnCMS deployment has two kinds of collections: user collections that you create through the schema, and system collections that the platform owns (directus_users, directus_files, directus_roles, directus_permissions, and so on).
In REST, user collections live under /items/<collection> and system collections live under their own top-level paths (/users, /files, /roles, /permissions, etc.). The two have the same query semantics; the split is about route organization, not capability.
In GraphQL, the namespacing is enforced through two endpoints:
/graphql— user collection types and operations./graphql/system— system collection types and operations.
Both endpoints share the same underlying schema, so deeply-nested relations that cross between user and system data still resolve correctly. The split exists because GraphQL has no built-in namespace mechanism, and putting users alongside a user-defined users collection would collide.
The SEARCH method
Section titled “The SEARCH method”For very large or deeply-nested REST queries, a GET request can run up against URL length limits, particularly when filter trees, deep relations, and long fields[] arrays compound. CairnCMS supports the SEARCH HTTP method as a drop-in replacement for GET, with the query in the request body:
SEARCH /items/articles HTTP/1.1Content-Type: application/json
{ "query": { "filter": { "status": { "_eq": "published" } }, "fields": ["id", "title", "author.name"], "sort": ["-published_at"], "limit": 50 }}The request shape is the same query options that would otherwise be query parameters, wrapped in a top-level query object. The response is identical to the equivalent GET response.
SEARCH is wired on collection-list endpoints — /items/<collection>, /users, /files, and the equivalent top-level routes for other system collections. It is not available on item-detail endpoints (/items/<collection>/<id>) or on utility endpoints like /server/info, where the URL length issue does not arise. Permissions and rate limiting apply the same way they do for GET.
Versioning and stability
Section titled “Versioning and stability”CairnCMS follows semver for the platform release. The HTTP API stability commitment for 1.x:
- Endpoint URLs, request shapes, and response envelopes are stable across minor and patch versions.
- New features add endpoints and fields; existing ones do not change shape.
- Breaking changes happen at major version boundaries and are documented alongside the release.
The auto-generated portions of the API like /items/<collection> and the GraphQL schema change with your data model rather than with platform releases. A renamed field changes your project’s API surface but is not a platform-API breaking change.
Where to go next
Section titled “Where to go next”- Authentication — login, refresh, providers, static tokens, and the cookie flow.
- Items — the conventions for creating, reading, updating, and deleting items in user collections.
- Files — uploading, downloading, transforming, and importing assets.
- Filters and queries — the query DSL shared by REST and GraphQL.
- GraphQL — schema introspection, the
/graphqland/graphql/systemsplit, and the differences from REST. - SDK — the official client library for JavaScript and TypeScript.
- System collections — the platform-owned tables and their endpoints.