Writes need a full-scope token. Everything on this page is a 403 with a
read token.
curl -X POST 'https://cms.example.com/api/v1/content/article' \
-H 'Authorization: Bearer apograph_…' \
-H 'Content-Type: application/json' \
-d '{"values":{"title":"Hello","slug":"hello"}}' const response = await fetch(
'https://cms.example.com/api/v1/content/article',
{
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.APOGRAPH_TOKEN,
'Content-Type': 'application/json'
},
body: JSON.stringify({ values: { title: 'Hello', slug: 'hello' } })
}
);
const draft = await response.json(); // status: 'draft' mutation Create {
createArticle(input: { values: { title: "Hello", slug: "hello" } }) {
id
status
}
} Creating
POST /api/v1/content/:typeName
{
"values": { "title": "Hello", "slug": "hello" },
"locale": "de",
"localeGroupId": "…",
"relations": { "tags": { "link": ["9c4b…"] } }
}A create always produces a draft on a publishable type. There is no status
parameter — publishing is its own call.
| Field | Effect |
|---|---|
values | The field values bag. |
relations | Per-field link changes. See below. |
locale | Which language this row is written in. Create only. |
localeGroupId | Join an existing translation group as a sibling. Create only. |
locale on an update is ignored — an update never re-homes a row’s language.
Passing a localeGroupId on a create is how a new translation of an existing
record is written; the group is verified to exist in the workspace first, so a
typo is a 404 rather than a stray one-row group. A duplicate
(group, locale) is a 409.
Updating
PATCH /api/v1/content/:typeName/:id
Merge semantics: fields you do not mention are left alone. To clear a field,
send it explicitly as null.
Remember that on a publishable type, saving an edit to a live entry moves it
back to draft while keeping published_at — the modified state. Publish again
to make the edit live.
An unknown field name is dropped in silence
A key in values that is not a declared field is ignored — not stored, not
rejected. A misspelt field name writes nothing and returns 200. Read the
response back when you are writing against the API by hand.
Relation deltas
Relations are changed with a delta, not a replacement, so a record with thousands of links never has to be sent whole.
{
"relations": {
"tags": {
"link": ["9c4b…"],
"unlink": ["1f9a…"],
"order": ["9c4b…", "7d2e…"]
}
}
}| Key | Effect |
|---|---|
link | Assign these ids. |
unlink | Unassign these ids. |
order | Reorder. Owning side only. |
by | id (default) or localeGroup. |
Ids you do not mention are left alone.
This is for many and inverse relations only. An owning single relation is
set through values — naming one here is a 400.
Every id must name an entry in the same workspace, or the save is a 422.
Linking across locales
When both the type and its target are localized, a link may not cross locales — the English article links the English tag, and a target in another locale is a 422.
Setting by: "localeGroup" lets you pass translation group ids instead of
entry ids. Each is resolved to that group’s row in this entry’s own locale, so a
client that thinks in stories never has to keep a per-locale id map. A group with
no row in this locale is a 422 telling you to translate it first.
Publishing and deleting
| Route | Effect |
|---|---|
POST /api/v1/content/:type/:id/publish | Runs the publish gate and goes live. |
POST /api/v1/content/:type/:id/unpublish | Back to draft. |
DELETE /api/v1/content/:type/:id | Delete — soft on a paranoid type. |
Each has a group-addressed twin — …/group/:groupId/publish and so on — which
acts on that group’s row for the requested locale.
Publishing re-validates against the type’s rules. A failure is a 422 naming the fields, and the entry stays as it was. See the publish gate.
Bulk
POST /api/v1/content/:typeName/bulk saves up to 50 entries of one type in
one request.
{
"items": [
{ "values": { "title": "New one" } },
{ "id": "9c4b…", "values": { "title": "Edited" } }
]
}An item with an id updates; one without creates. An item naming only a
localeGroupId is genuinely ambiguous — it could be “create this record’s row in
another language” or “update the row it already has there” — so that one shape
requires an explicit op. Every other item infers it.
Each item means exactly what the equivalent POST or PATCH means, merge
semantics and all.
A bulk save is always 200, even when items fail
Items are written one transaction at a time, so a batch cannot roll back as a unit and one bad row does not reject the rest. The response carries a verdict per item in request order, with failures reporting the status and message the single-entry call would have returned — a 422 keeps its per-field issues. Retry the ones you can fix.
The other bulk routes take a list of ids, capped at 100:
| Route | Effect |
|---|---|
POST /api/v1/content/:type/bulk/publish | Publish many |
POST /api/v1/content/:type/bulk/unpublish | Unpublish many |
POST /api/v1/content/:type/bulk/delete | Delete many |
A bulk save requires both content:create and content:update, since one
request may do either — which a full token has.
Reading your writes back
A read token cannot see the draft it just created. A full token can, with
?status=draft or ?status=any. See API tokens.
What to read next
- The media API — uploading an asset so a media field has an id to store.
- Preview and drafts — reading the draft you just created, and the scope rule that allows it.
- Errors and status codes — the shape of a 422, and the verdict list a bulk save returns.