Docs
Dashboard

Concepts

Content types and entries

Singletons, collections, the ten field types, and what a response looks like.

A content type is a shape you define: a name, a kind, and a list of fields. An entry is one filled-in instance of that shape.

Singleton or collection

A type is one of two kinds, and the kind is fixed once the type exists.

Singleton. There is exactly one of it. A homepage hero, a site footer, an “about” page. GET /v1/content/homepage returns its values object directly, with no array and no paging, because there is nothing to page through.

Collection. There are many. Team members, case studies, pricing tiers. GET /v1/content/team-member returns a paginated array.

The field types

TypeHoldsFilterable with
stringA single lineeq ne contains starts_with in is_null
textMultiple lines, no formattingeq ne contains starts_with is_null
richtextA formatted documentis_null
numberA numbereq ne gt gte lt lte in is_null
booleanTrue or falseeq ne is_null
dateA dateeq ne gt gte lt lte is_null
selectOne of a fixed listeq ne in is_null
urlA linkeq ne contains starts_with is_null
imageOne pictureis_null
gallerySeveral picturesis_null

The three unfilterable types are deliberate. richtext is a document tree and image/gallery are asset references. “Does this body contain the word X” is a search feature with its own index, not an equality test on JSON, and pretending otherwise would ship a filter that quietly scans every row.

An operator a field type does not support is refused, not silently ignored. A contains on a boolean is a mistake, and answering it with “0 results” hides the mistake.

What a response looks like

GET /v1/content/{type}
{
  "data": [
    {
      "id": "ent_2c8RkXqGn4wVtYbMhJpDs",
      "values": {
        "name": "Ada Lovelace",
        "role": "engineer",
        "joined": "2024-03-01",
        "photo": {
          "url": "https://cdn.example.com/…",
          "alt": "Ada at her desk",
          "width": 1600,
          "height": 900,
          "content_type": "image/jpeg"
        }
      },
      "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": 20,
    "has_more": false,
    "next_cursor": null,
    "next_offset": null
  }
}

Scalar fields appear as themselves. An image resolves to an object; a gallery to an array of them; a richtext to { html, json, plain, toc }. You never receive a raw asset id.

Only published content is served

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

Fetching an entry that exists but is not published answers entry_not_published or content_not_published, which says so rather than pretending the entry does not exist.

Templates

The dashboard offers templates when you create a type: a gallery, a team member, a pricing tier, a case study and others. Picking one copies the fields into your project.

A copy, not a link, so improving a template later never changes a type somebody is already using, and you are free to rename, delete and add fields the moment it lands. The template is a starting point, not a parent.

Writing from code

Both writes need a secret key.

POST /v1/content/{type}/entries
{ "values": { "name": "Ada Lovelace", "role": "engineer" }, "publish": true }
PUT /v1/content/{type}

Replaces a singleton’s draft, creating its one entry on first save.

Omit publish and you get 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.

The type itself can be created from code too, with PUT /v1/schema/content-types/{type}, which is what lets an agent set a project up end to end.

Updated at, Wednesday, September 2, 2026