Schema Management
Manage channel, blog, product, customer and checkout-profile fields declaratively: export the current schema, plan a change, apply it with a fingerprint, and record releases.
The schema API lets you keep a data model in a file and converge a site to it. You send the full desired field list; Nimbu diffs it against the live schema, shows you the operations, and only writes when you apply that exact plan. The Nimbu CLI wraps these endpoints and is the recommended way to use them. This page documents the raw HTTP contract; the per-target operations are also in the API reference, for example GET /channels/{channel_slug}/schema and POST /releases.
Send Authorization: Bearer $TOKEN (plus X-Nimbu-Site for user-scoped tokens), as described in
Authentication.
Targets
Every target has the same three endpoints: GET …/schema, POST …/plan, POST …/apply.
| Target | Base path | Scope needed |
|---|---|---|
| Channel | /channels/{slug} | write_channels |
| Blog | /blogs/{slug} | write_content |
| Product fields | /products/customizations | write_products |
| Checkout profile | /products/checkout_profiles/{slug} | write_products |
| Customer fields | /customers/customizations | write_customers |
All three endpoints, including the GET …/schema export, need write access. A read-only token gets
403. Every target also requires a master token, and for channels, products, customers and checkout
profiles the backoffice user behind the token needs permission to change the data model. Sensitive channels additionally need
read_sensitive_channels and write_sensitive_channels, and so does creating a channel with
"sensitive": true.
For channels, blogs and checkout profiles, planning a slug that does not exist yet plans its creation
(a create_target operation). The slug in the path must already be canonical (lowercase, slugified)
and a slug in the body must match it; otherwise you get 422.
Export the current schema
curl "https://api.nimbu.io/channels/events/schema" \
-H "Authorization: Bearer $TOKEN"{
"slug": "events",
"name": "Events",
"description": null,
"sensitive": false,
"publishable": false,
"label_field": "headline",
"title_field": "headline",
"order_by": "created_at",
"order_direction": "desc",
"submittable": false,
"rss_enabled": false,
"fields": [
{ "name": "headline", "label": "Headline", "type": "string", "required": true, "localized": false, "…": "…" },
{ "name": "venue", "label": "Venue", "type": "belongs_to", "reference": "venues", "…": "…" },
{
"name": "kind",
"label": "Kind",
"type": "select",
"options": [
{ "name": "Concert", "slug": "concert" },
{ "name": "Talk", "slug": "talk" }
]
}
]
}A missing channel, blog or checkout profile returns 404. The export is a valid plan document: feeding
it straight back to …/plan yields no operations.
Document format
The body of plan and apply is the export format plus a few control keys.
Target attributes (top level, all optional; only keys you send are compared):
| Target | Attributes |
|---|---|
| Channel | slug, name, description, sensitive, publishable, label_field, title_field, order_by, order_direction, submittable, submittable_fields, submittable_html_fields, submittable_notifications, submittable_receivers, submittable_notification_template, submittable_confirmations, submittable_confirmation_receivers, submittable_confirmation_template, spam_detection, rss_enabled, rss_title, rss_description, rss_title_field, rss_description_field, rss_image_field |
| Blog | slug, name, description, seo_title, seo_description, seo_keywords, locales |
| Checkout profile | slug, name, private_fields |
| Products, customers | none, only fields |
Channel attributes that point at fields (label_field, title_field, order_by, rss_*_field,
submittable_fields, submittable_html_fields) take field names, and can reference fields added
in the same document. label_field also accepts "id"; order_by also accepts created_at,
updated_at or _position. For channels, an rss object with enabled, title, description,
title_field, description_field and image_field is accepted as shorthand for the rss_* keys. Notification
templates must be "default" or the slug of an existing notification.
Fields (fields, required, an ordered array). Each field is an object with name (unique) and any of:
label, type, hint, required, localized, unique, auto_expand, immutable, encrypted,
required_expression, geo_type, calculated_expression, calculation_type, private_storage,
text_formatting, reference, options, renamed_from.
reference(onbelongs_to/belongs_to_many) is a channel slug, or one ofcustomers,products,orders,pages,articles.options(aliasselect_options) onselect/multi_selectis an ordered array of{ "slug", "name" }. Slugs must be unique and canonical; options are matched by slug.renamed_fromrenames an existing field instead of dropping and re-adding it. Entry data is kept.- Field order in the array becomes the field position.
- The managed publication fields
_statusand_publish_atnever appear in exports and cannot be declared. - Unknown keys,
nullfor required-by-shape attributes (name,type,required, …) and non-boolean values for boolean attributes are rejected with422. - Checkout profiles only support
name,label,typeandrequired. Renames,belongs_to*,calculatedandgeofields are rejected, and option slugs must equal the slugified option name. Every applied change creates a new checkout profile version.
Control keys:
| Key | Type | Meaning |
|---|---|---|
prune | boolean | Remove fields (and select options) that exist on the site but are absent from fields. Default false. |
fingerprint | string | Required on apply: the fingerprint returned by the plan you reviewed. |
confirm_destructive | boolean | Required (true) on apply when the plan contains destructive operations. |
Without prune, fields that exist only on the site are kept (moved after the declared ones) and
reported under drift.
Plan
POST …/plan never writes. It returns the fingerprint of the current schema and the operations
needed to reach your document.
curl -X POST "https://api.nimbu.io/channels/events/plan" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Events",
"label_field": "headline",
"fields": [
{ "name": "headline", "label": "Headline", "type": "string", "required": true },
{ "name": "starts_at", "label": "Starts at", "type": "date_time" },
{ "name": "summary", "label": "Summary", "type": "text", "renamed_from": "intro" }
],
"prune": true
}'{
"target": "channel:events",
"fingerprint": "5d1c…e9",
"exists": true,
"ops": [
{ "kind": "add_field", "risk": "safe", "field": "starts_at", "type": "date_time" },
{ "kind": "rename_field", "risk": "safe", "from": "intro", "to": "summary", "entries": 42 },
{ "kind": "remove_field", "risk": "destructive", "field": "legacy_code", "entries_with_value": 7 }
],
"drift": [{ "field": "legacy_code", "type": "string", "note": "exists on target, absent from file" }],
"warnings": [],
"info": []
}Operation kinds: create_target, update_target, add_field, rename_field, retype_field,
update_attr, toggle_localized, remove_field, reorder_fields, add_option, update_option,
remove_option, reorder_options.
Each operation carries a risk:
| Risk | When |
|---|---|
safe | Everything else. |
destructive | remove_field, remove_option, toggle_localized, and changing reference (existing references are not remapped) or encrypted (existing values are not migrated). |
blocked | retype_field outside a compatible family. Only string/text/email and integer/float convert in place; otherwise add a new field and migrate. |
warnings lists unresolved problems, for example a reference to a channel that does not exist yet
("reference 'venues' not found on target; apply it first"). info holds notes such as a
renamed_from source that was not found (the field is added as new instead).
Apply
POST …/apply takes the same document plus the plan's fingerprint. Nimbu re-plans under a site-wide
lock, checks the fingerprint, and saves.
curl -X POST "https://api.nimbu.io/channels/events/apply" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "Events", "fields": [ … ], "prune": true,
"fingerprint": "5d1c…e9", "confirm_destructive": true }'{
"target": "channel:events",
"fingerprint": "a07f…31",
"applied": true,
"ops": [ … ]
}The returned fingerprint is the schema's new state. Applying a document that produces no operations
is a no-op that still returns applied: true.
| Status | message | What to do |
|---|---|---|
409 | Schema changed; plan again | Someone changed the schema since your plan. The body carries a fresh plan; review it and apply with its fingerprint. |
409 | Another site operation is in progress; try again | Another schema apply holds the site lock. Retry shortly. |
422 | Schema contains blocked changes or unresolved references | The plan has blocked ops or warnings. Fix the document. Body includes plan. |
422 | Destructive changes require confirm_destructive | Review the destructive ops and resend with "confirm_destructive": true. Body includes plan. |
422 | … references a pruned field; replace or clear it in the schema | A pruned field is still used by a channel setting (label, title, order, RSS, submittable fields) or a checkout profile's private_fields. Point the setting elsewhere in the same document. |
422 | other validation messages | Invalid document (unknown attributes, bad option slugs, …). |
Error bodies follow the usual envelope: { "message": "…", "plan": { … } } (the plan key only where
noted).
Releases
Releases are an append-only log of deploys for a site: what was shipped, from which commit, by whom. The CLI writes one after a successful deploy; you can write your own from CI.
GET /releaseslists releases, newest first, with the usualpage/per_pagepagination.POST /releasesrecords one and returns201.
Both need channel write access (write_channels on a master token). The CLI calls GET /releases as a
permission preflight before recording.
curl -X POST "https://api.nimbu.io/releases" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"environment": "production",
"commit": "9f3c2a1",
"ref": "main",
"summary": { "channels": ["events"], "theme_files": 12 },
"forced": false
}'{
"id": "66f0a1b2c3d4e5f60718293a",
"environment": "production",
"commit": "9f3c2a1",
"ref": "main",
"actor": "66e1…",
"summary": { "channels": ["events"], "theme_files": 12 },
"forced": false,
"created_at": "2026-09-25T10:00:00.000Z"
}environmentandcommitare required.summaryis a free-form JSON object of at most 16 KB.forced(defaultfalse) marks a forced deploy.actoris set by the server: the backoffice user id, orapplication:<id>for application tokens. Anyactorin the body is ignored.- Releases cannot be updated or deleted.
Realtime API
Receive live channel entry changes over a websocket: request a one-time grant, subscribe to RealtimeApiChannel, renew leases, handle added/changed/removed/resync events, and filter with live queries.
OpenID Connect ("Login with Nimbu")
Use a Nimbu site's customer accounts as an OpenID Connect identity provider: discovery, the authorization code flow, ID tokens, userinfo claims and the storefront consent screen.