Skip to content

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.

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.

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/articles
https://cms.example.com/auth/login
https://cms.example.com/graphql

The reference pages omit the base URL and use just the path. Substitute your deployment’s URL when invoking the endpoints.

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.

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.

By default, every request must carry an access token. Two ways to attach one:

  • Authorization headerAuthorization: Bearer <token>. Preferred for server-to-server calls and any context where you control the request.
  • access_token query 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, and Referer headers.

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.

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:

CodeHTTP statusMeaning
FAILED_VALIDATION400Field-level validation rejected the payload.
INVALID_PAYLOAD400The request body was unparseable or structurally wrong.
INVALID_QUERY400Query parameters cannot be combined as requested.
INVALID_CREDENTIALS401Login failed, or a token is missing/invalid.
INVALID_OTP401TFA code rejected.
INVALID_IP401The role’s IP allow list rejected the source IP.
TOKEN_EXPIRED401Token was valid but has aged out.
INVALID_TOKEN403Token is malformed or signed with a different secret.
FORBIDDEN403The role does not have permission for this action, or the item does not exist.
ROUTE_NOT_FOUND404No such endpoint.
UNSUPPORTED_MEDIA_TYPE415Content-Type not accepted by this endpoint.
UNPROCESSABLE_ENTITY422The request was syntactically valid but semantically wrong (a constraint violation, for example).
REQUESTS_EXCEEDED429Hit the rate limit.
SERVICE_UNAVAILABLE503An external dependency (cache, mail, storage) failed.
INTERNAL_SERVER_ERROR500An 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.

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.

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.1
Content-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.

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.

  • 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 /graphql and /graphql/system split, and the differences from REST.
  • SDK — the official client library for JavaScript and TypeScript.
  • System collections — the platform-owned tables and their endpoints.