Features Apograph CMS on GitHub

The delivery API

Content plugin @apograph/content-server@apograph/content-admin

One public API in two protocols, reached with a bearer token scoped to a set of workspaces.

Documents 0.5.2 Updated Edit this page Report a problem

On this page

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    REST

GraphQL 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 envelopeid, 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

RouteWhat it does
GET /api/v1/content-typesThe content types this token can read
GET /api/v1/content-types/:nameOne type’s field schema
GET /api/v1/content/:typeList published entries
GET /api/v1/content/:type/:idOne entry
GET /api/v1/content/:type/:id/relations/:fieldPage one relation field’s links
GET /api/v1/content/:type/:id/mediaOne entry’s media assets
GET /api/v1/content/:type/:id/translationsAn entry’s sibling translations
GET /api/v1/content/:type/group/:groupIdAn entry addressed by translation group
POST /api/v1/content/:typeCreate an entry
PATCH /api/v1/content/:type/:idUpdate an entry
POST /api/v1/content/:type/:id/publishPublish
DELETE /api/v1/content/:type/:idDelete
POST /api/v1/content/:type/bulkCreate and update many
POST /api/v1/media/assetsUpload an asset
GET /api/v1/media/assets/:id/rawDownload an asset
POST /api/v1/graphqlGraphQL queries and mutations
GET /api/v1/graphqlThe 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 OpenAPI

These 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.