Delivery · 17 / 23
Delivery API
REST and GraphQL over one implementation, with the same token, the same permissions and the same visibility rules.
Request
GET /api/v1/content/article/group/9b2d7c1e?locale=deAuthorization: Bearer orthacms_0TYI4Q…X-Workspace-Id: 3c1e5a… Response
{ "id": "e41f0a2b-…", "status": "published", "locale": "de", "localeGroupId": "9b2d7c1e-…", "publishedAt": "2026-09-20T14:03:11.000Z", "values": { "title": "Die stille Rückkehr der Nachtzüge", "slug": "nachtzuege" }} Request
POST /api/v1/graphql query { article(localeGroupId: "9b2d7c1e-…", locale: "de") { title status author { name } }} Response
{ "data": { "article": { "title": "Die stille Rückkehr der Nachtzüge", "status": "PUBLISHED", "author": { "name": "Mara Lind" } } }} Request
POST /api/v1/mcp { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "content_get", "arguments": { "typeName": "article", "localeGroupId": "9b2d7c1e-…", "locale": "de" } }} Response
{ "jsonrpc": "2.0", "result": { "structuredContent": { "id": "e41f0a2b-…", "status": "published", "values": { "title": "Die stille Rückkehr der Nachtzüge" } } }} How it works
- 01
Create a read-only token for your front end.
- 02
Read entries over REST or GraphQL, by id or by language group.
- 03
Bring relations, media and translations back in the same request.
- REST
- GET /api/v1/content/:type
- GraphQL
- POST /api/v1/graphql
- By default
- Published content only
In depth · For the engineers building the front end
Two protocols that cannot disagree
REST and GraphQL are served by the same code — the same query, the same guards, the same visibility rule.
One implementation, two front doors
GraphQL at /api/v1/graphql calls the same services as the REST routes, so token checks, workspace grants and the published-only rule apply identically. Only a bearer token gets in; an admin session cookie does not.
The same filtered, localised request written as a REST URL and as a GraphQL query, above the single JSON response both return.
The read surface
Show the table
| Route | What it returns |
|---|---|
| GET /v1/content-types | Every type granted to this token’s workspace. |
| GET /v1/content-types/:name | One type’s full field schema. |
| GET /v1/content/:type | Published entries, with filter, sort, fields and locale. |
| GET /v1/content/:type/:id | One published entry. |
| …/relations/:field | A page of one relation field’s links. |
| …/media | Media fields resolved to metadata and URLs. |
| …/translations | The entry’s other language rows. |
Details that decide an integration
- Scoped tokens
- Read or full, granted to specific workspaces; read tokens see published entries only.
- Batch writes
- A full token creates, patches, publishes and deletes in batches, with a verdict per item.
- Batch publish is all-or-nothing
- Candidates are re-validated inside one transaction before they go live.
- The API describes itself
- An OpenAPI document is generated from the same types the routes use.
- The admin’s filter grammar
- The filter you build in the content library is the JSON your front end sends.
- Sparse fieldsets are enforced
- Ask for three fields and you get three; an unknown field is an error.
Questions
How do I know when content changes?
Subscribe a webhook to entry.published and entry.unpublished, or keep polling if you prefer.
Can I read drafts for a preview environment?
Yes, with a full-access token and status=draft or status=any. Give the public site a read-only token and the preview build a full one.
What if an entry lacks the requested language?
It is not found by default. Ask for the fallback and you get the default language’s row.