Guide 06 · Developer docs

The APIs: REST, GraphQL and custom endpoints

Every Core dna site answers three kinds of API request under /cdna-api: REST for everything, GraphQL for reads that span several entity types, and web events for endpoints you define yourself as jobs. Which fits which task, one worked request for each, and what is true of all three. Jobs were called hooks until the module was renamed Orchestration; the API's routes still use the old name.

7
units
25
pages

Who this is for. Three readers: the developer building a headless front end that reads content from Core dna, the JavaScript developer adding live behaviour to a server-rendered site, and the integrations engineer moving data between Core dna and another system. Each section below says which of the three it serves. It assumes you can already build and deploy a template (Core dna for site builders) and know what a layout, a layout area and a component are (Input types, layouts, components and images).

Every Core dna site answers three kinds of API request, on its own hostname, under /cdna-api/. Teams tend to reach for REST for everything and then find a home page needs five round trips. Pick the surface first: most of what follows depends on which one you chose.

The units, in order

  1. The REST API: predicting an endpoint, filtering, paging and shaping the response, and the errors you will meet.
  2. Writing content: how a write changes versioned content: revisions, areas, component collections, custom fields and URLs.
  3. Reading with GraphQL: one query across several entity types, with fields separated by commas, not spaces.
  4. Custom endpoints: a job with a web event standing in for code you would otherwise write, and how to secure one.
  5. Credentials and access: authentication, authorisation and the CSRF token.
  6. Client code and imports: the client layer, external ids, bulk reads and failure handling.
  7. AI assistants and the MCP server: what the MCP server does for an editor or an agent.

Three surfaces, one platform

SurfaceURLWhat it is for
REST/cdna-api/<module>/<entity>The complete surface. Everything a site holds can be listed, read, created, edited and deleted here, one entity type per request.
GraphQLPOST /cdna-api/graphqlReads only. Fetches several entity types in one request, with the fields you name and nothing else.
Custom endpoint/cdna-api/webhook/<slug>An endpoint you define in the admin as a job with a web event, with no framework code. A synchronous job answers the request itself, with whatever body and status its actions produce.

Which one to use

Decide before reading further. The consequences in the last three rows are what should decide it, not habit.

RESTGraphQLCustom endpoint
Choose it forWrites, uploads, and any read of one entity typeA read that spans several entity typesA question neither of the others expresses: a calculation, a lookup across records, a response in someone else's format
MethodsGET, POST, PUT, DELETEPOST onlyThe ones set on the web event, from GET, POST, PUT and DELETE
Can writeYesNo. Mutations are rejectedWhatever the job's actions do
Round trips for pages, posts and a menuThreeOneOne
CachingThe platform caches no API response. A GET is keyed by its URL, so your own layer can cache itA POST has no URL to key on: cache it in your application, by queryAs REST, for a GET web event; the job runs on every request that reaches it
Where its reference livesThe API reference, and the models in the generated ReferenceThis section; nothing is generated for itYou write it, because you defined it

REST in one request

For all three readers. A list and a detail call, each with the fields chosen by properties. These are real responses from an example site.

Shell
curl -G 'https://www.example.com/cdna-api/blogs/blogs' \
  --data-urlencode 'properties={ id, name, slug, posts: publishedPostsCount }' \
  --data-urlencode 'limit=2' \
  --data-urlencode 'orderBy=id:ASC' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
JSON
{"result":"success","totalCount":5,"data":[
  {"id":1,"name":"Blogs Listing","slug":"main-blog","posts":12},
  {"id":2,"name":"Events Listing","slug":"events-listing","posts":8}
]}
Shell
curl -G 'https://www.example.com/cdna-api/blogs/blog/1' \
  --data-urlencode 'properties={ blog { id, name, slug, created } }' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
JSON
{"result":"success","data":{"blog":{"id":1,"name":"Blogs Listing","slug":"main-blog","created":"2019-08-27T09:27:38-05:00"}}}

This is the whole of the platform, not a subset: anything an editor can do in the admin to a record has an endpoint here. The REST API unit explains how to predict any endpoint without looking it up, and Writing content how a write changes versioned content: revisions, areas, component collections, custom fields and URLs.

