---
title: Content types and entries
description: 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.

> **INFO: The kind cannot change later**
>
> A singleton that becomes a collection would change the response from an object
> to an array, which breaks every frontend reading it. So the kind is immutable
> (`content_type_kind_immutable`). If you are unsure, a collection with one entry
> is the recoverable mistake; a singleton that should have been a collection is
> not.

## 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

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

```json
{
  "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.

> **INFO: Ask for the shape rather than deriving it**
>
> `/v1/introspection` returns a `response_shape` per type describing exactly
> this, key by key, including the extra `url_expires_at` a private project adds
> to every image. It is generated from the same code that serves the response,
> so it is the answer rather than an approximation of it.

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

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

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

```http
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.