Compositions: the ten composition routes

View as Markdown
Last updated September 14, 2026

Auth: every route below takes either a session authtoken or an Open Authorization (OAuth) Authorization: Bearer token. Both are first-class, and the service forwards whichever you used to the Content Management API (CMA). 401 error_code 39 fires only when neither reaches the service. How the credential is chosen.

The composition routes of the managed Studio API service. All paths are prefixed with /v1 and nested under a project. All require a credential plus the organization_uid header. You send the ui tree uncompressed: the service compresses it. For the layout-tree shapes themselves, see Building Blocks. For status codes and validation, see Errors and validation. For the other four route groups and the shared envelope, see the Endpoint reference.

Machine-readable contract. Every route on this page is also described in the OpenAPI 3.1 spec. Generate an MCP tool server, an SDK, or a Postman collection with any OpenAPI-consuming generator. Each operationId doubles as an MCP tool name.

Base path throughout:

/v1/projects/{projectUid}/compositions

The curl examples below use $CS_AUTH for whichever credential header you hold, authorization: Bearer <access_token> for an OAuth client, or authtoken: <session_token> from a browser session:

export CS_AUTH='authorization: Bearer <access_token>'

1. Create a composition

POST /v1/projects/{projectUid}/compositions

Placement comes from the body (place_composition_as). For flavor-scoped variants that set it from the route, see section 2.

Request body

FieldTypeRequiredNotes
titlestringYesat most 256 chars, and not blank: a whitespace-only title is rejected.
place_composition_as"page" | "section"Yespage = template, section = section.
uiobject (node tree)YesUncompressed node object. Root type must be "page" for both flavors.
urlstring-URL pattern (templates). Omit for sections.
composable_uidstring-Stable identity. If omitted, it is backfilled with the entry uid. If provided, it must be non-blank and unique across the content type.
connected_content_typestring-Connected templates only. "" for freeform / sections.
schema_versionstring-Defaults to "1.0.0". Must be a string.
data_sourcesarray-Sent as an array. The service stringifies it. Defaults to [].
linked_sectionsarray-Deprecated. The value you send is discarded. Derived from the ui tree on every write that carries one. See below.
static_value, url_metadata, linked_schemas, symbols, ui_preview--Passed through. See Building Blocks.

Unknown fields are silently stripped (the service uses a whitelist validator).

linked_sections is derived, not accepted. It's a real CMA reference field, and the only thing a reverse lookup ("which templates use this section?") can read. Taking it from the request body let it drift: a caller that changed ui without restating the field left the references describing an older tree, so removed sections kept reporting as in use. The service now recomputes it from ui in the same step that compresses the tree, so the two can't disagree. The field is still accepted for backward compatibility and ignored. On an update that carries no ui, the stored value is left alone rather than blanked: the stored tree hasn't changed, so its references still describe it.

Response: 201 Created

{
  "notice": "Composition created successfully",
  "composition": {
    "uid": "blt9f2c…",
    "title": "Blog Post",
    "composable_uid": "blt9f2c…",   // = uid (backfilled) unless you supplied one
    "place_composition_as": "page",
    "ui": "zlib:eJyrVkrLz1eyUl…",    // COMPRESSED in the response
    "data_sources": "[]",            // JSON-encoded string
    "schema_version": "1.0.0"
  },
  "warnings": [ /* non-blocking findings — present only when any exist */ ]
}

Notable errors

422 composition_invalid (structural validation: all failing paths at once, or a blank composable_uid), 409 composition_uid_conflict (duplicate composable_uid, any locale), 401 / 422 (auth / access). Full catalog: Errors & validation.

curl -X POST 'https://<host>/v1/projects/<projectUid>/compositions' \
  -H "$CS_AUTH" -H 'organization_uid: <org-uid>' \
  -H 'Content-Type: application/json' \
  -d '{ "title": "Blog Post", "place_composition_as": "page",
        "url": "/blog/{{entry.slug}}", "connected_content_type": "blog_post",
        "ui": { "uid": "root", "type": "page", "props": {}, "slots": {}, "metadata": {} },
        "data_sources": [] }'

2. Create a template / section (flavor-scoped)

POST …/compositions/templates, POST …/compositions/sections

Convenience variants that set place_composition_as from the route, so you omit it (and the fields that don't apply). Same response shape as section 1.

RouteSetsBody omits
…/templatesplace_composition_as: "page"place_composition_as, linked_schemas
…/sectionsplace_composition_as: "section"place_composition_as, url, connected_content_type, linked_sections
# section
curl -X POST 'https://<host>/v1/projects/<projectUid>/compositions/sections' \
  -H "$CS_AUTH" -H 'organization_uid: <o>' -H 'Content-Type: application/json' \
  -d '{ "title": "Promo band", "linked_schemas": [],
        "ui": { "uid": "root", "type": "page", "props": {}, "slots": {}, "metadata": {} } }'

3. List compositions

GET /v1/projects/{projectUid}/compositions

Returns lightweight items by default: the heavy ui and data_sources are omitted so a page of many compositions stays small.

Query params

