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
| Type | Holds | Filterable with |
|---|---|---|
string | A single line | eq ne contains starts_with in is_null |
text | Multiple lines, no formatting | eq ne contains starts_with is_null |
richtext | A formatted document | is_null |
number | A number | eq ne gt gte lt lte in is_null |
boolean | True or false | eq ne is_null |
date | A date | eq ne gt gte lt lte is_null |
select | One of a fixed list | eq ne in is_null |
url | A link | eq ne contains starts_with is_null |
image | One picture | is_null |
gallery | Several pictures | is_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
/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.
/v1/content/{type}/entries { "values": { "name": "Ada Lovelace", "role": "engineer" }, "publish": true }
/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