GraphQL in one request

For the headless developer. Two entity types, three fields each, one round trip. The response is keyed by the name you gave each part of the query.

Shell
curl 'https://www.example.com/cdna-api/graphql' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{"query": "query { latest: BlogPost(filter: {blog.id: 1}, first: 3, orderBy: {published: \"DESC\"}) { id, name, fullPath }, sections: Page(filter: {parent.id: 1}, first: 2, orderBy: {position: \"ASC\"}) { id, name, fullPath } }"}'
JSON
{"result":"success","data":{
  "latest":[{"id":35,"name":"New","fullPath":"/blog/new"},{"id":12,"name":"99 problems but a batch ain’t one","fullPath":"/blog/99-problems-but-a-batch-ain-t-one"},{"id":11,"name":"What is Roast Development?","fullPath":"/blog/what-is-roast-development"}],
  "sections":[{"id":79,"name":"Form Example","fullPath":"/form-example"},{"id":78,"name":"Form Integration Example","fullPath":"/form-integration-example"}]
}}

It reads and never writes: every write goes through REST. The GraphQL API has the query shape, the arguments and the rule that keeps a query from failing: fields on one line must be separated by commas, not spaces.

A custom endpoint in one request

For the JavaScript developer and the integrations engineer. This is the surface few people know exists. In the admin, under Orchestration, a job gets a web event trigger with a slug and the HTTP methods it answers. Mark the job synchronous and give it an HTTP response action, and the platform answers the request with the status code, headers and body that action produces. The body is a template, so it can read the request and any records the job's earlier actions fetched. (Jobs were called hooks, and the route still says webhook.)

Shell
curl 'https://www.example.com/cdna-api/webhook/store-hours?date=2026-10-01'
JSON
{"date":"2026-10-01","open":"09:00","close":"17:30","holiday":false}

That response is whatever the job's author wrote; nothing about its shape comes from the platform. Two defaults to know before you build one: a web event is open to anyone unless its authentication flag is set, and that flag admits backend users only, not site members. Anything after the slug in the path is passed to the job as well, so one web event can serve /cdna-api/webhook/store-hours/sydney and /…/melbourne. The whole pattern, with its authentication and its failure modes, is in Custom endpoints.

True of all three

  • One site, one API. There is no central endpoint. Each site answers on its own hostname and sees its own records.
  • No cross-origin headers. The platform sends none, so a browser on another origin cannot read a response. A headless front end calls the API from its server, not from the visitor's browser; scripts on the site's own pages are same-origin and unaffected.
  • Site members are refused. A signed-in member calling a REST or GraphQL endpoint is refused on everything except the routes built for visitors, with a 404 by default, or a 403 where the site’s access-denied setting asks for it (Responses and errors). The routes built for visitors: the cart, checkout and payment methods, form submission, the sign-in and registration calls, and reading the member's own profile at /cdna-api/users/account. Members reach content through templates, not through the API.
  • How a request authenticates (an OAuth access token, the preferred way; an API key, the older one; the session cookie; or Basic credentials) is set out on the API reference's Authentication page.

Covered elsewhere

  • Every endpoint, with parameters and example requests in raw HTTP, cURL, PHP, Python and JavaScript: the API reference.
  • Task-sized: the how-tos under APIs and JavaScript in the how-to library.
  • The records a template can read without any API call: the Models reference.

Contents

7 units, in reading order.

The REST API

How the REST API is laid out, so that you can predict an endpoint instead of looking it up, then its parameters, its answers and the checks that confirm a request did what you asked. For anyone who reads or writes records over REST. 8 pages

