Content leaves Apograph through one public API, served in two protocols over the same data, the same tokens and the same rules:
POST /api/v1/graphql GraphQL
GET /api/v1/content/:type RESTGraphQL is a protocol adapter over the REST API’s internals, not a second API. Same bearer tokens, same guards, same scopes, same visibility rules; its resolvers assemble the same objects the REST controllers return. Whichever you pick, the answers agree.
What a public read returns
By default, published entries only. That is the whole contract of this API
for an anonymous consumer, and it is why a read-scoped token is safe to put in
a build pipeline.
curl https://cms.example.com/api/v1/content/article \
-H 'Authorization: Bearer apograph_…'{
"items": [
{
"id": "9c4b1e77-…",
"createdAt": "2026-02-11T09:14:03.221Z",
"updatedAt": "2026-03-02T16:40:55.010Z",
"publishedAt": "2026-02-12T08:00:00.000Z",
"values": { "title": "…", "slug": "…" }
}
],
"page": 1,
"pageSize": 25,
"total": 128,
"pageSize": 25
}Every entry carries an envelope — id, createdAt, updatedAt,
publishedAt, and on localized types locale and localeGroupId — plus a
values bag holding the type’s own fields. The envelope is always returned and
is not selectable.
The four things worth knowing first
Reads are scoped to one workspace. A token is minted over a bucket of one or more workspaces, and each request picks one. With a single-workspace token the header is optional. See API tokens.
Only granted types are visible. A workspace is granted a set of content types. A type the workspace was not granted does not appear in the type list, is not reachable by name, and cannot be hopped into by a filter or an expansion — including in the GraphQL schema, which is built per workspace grant set, so introspection cannot enumerate what you were not given.
Reads are shallow by default. Relations and media are not expanded unless you ask, because the cost scales with the number of expanded fields and most reads want neither. See expanding.
Writes need a full token. Read-only is the default and the common case.
Endpoints
| Route | What it does |
|---|---|
GET /api/v1/content-types | The content types this token can read |
GET /api/v1/content-types/:name | One type’s field schema |
GET /api/v1/content/:type | List published entries |
GET /api/v1/content/:type/:id | One entry |
GET /api/v1/content/:type/:id/relations/:field | Page one relation field’s links |
GET /api/v1/content/:type/:id/media | One entry’s media assets |
GET /api/v1/content/:type/:id/translations | An entry’s sibling translations |
GET /api/v1/content/:type/group/:groupId | An entry addressed by translation group |
POST /api/v1/content/:type | Create an entry |
PATCH /api/v1/content/:type/:id | Update an entry |
POST /api/v1/content/:type/:id/publish | Publish |
DELETE /api/v1/content/:type/:id | Delete |
POST /api/v1/content/:type/bulk | Create and update many |
POST /api/v1/media/assets | Upload an asset |
GET /api/v1/media/assets/:id/raw | Download an asset |
POST /api/v1/graphql | GraphQL queries and mutations |
GET /api/v1/graphql | The schema, as SDL |
Every group-addressed route has an id-addressed twin. See localized reads.
The admin API is a different thing
Everything under /api/ without the v1 prefix — /api/content,
/api/users, /api/workspaces — is the admin application’s own API. It
authenticates with a session cookie, enforces per-user RBAC, and is not
versioned.
It is not a public API, and a bearer token does not reach it. Build against
/api/v1.
The generated reference
A running server serves its full OpenAPI document as an interactive reference:
http://localhost:3000/reference # Scalar UI
http://localhost:3000/reference/json # raw OpenAPIThese pages are the narrative; that document is exhaustive and generated from the code, so it is the place to check an exact parameter or response shape.
Hearing about changes
The API is pull; the webhooks plugin is the push half. An administrator registers an endpoint, and the CMS POSTs a signed envelope to it whenever an entry is created, updated, published, unpublished, deleted, restored or purged — filtered by workspace, event kind and content type. The body carries references, not values: the receiver reads the record back through this API with its own token, so the read passes through the same visibility rules and audience entitlements as any other.
Delivery is at-least-once and unordered, retried on a fixed ladder for about nine hours, and logged per delivery in the admin. Reacting to changes is the integrator’s page: when to poll instead, a receiver that verifies the signature and deduplicates, and the cache-invalidation and rebuild patterns that follow.
Entry events only
Media, account and workspace changes do not fire a webhook, and there is no scheduled publishing to fire one at a future time. A webhook tells you that something changed and which record; it never carries the record.