ParamTypeDefaultNotes
place_composition_aspage|section-Filter by flavor.
composable_uidstring-Filter by exact identity.
titlestring-Case-insensitive substring match.
limitint (1 to 100)50Values above 100 are rejected (400).
skipint (0 or more)0Offset pagination.
include_countbooleanfalsetrue adds meta.count with the total.
include_uibooleanfalsetrue makes each item include ui and data_sources.
decompressionbooleanfalseWith include_ui=true, expands each ui to a node tree.
localestringproject localeLocale to read.

Response: 200 OK

{
  "compositions": [
    { "uid": "blt9f2c…", "title": "Blog Post", "composable_uid": "blog-post",
      "place_composition_as": "page", "url": "/blog/{{entry.slug}}",
      "schema_version": "1.0.0" }   // ui + data_sources omitted by default
  ],
  "meta": { "count": 42, "limit": 50, "skip": 0, "returned": 1, "include_ui": false }
}

meta.count is present only when include_count=true. A ui blob that fails to decompress is left compressed rather than failing the whole page.

curl 'https://<host>/v1/projects/<projectUid>/compositions?place_composition_as=section&include_count=true' \
  -H "$CS_AUTH" -H 'organization_uid: <o>'

4. Fetch a composition

GET /v1/projects/{projectUid}/compositions/{uid}

ParamInDefaultNotes
decompressionqueryfalsetrue returns ui as a node object, with data_sources JSON-parsed.
localequeryproject localeLocale to read.
environmentquery-Reserved for reading published content (future), ignored today.

Response: 200 OK

// default (compressed)
{ "composition": { "uid": "blt9f2c…", "ui": "zlib:eJyr…", "data_sources": "[]" },
  "meta": { "compressed": true, "schema_version": "1.0.0" } }

// ?decompression=true
{ "composition": { "uid": "blt9f2c…", "ui": { "uid": "root", "type": "page", … }, "data_sources": [] },
  "meta": { "compressed": false, "schema_version": "1.0.0" } }

meta.compressed tells you which form ui is in. 404 composition_not_found if the uid isn't in the CMS.

5. Fetch, asserting a flavor

GET …/{uid}/template, GET …/{uid}/section

Same as section 4, but the entry must be the requested flavor, otherwise 404 composition_flavor_mismatch. Turns the suffix into a meaningful assertion ("fetch this uid as a template, fail if it isn't") rather than a redundant alias. Accepts the same decompression / locale query params.

curl 'https://<host>/v1/projects/<projectUid>/compositions/<uid>/template?decompression=true' \
  -H "$CS_AUTH" -H 'organization_uid: <o>'
# → 404 composition_flavor_mismatch if <uid> is a section

6. List references

GET /v1/projects/{projectUid}/compositions/{uid}/references

Returns the entries (e.g. templates) that reference this composition, the same signal the delete guard uses. Use it to show "used by N" and warn before deleting.

Response: 200 OK

{ "references": [ { "uid": "blt123", "content_type_uid": "compositions", "title": "Home page" } ],
  "meta": { "count": 1 } }

An unreferenced composition returns { "references": [], "meta": { "count": 0 } }. 404 if the composition doesn't exist. If the reference check itself fails, the call fails closed and surfaces the upstream status.

7. Update a composition

PUT /v1/projects/{projectUid}/compositions/{uid}

A partial merge: send only the fields you're changing. Everything else is preserved.

  • If you send ui, send it uncompressed. The service re-validates and re-compresses it.
  • composable_uid is immutable: sending a different value returns 422 composition_uid_immutable.
  • An empty patch returns the stored entry unchanged (no CMS write).

Response: 200 OK

{ "notice": "Composition updated successfully",
  "composition": { /* the merged entry, ui compressed */ },
  "warnings": [ /* if re-validation produced any */ ] }
curl -X PUT 'https://<host>/v1/projects/<projectUid>/compositions/<uid>' \
  -H "$CS_AUTH" -H 'organization_uid: <o>' -H 'Content-Type: application/json' \
  -d '{ "title": "Renamed" }'

8. Delete a composition

DELETE /v1/projects/{projectUid}/compositions/{uid}

Hard-deletes the entry, guarded by a reference check.

ParamInDefaultNotes
forcequeryfalsetrue deletes even if referenced.

If other compositions reference this one, the delete is blocked with 409 composition_referenced (the referrers are listed) unless ?force=true. If the reference check itself can't complete, the delete is refused (fail-closed) with the upstream status preserved, never silently deleted.

Response: 200 OK

{ "notice": "Composition deleted successfully" }

Client gotcha. Do not send Content-Type: application/json on a body-less DELETE: Fastify rejects that combination with a 400 before the handler runs. Send the DELETE with no Content-Type header.

curl -X DELETE 'https://<host>/v1/projects/<projectUid>/compositions/<uid>?force=true' \
  -H "$CS_AUTH" -H 'organization_uid: <o>'

See also

  • Endpoint reference: all 16 routes in one table, including the ten above, plus the base URLs, authentication model, and request/response conventions they share.
  • Errors and validation: the response envelope, the full status and error_code catalog, and the Studio rules the service enforces.
  • Projects: the record every route on this page resolves its stack, content type, and locale through.