Predicting an endpoint Every module answers under /cdna-api on a predictable prefix, and every entity gets the same requests from the same generic controllers. The rule, the handful of prefixes that differ from it, and the standard set, so you can write a request without looking it up. Read more Filters, ordering and paging Everything in a list's query string that is not a shared parameter is a filter on the entity's own fields. How to write one, how to page and order a list, and the two habits that keep a filter honest: check it against a count, and always send a limit. Read more Shaping the response with properties One parameter decides what a response contains: properties, a selector that names fields, renames them, reaches into relations and calls getters with arguments. How to write one, what shape the response then takes, and how a mistake shows up, which is not as an error. Read more Query string or request body Where each part of a request goes: filters and paging in the query string, the record in the body, and properties in either, depending on the method. The long selector that does not fit in a URL, and the one client that cannot send it. Read more Responses and errors Every JSON answer has the same envelope, and the status codes mean the same thing everywhere. What each code tells a client to do, the 401 that has no body, the 403 that means “refresh your token”, and how to put a validation error next to the field that caused it. Read more A relation as an id or an object A payload can set a relation with a bare id or with an object carrying one, and both do the same thing. What the neighbouring forms do, which is not the same thing at all: an object with more fields edits the related record, and one without an id creates a new one. Read more Confirming a request did what you asked What to send for each kind of request, and the checks that confirm a read or a write did what you asked. Each one below was reproduced on this site. Read more The generated documentation What a record looks like in a response is declared in the platform's code, and the documentation of it is generated from that declaration. Which pages are generated, which are written by hand, and which to trust first when they differ. Read more

Writing content

The reference lists the write endpoints; this unit explains how a write behaves, so you choose the write shape that keeps the content you expect. Revisions and the live revision, the three ways to write one, going live and rolling back, writing areas and component collections, custom fields, custom URLs, and writes that may have applied when the client gave up waiting. 8 pages

Versioned content and the live revision Most content does not change when you edit it: the edit becomes a new revision beside the old ones, and only the one marked live is what visitors see. Which records work this way, the three separate things that decide what is on the site, and how to read them. Read more patchRevision, revisions, or a revision by id Three request shapes write the content of a versioned record, and they differ in what they keep. patchRevision copies a revision and changes only what you send; revisions without an id replaces everything; revisions with an id rewrites a revision in place. Which to use, with the two keys that make a patch safe. Read more Making a revision live, rolling back and publishing Choosing which revision is live, going back to an older one, and deciding when visitors see the record at all. The request for each, the routes the types with their own rules use, and how to pin a patch so it does not undo a rollback. Read more Writing areas A layout area takes a value in the shape its input type expects, and that shape is often not the one a read gives back. The write shape for each input type, the ones that fail loudly and the ones that do not, and why a record read with areasContent cannot be sent back as it is. Read more Editing component collections A component collection is a list of components in one layout area: the sections of a page, the blocks of a guide page. Which key changes it, how positions count, why a position can point at the wrong element after a removal, and how to place a global component. Read more Custom fields Custom fields are the extra fields an administrator adds to menus, products, users and a dozen other record types. How to find a record's fields, read them by name, write one without touching the others, and why the answer to a write can look as if fields were lost. Read more Custom URLs and slugs A record's address comes from its slug, or from a custom URL that replaces it. How slugs are made and checked, how to set and clear a custom URL, and three things to get right: clearing a custom URL, setting one only after the record is created, and starting it with a slash. Read more Retries and writes that may have applied A write that timed out may have happened. Which requests are safe to send again, what a repeat of the others does, measured on this site, and how to make a create, a collection edit and a cart change safe to retry. Read more

Reading with GraphQL

The read-only endpoint that fetches several entity types in one round trip, with only the fields you name. Mainly for the headless developer whose front end needs pages, posts and a menu on one screen. 1 page

Custom endpoints without writing framework code

A web event on a synchronous job (called a hook before the module was renamed Orchestration) is an API endpoint you define in the admin, with no framework code. For the JavaScript developer who needs one answer REST does not give, and the integrations engineer whose partner calls the site. 2 pages

Credentials and access

Which credential each kind of client should send, how the platform reads it, and why the same request answers a site member, a backend user and a script differently. For every API reader, and above all the integrations engineer. 2 pages

Client code and imports

How to structure the code that talks to the API: the scripts on a server-rendered or hybrid site, and the server-side imports and syncs that move records in and out. For the JavaScript developer and the integrations engineer. 3 pages

AI assistants and the MCP server

Every Core dna site runs an MCP server, which lets an AI assistant in your editor read and change the site's records and configuration as you. For any developer or editor who works with an assistant. 1 page