Custom Field Types
Learn how to send and receive file uploads, references, galleries, and other complex field types when working with channel entries via the Nimbu REST API, and how file fields limit uploads.
Channel entries have dynamic custom fields defined per channel. Simple types (string, integer, float, boolean, date, email, json) are sent as primitive values. This page documents the wire format for complex field types: files, references, galleries, and selects.
All examples use the create entry and update entry endpoints:
POST /channels/{channel_id}/entries
PUT /channels/{channel_id}/entries/{entry_id}File Fields
File fields accept uploads as base64-encoded data inside a JSON object with __type: "File".
For larger files, upload straight to storage and send a FileRef instead: see
Direct File Uploads. Each field can limit the size and the kinds of files it takes,
see Upload limits on file and gallery fields.
Upload a file
{
"document": {
"__type": "File",
"filename": "report.pdf",
"attachment": "JVBERi0xLjQKJ..."
}
}| Key | Type | Required | Description |
|---|---|---|---|
__type | string | yes | Must be "File" |
filename | string | yes | Name with extension — used to detect content type |
attachment | string | yes | Raw base64-encoded file content (no data: URI prefix) |
Remove a file
{
"document": {
"__type": "File",
"remove": true
}
}Preserve an existing file
When updating an entry, omit the file field entirely to keep the current file. If you send back the
response object as-is (with url but no attachment), the API also preserves the existing file.
Response format
Without X-Nimbu-Client-Version header:
{
"document": {
"filename": "report.pdf",
"url": "https://cdn.nimbu.io/files/report.pdf",
"content_type": "application/pdf"
}
}With X-Nimbu-Client-Version header set:
{
"document": {
"__type": "File",
"filename": "report.pdf",
"url": "https://cdn.nimbu.io/files/report.pdf",
"content_type": "application/pdf",
"size": 204800,
"width": null,
"height": null,
"version": "abc123",
"checksum": "def456"
}
}For image files, width and height are populated with pixel dimensions.
Code examples
# Encode file and send as base64
curl -X PUT "https://api.nimbu.io/channels/blog/entries/ENTRY_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"document": {
"__type": "File",
"filename": "report.pdf",
"attachment": "'"$(base64 -i report.pdf)"'"
}
}'Upload limits on file and gallery fields
File and gallery fields have three optional settings that control what can be uploaded into them. None of them is set on a new field.
| Option | Type | Fields | Meaning |
|---|---|---|---|
max_file_size | integer (bytes) | file, gallery | Largest file the field takes, from 1 byte up to 50 MB (52428800). Not set: the field has no limit of its own |
allowed_file_types | array of category keys | file, gallery | The kinds of files the field takes. Empty or not set: every category |
storefront_direct_uploads | boolean | file and gallery fields of channel entries, file fields of customers | Website visitors may upload straight to storage for this field. Default false. Not together with write_protected. See Direct File Uploads |
You can set them in three places:
- Admin field editor: "Maximum File Size (MB)", "Allowed File Types" and "Allow Direct Uploads From Website Visitors". The editor shows the size in MB; the API and schema files use bytes.
- Customizations API: in the
customizationsarray of a channel create or update, and in/customers/customizationsand/products/customizations. A field definition sent there takes the same keys. - Schema management: as field attributes in
planandapplydocuments (see Schema Management).
The field definitions you read back only include these keys when they are set: max_file_size and
allowed_file_types when they have a value, storefront_direct_uploads when it is true. Fields
without them look exactly as they did before.
{
"name": "offerte",
"label": "Offerte",
"type": "file",
"max_file_size": 10485760,
"allowed_file_types": ["pdf", "spreadsheets"]
}max_file_size must be a whole number of bytes. A float, a boolean or a string like "10MB" is
refused with must be an integer, and a value outside the range with
must be between 1 byte and 50 MB. An unknown category in allowed_file_types is refused as well,
with the list of keys that field accepts.
File type categories
| Key | Extensions |
|---|---|
images | jpg jpeg png gif webp heic heif bmp tif tiff ico icns avif |
svg | svg |
video | mp4 mov m4v webm mkv avi mpg mpeg wmv ogv hevc |
audio | mp3 m4a aac ogg mid midi aif wma wav |
pdf | pdf |
documents | doc docx odt rtf txt pages tex wpd |
spreadsheets | xls xlsx ods csv numbers |
presentations | ppt pptx pps ppsx odp key |
archives | zip rar 7z tar gz |
design | psd ai eps |
other | css js xml json ttf otf woff woff2 eot fon eml emlx msg oft ost vcf email ics gpx kml kmz shp shx dbf fla flv swf oxt |
Together the categories make up the list of extensions a file field accepts at all. HTML files
(html, htm, xhtml, …) are never accepted, whatever the field allows.
Gallery fields only take images and svg. A gallery image must also be one of jpg, jpeg,
gif, png, bmp, svg or webp, so images on a gallery does not let in formats like heic
or tif.
When the limits are checked
The limits apply to every way a file reaches the field: API writes with base64 or a FileRef,
signed uploads, remote URLs, theme forms, customer account forms, cart fields,
admin forms, CSV and site imports, and new uploads added while duplicating an entry.
Only a new or replaced file is checked. A file that is already stored is never checked again, so tightening a field's limits does not stop you from saving older records.
A file that breaks a limit fails the write with 422 and a validation error on that field (see
Error Handling). The messages read like this:
is too large (max. 10 MB)file type .jpg is not allowed (allowed: PDF, Spreadsheets)
Storefront defaults
Website visitors get stricter defaults, also on fields where nobody set any option:
- 50 MB per file. A field with its own
max_file_sizekeeps the lower of the two. - No
svgand nootherfiles, unless the field lists those categories inallowed_file_types. A field that lists categories gets exactly those.
These defaults apply to storefront writers on every path: theme forms, customer sign-up and
profile forms, cart fields, and storefront API calls with base64, a FileRef or a signed upload.
| Writer | Counts as |
|---|---|
| Theme forms, customer account forms and cart fields on the website | storefront |
| API calls with a customer token, an app token sent with a customer session, a non-master anonymous token, or no credential | storefront |
| Admin users, backend users, site tokens, master tokens | backend |
| Imports, background jobs | backend |
Backend writers only get the field's own options. On a field without options nothing changes for them: every category, and no limit of the field's own.
Single Reference Fields
Single reference fields (belongs_to) link an entry to one entry in another channel.
Set by ID
Pass the entry ID as a plain string:
{
"author": "507f1f77bcf86cd799439011"
}Set by reference object
{
"author": {
"__type": "Reference",
"className": "authors",
"id": "507f1f77bcf86cd799439011"
}
}| Key | Type | Required | Description |
|---|---|---|---|
__type | string | yes | Must be "Reference" |
className | string | yes | Channel slug of the referenced channel |
id | string | yes* | Entry ID (*either id or slug required) |
slug | string | yes* | Entry slug (*either id or slug required) |
Set by slug
{
"author": {
"__type": "Reference",
"className": "authors",
"slug": "john-doe"
}
}Clear a reference
{
"author": null
}Response format
Default (pointer):
{
"author": {
"__type": "Reference",
"className": "authors",
"id": "507f1f77bcf86cd799439011"
}
}With ?include_slugs=true:
{
"author": {
"__type": "Reference",
"className": "authors",
"id": "507f1f77bcf86cd799439011",
"slug": "john-doe"
}
}With ?resolve=author:
{
"author": {
"__type": "Object",
"className": "authors",
"id": "507f1f77bcf86cd799439011",
"name": "John Doe",
"slug": "john-doe"
}
}Multi Reference Fields
Multi reference fields (belongs_to_many) link an entry to multiple entries in another channel.
Replace all references
Pass an array of IDs:
{
"tags": ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"]
}Or use reference objects:
{
"tags": {
"objects": [
{ "__type": "Reference", "className": "tags", "id": "507f1f77bcf86cd799439011" },
{ "__type": "Reference", "className": "tags", "id": "507f1f77bcf86cd799439012" }
]
}
}Add references
Append to existing references without removing current ones:
{
"tags": {
"__op": "AddReference",
"objects": [
{ "__type": "Reference", "className": "tags", "id": "507f1f77bcf86cd799439011" }
]
}
}Remove references
Remove specific references while keeping the rest:
{
"tags": {
"__op": "RemoveReference",
"objects": [
{ "__type": "Reference", "className": "tags", "id": "507f1f77bcf86cd799439011" }
]
}
}Batch operations
Combine add and remove in a single request. The list of steps goes in changes; they run in
order:
{
"tags": {
"__op": "Batch",
"changes": [
{
"__op": "AddReference",
"objects": [
{ "__type": "Reference", "className": "tags", "id": "new-tag-id" }
]
},
{
"__op": "RemoveReference",
"objects": [
{ "__type": "Reference", "className": "tags", "id": "old-tag-id" }
]
}
]
}
}Accepted __op values
__op | Effect |
|---|---|
AddReference / AddRelation | add the listed objects to the current references |
RemoveReference / RemoveRelation | remove the listed objects |
Batch | apply each entry in changes in order (entries use the same __op values) |
none (omitted or "") | replace all references with objects (a hash without an objects array changes nothing) |
Any other __op, including one nested inside a Batch, fails the whole request with 400
{ "message": "Invalid type for 'tags.__op': expected one of AddReference, AddRelation, RemoveReference, RemoveRelation, Batch" }.
Objects that are not { "__type": "Reference" } with a matching className are skipped. A
reference can also use "slug" instead of "id".
Response format
{
"tags": {
"__type": "Relation",
"className": "tags",
"objects": [
{ "__type": "Reference", "className": "tags", "id": "507f1f77bcf86cd799439011" },
{ "__type": "Reference", "className": "tags", "id": "507f1f77bcf86cd799439012" }
]
}
}Gallery Fields
Gallery fields hold ordered collections of images with optional captions.
Set gallery images
{
"photos": {
"__type": "Gallery",
"images": [
{
"__type": "GalleryImage",
"file": {
"__type": "File",
"filename": "hero.jpg",
"attachment": "/9j/4AAQSkZJRg..."
},
"caption": "Hero image",
"position": 0
},
{
"__type": "GalleryImage",
"file": {
"__type": "File",
"filename": "detail.jpg",
"attachment": "/9j/4AAQSkZJRg..."
},
"caption": "Detail shot",
"position": 1
}
]
}
}Each GalleryImage accepts:
| Key | Type | Required | Description |
|---|---|---|---|
__type | string | yes | Must be "GalleryImage" |
file | File object | for new images | File upload (same format as file fields) |
caption | string | no | Image caption text |
position | integer | no | Sort order (0-based) |
id | string | for existing | ID of an existing gallery image |
Update an existing image
Include the id of the existing image and only the fields you want to change:
{
"photos": {
"__type": "Gallery",
"images": [
{
"__type": "GalleryImage",
"id": "existing-image-id",
"caption": "Updated caption"
}
]
}
}Remove a gallery image
{
"photos": {
"__type": "Gallery",
"images": [
{
"__type": "GalleryImage",
"id": "image-to-delete",
"remove": true
}
]
}
}Response format
{
"photos": [
{
"id": "img-001",
"caption": "Hero image",
"position": 0,
"file": {
"filename": "hero.jpg",
"url": "https://cdn.nimbu.io/galleries/hero.jpg",
"content_type": "image/jpeg"
}
}
]
}With X-Nimbu-Client-Version header, each image file includes size, width, height, version,
and checksum.
Select & Multi-Select Fields
Select (single)
Pass the option ID as a string:
{
"status": "published"
}Multi-Select
Pass an array of option IDs:
{
"categories": ["news", "featured", "technology"]
}Time Fields
A time field stores a wall-clock time of day (for example opening hours), not a moment in time.
Reading
Reads return an ISO-8601 timestamp in the site timezone, anchored to 2000-01-01. For a site in Europe/Brussels, 19:00 reads back as:
{ "opens_at": "2000-01-01T19:00:00.000+01:00" }Only the time digits in the site timezone mean anything. Ignore the date. Localized time fields use
the same format inside translations.
Writing
Send either:
- a plain time:
"19:00"or"19:00:30" - any ISO-8601 timestamp with an offset or
Z: it is converted to the site timezone and the resulting wall-clock time is stored
{ "opens_at": "19:00" }Because of that conversion, a client that parses the value it read and re-sends it normalised to
UTC ("2000-01-01T18:00:00.000Z" for the Brussels example) still stores 19:00. A timestamp on
another date is converted using that date's offset, so daylight saving time can shift the result by
an hour. Send "HH:MM" when you only mean a time of day.
Response Metadata
The X-Nimbu-Client-Version request header controls how much metadata the API returns for file and
gallery fields.
Without the header, file fields return a compact object:
{ "filename": "photo.jpg", "url": "https://...", "content_type": "image/jpeg" }With the header set (any value), file fields return full metadata:
{
"__type": "File",
"filename": "photo.jpg",
"url": "https://...",
"content_type": "image/jpeg",
"size": 245760,
"width": 1920,
"height": 1080,
"version": "v1a2b3",
"checksum": "sha256..."
}Resolving References
When fetching entries, use query parameters to control how references are returned:
| Parameter | Example | Effect |
|---|---|---|
include_slugs | ?include_slugs=true | Adds slug to each reference pointer |
resolve | ?resolve=author | Inlines the full referenced object |
resolve | ?resolve=author,tags | Resolves multiple fields (comma-separated) |
Example — fetch an entry with resolved author:
GET /channels/blog/entries/ENTRY_ID?resolve=authorComplete Example
Creating an entry with multiple field types combined:
{
"title": "New Blog Post",
"body": "Article content here...",
"published": true,
"publish_date": "2025-03-15",
"hero_image": {
"__type": "File",
"filename": "hero.jpg",
"attachment": "/9j/4AAQSkZJRg..."
},
"author": {
"__type": "Reference",
"className": "authors",
"slug": "john-doe"
},
"tags": {
"objects": [
{ "__type": "Reference", "className": "tags", "slug": "javascript" },
{ "__type": "Reference", "className": "tags", "slug": "tutorial" }
]
},
"gallery": {
"__type": "Gallery",
"images": [
{
"__type": "GalleryImage",
"file": {
"__type": "File",
"filename": "screenshot-1.png",
"attachment": "iVBORw0KGgo..."
},
"caption": "Step 1",
"position": 0
}
]
},
"status": "published",
"related_topics": ["news", "featured"]
}curl -X POST "https://api.nimbu.io/channels/blog/entries" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d @entry.jsonLocalized Fields (content_locale)
Channels can mark custom fields as localized. On a site with more than one locale, the entry JSON
returns those fields in the site's default locale at the top level and every locale under
translations:
{
"id": "66f0a1b2c3d4e5f60718293a",
"title": "Our story",
"translations": {
"nl": { "title": "Ons verhaal" },
"en": { "title": "Our story" }
}
}Reading
?content_locale=<code> selects the working locale for a request. The locale must be enabled on the
site, otherwise the API answers 422 Invalid content_locale: <code> is not enabled for this site.
On reads it does not change the top-level values — those stay in the default locale — but it
scopes locale-aware operations such as search:
curl "https://api.nimbu.io/channels/blog/entries?search=story&content_locale=nl" \
-H "Authorization: Bearer $TOKEN"Read translated values from the translations map, not from the top-level fields.
Writing
Pass content_locale on the update request to write the localized fields of that locale:
curl -X PUT "https://api.nimbu.io/channels/blog/entries/66f0a1b2c3d4e5f60718293a?content_locale=en" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "title": "Our story" }'Only the en value of each localized field you send is written; the default-locale value and other
locales stay untouched. Without content_locale, the write goes to the default locale. A locale that
is not enabled on the site returns 422.
Non-localized fields ignore content_locale and are always written as-is. Pages accept a nested
translations object for multi-locale writes in one request; see
Working with Pages.
Common Gotchas
__typeis required on File, GalleryImage, and Gallery objects. For references,__typeis required when using the object format — but single references also accept a plain ID string.- Base64 must be raw — do not include
data:image/png;base64,prefixes. Send only the base64 string. classNameis the channel slug — use the human-readable channel identifier (e.g."authors","blog-posts"), not an internal class name.- Multi-reference semantics — sending an array of IDs, or
{ "objects": [...] }without__op, replaces all references. UseAddReference/RemoveReference(orBatchwith achangesarray) to modify incrementally. An unknown__opreturns 400; nothing is written. - Gallery image ordering — set
positionexplicitly. Images without a position may appear in arbitrary order. - File size limits — base64 encoding increases payload size by ~33%. For large files, use a
direct upload: sign,
PUTthe file to storage, then send theFileRefas the field value. It works for file and gallery fields on entries, customers and products. A field can also set its ownmax_file_size, and storefront writers are capped at 50 MB per file (see Upload limits). - Omit unchanged fields — when updating an entry, only include fields you want to change. Omitted
fields are preserved. Sending a file field without
attachmentpreserves the existing file.
Channel Access Control
How channel-level ACLs and per-entry ACLs work for channel entries in the REST API, themes, and customer-session requests.
Direct File Uploads
Upload files for channel entry, customer and product file fields straight to storage with a signed URL, then attach them with a FileRef instead of sending base64.