---
title: Content
description: Reading and writing your own content types with the /v1/content and /v1/schema endpoints.
---

The endpoints for the types **you** define. For the blog, knowledge base and
authors, see [Built-in modules](/modules/blog).

## Fetch a type

```http
GET /v1/content/{type}
```

`{type}` is the type slug, exactly as `content_types[].slug` reports it in
[introspection](/api/introspection).

A **singleton** answers with its values object. A **collection** answers with a
paginated array.

| Parameter | |
|---|---|
| `limit` | Collections only. 1–100, default 20. Refused outside that, not clamped. |
| `cursor` | Collections in their default order. Opaque. |
| `offset` | Collections when `sort` is used. |
| `where[…]` | Filter by the type's own fields. See [filtering](/api/pagination#filtering). |
| `sort` | A field key, or `created_at` / `published_at` / `updated_at`. `-` prefixes descending. |
| `ids` | Comma-separated `ent_…` ids, at most 50. Returns exactly those, in the sort order. |
| `release` | A `rel_…` id. Omit for the default release. |
| `media_urls` | `permanent` to refuse expiring image URLs. |

```bash
curl "https://api.zevcontent.net/v1/content/team-member\
?where[role]=engineer&sort=-joined&limit=10" \
  -H "Authorization: Bearer $ZEVCONTENT_KEY"
```

```json
{
  "data": [
    {
      "id": "ent_2c8RkXqGn4wVtYbMhJpDs",
      "values": { "name": "Ada Lovelace", "role": "engineer", "joined": "2024-03-01" },
      "published_at": "2026-08-14T09:00:00.000Z",
      "updated_at": "2026-08-20T11:12:00.000Z"
    }
  ],
  "meta": {
    "type": "team-member",
    "kind": "collection",
    "release": { "id": "rel_…", "name": "Launch", "default": true },
    "limit": 10,
    "has_more": false,
    "next_cursor": null,
    "next_offset": null
  }
}
```

> **INFO: Sorting changes the paging style**
>
> `sort` switches this endpoint from cursor to offset paging: `next_cursor` goes
> null and `next_offset` fills in. Read both keys and pass back whichever is set.

## Fetch one entry

```http
GET /v1/content/{type}/{entryId}
```

By its `ent_…` id. Use this for a detail page when you already hold the id;
filter the listing instead when you hold a slug-like field value.

Takes `release` and `media_urls`.

## Create an entry

```http
POST /v1/content/{type}/entries
```

**Secret key only.** Collections only.

```json
{
  "values": { "name": "Ada Lovelace", "role": "engineer" },
  "publish": true
}
```

`values` is keyed by field key and validated against the type: a missing
required field is `missing_required_field`, a wrong type is
`invalid_field_value`, an unknown key is `unknown_field` naming the ones that
exist.

Omitting `publish` creates a **draft**. That is the right default for a
migration script or an agent, because content generated by a machine should
usually be reviewed before it is public.

## Save a singleton

```http
PUT /v1/content/{type}
```

**Secret key only.** Singletons only.

Replaces the singleton's draft with `values`, creating its one entry on first
save, and publishes it in the same call with `"publish": true`.

It is a replace, not a merge: `values` is the whole object. A key you omit is
cleared, not left alone.

## The schema itself

```http
GET /v1/schema/content-types
```

**Secret key only.** Every type definition with its fields. These are the same
definitions introspection embeds, on their own endpoint for a client that wants
the schema without the rest of the map.

```http
PUT /v1/schema/content-types/{type}
```

**Secret key only.** Create or update a type and its fields. This is how a
project is modelled from code rather than in the dashboard, and it is what lets
an agent set one up end to end.

Two things are immutable once set, because changing them breaks every frontend
already reading:

- A type's **kind** (`content_type_kind_immutable`). A singleton becoming a
  collection changes the response from an object to an array.
- A field's **type** (`field_type_immutable`). A `string` becoming a `number`
  changes what every reader receives.

Everything else is editable: labels, options, order, and adding or removing
fields.

## Only published content is served

`/v1/content` serves published entries. A draft is invisible here and visible
in the dashboard, which is what lets somebody work on a page for a week without
it appearing on your site.

An entry that exists but is not published answers `entry_not_published` rather
than pretending it does not exist.