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.
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".
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)"'"
}
}'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 very large files, consider using the standalone upload endpoint first, then reference the upload.
- 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.
Working with Pages
Read and safely change page content through the REST API: the page document, merge vs replace updates, surgical batch operations with ETags and If-Match, dry runs, compact reads, drafts and preview links, schema and item reads, and page versions.