use-compositions-api
When to use
Create, read, list, update, or delete Studio compositions through the managed Studio API service, the REST layer that validates the layout, compresses the ui spec, and forwards the caller's identity, so you don't hand-encode entries against the raw CMA.
Use when the project talks to the Studio API service (/v1/projects/:projectUid/compositions) and you want guardrails: server-side Studio validation, automatic ui compression, composable_uid backfill, a referenced-guard on delete, and list/references endpoints. Phrases: "compositions REST API", "Studio API service", "create composition endpoint", "list compositions", "composition references API", "managed composition API". Do NOT use for: raw-CMA / seed-pipeline / migration authoring where you hold a management token and encode the ui yourself. That's author-composition-via-api. Do NOT use for interactive UI authoring. That's build-section / build-connected-template. The ui-tree shapes themselves are NOT redefined here. They live in author-composition-via-api.
Auth preflight: settle the credential before the first API call. This skill used to carry an exception for the compositions service. That exception is gone: since 2026-08-25 the service forwards OAuth Bearer to the CMA, and since 2026-08-27 registered components does too. OAuth-first applies here like everywhere else, so do not ask the user for a session
authtoken. Resolve it OAuth-first perauthenticate-cma:CS_OAUTH_ACCESS_TOKEN, else the Contentstack MCP's stored session: Read that skill for where it is, even with no MCP connected. Never ask the user for a sessionauthtoken. If nothing resolves, or a refresh fails with400 invalid_refresh_token, hand them! CONTENTSTACK_REGION=<code> npx @contentstack/mcp --auth(it needs a TTY and a browser, so it cannot be run for them) and wait.403 error_code 316is a valid credential aimed at another org: fix the org or theapi_key, do not re-authenticate.
Use the Compositions API (managed Studio service)
Context
The Studio API service is a REST wrapper over the CMS. Instead of building a composition entry by hand, you POST the layout and the service does the mechanical work:
- You send the
uitree uncompressed (a JSON object). The service compresses it tozlib:<base64>. You never touch zlib. - The service validates the tree against Studio's structural rules and returns all problems at once, field-anchored.
composable_uidis backfilled with the entry uid when you omit it, and uniqueness is enforced content-type-wide.- Delete is guarded: it refuses to orphan a referenced composition unless you force it.
This is the counterpart to author-composition-via-api, which drives the raw CMA (management token, self-encoded ui). Same result (a composition entry) at a higher level. Pick this skill when you want the guardrails and are acting as a signed-in user. Pick author-composition-via-api for management-token automation and full low-level control.
Preview: not yet GA. The service is rolling out. Paths and response shapes may change, and it may not be enabled on your region/tenant. If a call 404s at the host itself (not a composition-not-found body), the service isn't deployed for you. Fall back to
author-composition-via-api. A stack-scoped management-token mode is not available yet. Management-token automation must use the raw-CMA path.
Pre-flight checks
- A Studio project exists and is bound to a stack + a compositions content type. If not, run
provision-studio-project. - You have the
projectUid, every route is nested under it. - You have a resolved credential (OAuth Bearer by default, session
authtokenonly as the last rung,authenticate-cma) and theorganization_uidthat owns the project. Both styles are accepted. The service forwards whichever you send.401 error_code 39means neither arrived, not that Bearer was refused. - You know the base URL for the region (see below). Confirm the service answers before scripting against it.
The endpoints
All routes are prefixed with /v1 and nested under the project. Base URL per region, e.g. AWS NA https://composable-studio-api.contentstack.com/v1. EU / Azure / GCP have their own hosts.
| Operation | Method + path |
|---|---|
| Create (placement in body) | POST /v1/projects/{projectUid}/compositions |
| Create a template | POST …/compositions/templates |
| Create a section | POST …/compositions/sections |
| List / query | GET …/compositions |
| Fetch one | GET …/compositions/{uid} |
| List references | GET …/compositions/{uid}/references |
| Update (partial merge) | PUT …/compositions/{uid} |
| Delete (guarded) | DELETE …/compositions/{uid} |
Authentication
Send two headers on every request. The service acts as the calling user: it forwards your own credential to the CMA rather than holding a service credential of its own.
| Header | Value |
|---|---|
$CS_AUTH | authorization: Bearer <oauth> by default. authtoken: <session> only as the last rung, authenticate-cma. Send exactly one. 401 39 means neither arrived |
organization_uid | Org that owns the project |
Three checks, each with its own failure code: a project access denial gives 422, no credential reaching the service gives 401, and when Contentstack rejects the token the upstream 401/403 is preserved. Reads need read access to the project's stack. Writes need write access.
Task
Step 1: Build the ui tree (uncompressed)
Assemble the layout as a plain node object. Do not compress it. That's the service's job. The node anatomy, value sources, Repeater / Condition Block / Section Slot / Binding Override, and the closed prop/node-type sets are the single source of truth in author-composition-via-api (and the Building Blocks API reference). Reuse them. This skill does not restate them.
Step 2: Create the composition
POST the uncompressed payload. Placement comes from the body on the generic route, or from the flavored routes (/templates maps to page, /sections to section).
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": []
}'
On success (201) the response is { notice, composition, warnings? }. The returned composition.ui is the compressed string as stored. composable_uid equals the entry uid unless you supplied one. Non-blocking findings ride along in warnings[].
Step 3: Handle validation
Blocking problems return 422 composition_invalid with every failing path at once: errors: { "<path>": ["<message>"] }. Fix each path and resend. Warnings don't block, surface them but proceed.
Step 4: Read, list, reference
- Fetch one:
GET …/{uid}returns the entry withuicompressed by default. Add?decompression=trueto get the node tree back (anddata_sourcesparsed). - List:
GET …/compositionsreturns lightweight items (uiomitted). Filter withplace_composition_as,composable_uid,title. Paginate withlimit(1 to 100, default 50) /skip. Addinclude_count=truefor the total. Addinclude_ui=true(+decompression=true) to include the spec. - References:
GET …/{uid}/referenceslists what points at a composition. Check this before deleting a section.
Step 5: Update and delete
- Update is a partial merge:
PUT …/{uid}with only the fields you're changing. If you sendui, send it uncompressed. The service re-validates and re-compresses.composable_uidis immutable (422if you try to change it). - Delete:
DELETE …/{uid}. If other compositions reference it, you get409composition_referenced. Pass?force=trueto delete anyway. Send the DELETE without a JSONContent-Typeon an empty body (Fastify rejects that combination with a400).
Step 6: Attach a thumbnail when place_composition_as: "section"
A section created through this API has no ui_preview, so it lands as a blank tile in the Sections accordion and the Section Slot picker. Studio only captures one when an author opens the section and clicks Save. Browsing or reloading never repairs it.
tsx scripts/make-ui-preview.ts --entry <composition-entry-uid>
Routes, trade-offs and pitfalls: author-composition-via-api § Section thumbnails.
This step is mandatory, not optional. A create is not complete until ui_preview is set. If the thumbnail call fails, report it and retry, do not silently continue, and do not describe the section as created. "Cosmetic" means it cannot corrupt data. It does not mean skippable.
Inputs needed from the user
- Region base URL (or confirm which cloud/region the project is in).
projectUid,organization_uid, and a valid credential resolved perauthenticate-cma.- The composition to create (or the
uidto read/update/delete).
Definition of done
A composition created through this API is not finished when the write returns 200. Section compositions need ui_preview, the components they place need thumbnailUrl and CSLP tags, and the project needs Live Preview on, all six rows of complete-the-build. None of them affect rendering, so a green create call proves nothing about them.
Acceptance
- Create returns
201with acomposition.uid.composable_uidis set (backfilled or yours). - The
uiyou sent as an object comes back as azlib:-prefixed string. - A structurally invalid
uireturns422with field-anchorederrors, not a partial write. GET …/{uid}?decompression=trueround-trips your tree.- Deleting a referenced composition is blocked (
409) unless?force=true. - For
place_composition_as: "section",ui_previewis set, verified by re-reading the entry, not assumed from a successful POST. A blank tile means the create is unfinished (Step 6).
Common pitfalls
| Pitfall | Why it bites | Fix |
|---|---|---|
Sending ui pre-compressed | The service compresses for you. A zlib: string is treated as the tree and fails validation | Send the uncompressed node object |
data_sources as a JSON string | The service stringifies it. A string answers 400 | Send a plain array |
Blank composable_uid | A whitespace-only value is rejected, not backfilled | Omit it, or send a real slug. A blank value gives 422 |
Duplicate composable_uid in another locale | Uniqueness is content-type-wide, not per-locale | Expect 409 at create. Pick a unique slug |
Treating 422 stack_not_found as "missing" | It's the access-denied code (Studio convention, not 403) | Check the caller's rights on the project's stack |
DELETE with Content-Type: application/json + empty body | Fastify rejects it with 400 before the handler | Omit the header on a body-less DELETE |
| Host itself 404s | The service isn't deployed for your region/tenant yet | Fall back to author-composition-via-api (raw CMA) |
See also
author-composition-via-api: the raw-CMA path and the canonicalui-tree / binding shapes (management token, self-encoded).understand-authoring-headlessly: theuizlib tree +data_sources+ the six binding types, conceptually.embed-composition: render one managed region inside a code-owned page once the composition exists.provision-studio-project: create the project + compositions content type this API targets.