blog-posts), an optional description and a list of fields.
A field has:
- a name shown to editors (“Cover image”);
- a key, the name its value has in a record’s
data(cover_image); - a type, such as text, number or relation;
- required: whether every record must have a value (not for
booleanfields: a boolean is true or false, and no value reads as false); - multiple: whether the value is a list (not available for
boolean,long_text,rich_textandjson); - a type-specific configuration, such as a maximum length or enum options (the API reference lists each type’s).
Naming rules
These field keys are reserved, for records’ own attributes and a few kept for later:
id, object, collection, data, status, published_at, created_at, updated_at, deleted, deleted_at, purge_after, revision, schema_version.
Field types
Every type’s configuration also takes a
description (Help text in the web app, up to 500 characters) shown to editors, and the search options searchable, filterable and sortable (Search options).
A collection has at most 100 fields, and a record holds at most 2,000,000 bytes of values, all its fields together (too_large on data). A collection that needs more is usually two: move a group of its fields into a collection of their own and point to it with a relation.
Values are never converted between types: "5" is not an integer and 1 is not a boolean. With multiple on, the value is a list of distinct values of the field’s type, such as a list of tags or photos; a repeated value is refused (duplicate_value).
Links inside rich text must be
http, https, mailto, tel or relative URLs. Your site renders this content, so the API rejects javascript: and data: URLs.Formats and unique values
Atext field can have a format, and values that don’t match it are refused with invalid_format. A format sets its own length limits, so min_length and max_length are for plain text:
text and integer fields with one value can be unique: no two records of the collection have the same value. Emails compare ignoring capitals, records without a value don’t count, and a deleted record frees its values. A value another record has is refused with taken, and the error names that record’s record_id. Turning unique on, in place or with a field migration (such as turning a text field into a slug), is refused while records would share a value (duplicate_values, naming them); fix them first.
A slug for your URLs is a text field with format: slug. In the web app’s Type list, Slug, URL and Email are types of their own: text fields with that format. A slug is always unique (the field has unique: true), and editors type it themselves. Look a record up by slug with filter[slug]=hello-world. For slugs that only need to be unique within a section, like intro under several versions, use plain text.
Dates
A plaindate field holds a day, YYYY-MM-DD, with no time and no time zone. It is the same day for everyone, so a publish date or a birthday never shows as the day before. Print it as it is: in JavaScript, new Date("2026-10-09") is midnight UTC, which is still 8 October in the Americas.
With include_time: true the field holds a moment, such as an event’s start. Send it with any offset (2026-10-09T19:00:00+02:00 or 2026-10-09T17:00:00Z); it is stored in UTC. Responses write it:
- in UTC (
2026-10-09T17:00:00Z), by default; - with the offset of the field’s
time_zone(an IANA name such asEurope/Copenhagen), daylight saving included, when the field has one:2026-10-09T19:00:00+02:00, and2026-12-01T19:00:00+01:00in winter. Your site prints the local time without knowing the zone, and the editor shows and takes times in that zone, whoever is editing.
time_zone never rewrites values: the moments stay, and only how they are written changes. Turning include_time on or off does rewrite them, so it runs as a field migration. Removing the time takes a time_zone whose day each moment falls on (by default the field’s own, else UTC), and adding it makes each date midnight in the field’s zone.
Rich text
Rich text is stored as a ProseMirror document in the web editor’s vocabulary. Write it as Markdown, or as the document, and read it back as the document, as HTML or as Markdown.Write Markdown
A Markdown string (GitHub Flavored) in a rich text field is stored as the document it reads as:### become ###, task list items plain items, and footnotes and raw HTML their text. Images are the project’s assets, written  or with an asset’s image_url; any other image is refused (invalid_format). There is no HTML input: convert HTML to Markdown first.
The document
A document uses the web editor’s nodes and marks, and nothing else, so whatever is stored edits in the web app and renders:- Blocks:
paragraph,heading(level1 to 3),blockquote,bulletListandorderedList(start,type) oflistItem(each starting with a paragraph),codeBlock(language),horizontalRule,tableoftableRowoftableHeaderandtableCell(colspan,rowspan,colwidth,align), and images. Any block can have adir(auto,ltrorrtl). - Inline:
textandhardBreak. - Marks on text:
bold,italic,strike,underline,code(on its own) andlink(href, and optionallytitle,target,relandclass, which Tiptap adds).
invalid_format, naming ours where one fits: contains an unknown mark "strong"; use "bold". These are Tiptap’s names, so documents from a Tiptap editor with StarterKit and tables need no conversion.
Images in rich text
An image in a rich text document is one of the project’s assets, by ID, with its alt text:- Images go between the document’s blocks, not inside a list, a quote or a table. In Markdown, an image inside one moves to after it.
- A newly added image must be an uploaded JPEG, PNG, GIF, WebP, AVIF or SVG of the project; anything else is refused (
invalid_reference,type_not_accepted). An image already in a document stays when its asset is deleted later. - A document holds at most 100 images (
too_many_images). For more, an asset field with multiple on suits a gallery better. altis up to 1,000 characters, and search matches it.
Read HTML or Markdown
Passrich_text=html or rich_text=markdown to any request that returns records or drafts, and each rich text value is a string instead of the document:
- HTML is a fragment of the elements above (
p,h1–h3,ul,ol,table,pre,a,strongand so on), with every text and attribute escaped, to put in a page as it is. Images are WebP renditions at most 1,600 pixels wide with theirwidthandheight(an SVG as itself); one whose asset was deleted is left out. - Markdown reads back as the same document, so an agent or script can read it, change it and write it back. Markdown has no underline or merged table cells, so those are lost on the way, and text that looks like a URL or an email address comes back as a link. Images are written with their
image_url. - Images come with their URLs, so they need no
expand, and noassets:read. Records embedded withexpandread the same way. - Record history and webhook payloads keep documents.
assets:read scope: expanded then lists the assets its images show, in order, with their dimensions and image_url. Write the size your page needs into each image’s URL (Images), with no further request:
Relations and assets
A relation field points at records in another collection of the same project, set bytarget_collection_id. An asset field points at an uploaded file.
An asset field takes any file unless you limit it with accepted_types: MIME types or type/* wildcards, such as ["image/*", "application/pdf"]. In the web app, these are the field’s Allowed files (images, videos, audio, PDFs). The limit applies to newly written values: files already in records are kept, and a new file of another type is refused (type_not_accepted). To list only the assets a field takes, pass its types to GET /v1/projects/{project_id}/assets: one as filter[content_type], several as filter[content_type][in] (comma-separated). In the SDKs, pass content_type as a list.
Records store only the IDs. Reading content embeds the related records and assets in the same response with expand.
Search options
To filter and sort records, see Reading content. Every collection is indexed for full-text search. Members search in the web app, API keys withsearch:read through the API, and sites and apps with a search token, which searches only collections with Include in public search on. The searchable, filterable and sortable options choose how each field takes part, in every search: words match searchable fields, and filters and sorts need filterable and sortable ones. API searches take the same filter operators, and filter[published_at][exists] (true for published records, false for unpublished ones). searchable decides relevance, not privacy: whoever can search a collection can also read its records.
Changing a collection
You can change a collection’s name and description at any time. Adding fields, renaming them and loosening their options is safe: record data is stored by field ID, so it never moves.- Renaming a field’s key keeps its values. Records come back under the new key, and requests that use the old key keep working for six months, so your code has time to switch.
- Changing a field’s type, making it multiple, or adding or removing a date’s time rewrites stored values, so it runs as a field migration. You can preview how many records convert cleanly before you start, and from the start the collection’s fields can’t change, and once it is running its records can’t either.
- Tightening a field (making it required, lowering a maximum, removing enum options) is refused while existing records would break the new rule. Fix those records first, or run a field migration that clears the values that break it or fills in records without a value.
- Deleting a field or collection is a soft delete, and can be undone for 30 days (until its
purge_after).
schema_version it was last written with.