Nimbu Developer Docs

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.

TargetBase pathScope needed
Channel/channels/{slug}write_channels
Blog/blogs/{slug}write_content
Product fields/products/customizationswrite_products
Checkout profile/products/checkout_profiles/{slug}write_products
Customer fields/customers/customizationswrite_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):

TargetAttributes
Channelslug, 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
Blogslug, name, description, seo_title, seo_description, seo_keywords, locales
Checkout profileslug, name, private_fields
Products, customersnone, 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 (on belongs_to / belongs_to_many) is a channel slug, or one of customers, products, orders, pages, articles.
  • options (alias select_options) on select / multi_select is an ordered array of { "slug", "name" }. Slugs must be unique and canonical; options are matched by slug.
  • renamed_from renames 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 _status and _publish_at never appear in exports and cannot be declared.
  • Unknown keys, null for required-by-shape attributes (name, type, required, …) and non-boolean values for boolean attributes are rejected with 422.
  • Checkout profiles only support name, label, type and required. Renames, belongs_to*, calculated and geo fields are rejected, and option slugs must equal the slugified option name. Every applied change creates a new checkout profile version.

Control keys:

KeyTypeMeaning
prunebooleanRemove fields (and select options) that exist on the site but are absent from fields. Default false.
fingerprintstringRequired on apply: the fingerprint returned by the plan you reviewed.
confirm_destructivebooleanRequired (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:

RiskWhen
safeEverything else.
destructiveremove_field, remove_option, toggle_localized, and changing reference (existing references are not remapped) or encrypted (existing values are not migrated).
blockedretype_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.

StatusmessageWhat to do
409Schema changed; plan againSomeone changed the schema since your plan. The body carries a fresh plan; review it and apply with its fingerprint.
409Another site operation is in progress; try againAnother schema apply holds the site lock. Retry shortly.
422Schema contains blocked changes or unresolved referencesThe plan has blocked ops or warnings. Fix the document. Body includes plan.
422Destructive changes require confirm_destructiveReview the destructive ops and resend with "confirm_destructive": true. Body includes plan.
422… references a pruned field; replace or clear it in the schemaA 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.
422other validation messagesInvalid 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 /releases lists releases, newest first, with the usual page / per_page pagination.
  • POST /releases records one and returns 201.

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"
}
  • environment and commit are required. summary is a free-form JSON object of at most 16 KB. forced (default false) marks a forced deploy.
  • actor is set by the server: the backoffice user id, or application:<id> for application tokens. Any actor in the body is ignored.
  • Releases cannot be updated or deleted.

On this page