Declarative configuration

A JSON manifest describes the desired state of part of an account's catalog and presentation. The API validates the manifest, calculates a signed plan with the differences from the current state, and applies that plan as an asynchronous operation, one step per resource. The manifest itself is never stored. Only the plan is kept, for 24 hours.

The flow has three steps, each with its own permission: validate the manifest, create a plan, and apply the plan. See the Configuration reference for the full schemas.

Manifest (cademi/configuration-manifest/v1)

{
  "$schema": "cademi/configuration-manifest/v1",
  "resources": [
    {"kind": "product", "ref": "course-a", "external_id": "erp-123", "attributes": {"name": "Course A", "status": "published"}},
    {"kind": "module", "ref": "mod-1", "attributes": {"name": "Module 1", "product": {"$ref": "course-a"}}},
    {"kind": "webhook", "ref": "erp", "id": "whk_01J8Z3...", "attributes": {"url": "https://erp.example/hook", "secret": "***"}},
    {"kind": "banner", "ref": "old", "id": "ban_01J8Z2...", "$delete": true}
  ],
  "order": {"showcases": ["shw_01J8Z1...", "shw_01J8Z0..."]}
}
  • Supported kind values in this version: showcase, product, module, lesson, banner, menu_item, settings (all /settings/* groups), webhook, and custom_code. Any other value is rejected with 422 unsupported_resource. The account itself, domains, replicas, imports, exports, event streams, users, and enrollments cannot be managed through a manifest.
  • Identity: every item has a ref, a label that must be unique within the manifest. To target an existing resource, also send its public ID in id, or its external_id where the resource has that field (for example, product). An item without id or external_id is created.
  • attributes accepts the same fields as the create and update operations of the resource. To reference another item in the same manifest, use {"$ref": "<ref>"}. References are resolved in dependency order (showcase → product → module → lesson); the other kinds are independent. A circular reference is reported with the cyclic_reference error code.
  • Deletion is always explicit, with "$delete": true. Existing resources that are omitted from the manifest are left unchanged: nothing is pruned.
  • Ordered sets (order.showcases, order.products.<id>.content, order.menu_items) must be complete. A partial set is rejected with order_set_mismatch.
  • Secrets (webhook.secret and write-only settings fields) are accepted in the manifest but never returned. They appear as "***" in the plan.
  • Limits: the request body can be up to 1 MiB, and the manifest can contain up to 500 items (422 too_many_items).

Validation (POST /configuration/validations, configuration.validate)

Validation never writes anything and never queues work. It returns 200 even when the manifest has errors, so check the valid field:

{
  "data": {
    "object": "validation",
    "valid": false,
    "errors": [{"path": "resources.1.attributes.product", "code": "reference_not_found", "message": "..."}],
    "warnings": [],
    "required_capabilities": ["products.create", "modules.create", "webhooks.update"],
    "unsupported": []
  }
}

Each item is checked against the manifest schema and against the same rules as the resource's create operation (for new items) or update operation (for existing items). References must exist and be accessible with the current credentials. required_capabilities lists the permissions that applying the manifest would require. The Idempotency-Key header is optional here and has no effect.

Plan (POST /configuration/plans, configuration.plan)

This operation is synchronous and does not modify any resource. If the manifest is invalid, the API returns 422 validation_failed with the same error codes as the validation operation, and no plan is created. Otherwise, the response contains the plan:

{
  "data": {
    "object": "plan",
    "id": "pln_01J8Z4...",
    "summary": {"create": 2, "update": 1, "delete": 1, "noop": 0, "skip": 0},
    "steps": [
      {"ref": "course-a", "kind": "product", "action": "create", "id": null, "external_id": "erp-123", "revision": null, "attributes": {...}, "requires": ["products.create"], "changes": {"name": {"from": null, "to": "Course A"}}},
      {"ref": "erp", "kind": "webhook", "action": "update", "id": "whk_01J8Z3...", "revision": "2026-09-24T11:40:12.000000Z", "changes": {"secret": {"from": "***", "to": "***"}}, "requires": ["webhooks.update"]}
    ],
    "revisions": {"whk_01J8Z3...": "2026-09-24T11:40:12.000000Z", "ban_01J8Z2...": "2026-09-23T18:02:47.000000Z"},
    "expires_at": "2026-09-24T12:00:00+00:00",
    "signature": "hmac-sha256..."
  }
}

action is create, update, delete, noop, or skip. For skipped steps, skip_reason explains why, for example replica_readonly for a replicated resource, which is read-only. The plan is valid for 24 hours, as indicated by expires_at. It can only be applied once, and only with the same credentials that created it.

Application (POST /configuration/applications, configuration.apply, Idempotency-Key required)

Send {"plan_id": "pln_...", "signature": "...", "steps": [...]}: the plan exactly as it was returned, with the real secret values in place of "***". Replacing the masked values does not invalidate the signature. Before accepting the request, the API runs these checks in order:

ResponseWhen
404 not_foundThe plan was not found or is not accessible with the current credentials.
409 plan_expiredThe plan has expired or was already applied.
422 plan_invalidThe signature does not match, or a step was modified.
409 plan_staleThe revision of a resource has changed since the plan was calculated. Calculate a new plan.

If every check passes, the API returns 202 with a configuration.apply operation, and the Location header points to it. The operation has one item per step, in dependency order, up to 500 items. Track it as described in Asynchronous operations.

Each item is processed as follows:

  • Permissions and resource access are checked again when the item runs. If the credentials lose a permission between planning and application, only the affected item fails.
  • The item's revision is checked against the current state of the resource. A mismatch fails the item with revision_mismatch, and the item is not retried.
  • The change produces the same result and emits the same public events as calling the resource's own operation.

Steps are not applied as a single transaction. If a step fails, the steps that were already applied remain in effect, and the operation ends as partially_succeeded. Only failed operations with recoverable items can be resumed with POST /operations/{operation_id}/attempts. To complete a partially applied configuration, calculate and apply a new plan.

When the operation finishes, the plan is marked as applied, and the internal event configuration.applied ({plan_id, operation_id, summary}) becomes visible to credentials with audit.read. Repeating the request with the same Idempotency-Key returns the same operation.

Expired plans are permanently removed 7 days after they expire.

On this page