Studio Docs

When to Use

Bootstrap a Studio project via API: create the compositions CT with the schema Studio’s editor + runtime require, plus the publish + delivery-token setup.

Use when standing up a brand-new Studio project headlessly (no UI), seeding an environment for tests/demos, or onboarding a new stack with no compositions CT yet. Signals: “create compositions CT via API”, “seed a Studio project”, “bootstrap a stack for Studio”. Do NOT use for composition entries (author-composition-via-api) or stack assets/CTs outside compositions (provision-studio-stack).

Mandatory auth preflight: settle the credential before the first API call. Resolve it OAuth-first per authenticate-cma: CS_OAUTH_ACCESS_TOKEN, else the Contentstack MCP’s stored session. Never ask the user for a session authtoken. If nothing resolves, or a refresh fails with 400 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 316 is a valid credential aimed at another org: fix the org or the api_key, do not re-authenticate.

Provision Studio Project (API-First)

Context

Provisioning a Studio project touches two API surfaces:

  1. Contentstack CMA (api.contentstack.io and regional variants). Creates the stack, the compositions content type, the environment, the delivery + preview token, and enables Live Preview on the stack.
  2. Studio API (composable-studio-api.contentstack.com and regional variants). Creates the Studio project itself via /v1/projects, binds it to the stack (connectedStackApiKey) and the compositions CT (contentTypeUid), and configures environment + locale + Freeform.

Skip either surface and Studio cannot resolve compositions:

  • Without a compositions CT there is nothing to author against.
  • Without a Studio project the canvas iframe has no project to load.
  • Without Live Preview enabled on the stack the canvas iframe hangs forever on “Loading composition…” because the canvas-app’s rest-preview.contentstack.* calls fail CORS preflight.

The CT must carry an exact set of fields. If any are missing, mistyped, or wrong cardinality, the editor’s ?include[]=linked_sections fails, the runtime’s composable_uid lookup misses, or the canvas cannot persist the composition.

Once the project exists, composition entries are created via author-composition-via-api. Stack content outside the compositions CT (assets, page CTs the compositions reference) belongs in provision-studio-stack.

Reference: reference_studio_provisioning_api for the host map across regions.

Task

  1. Confirm prerequisites. You have a resolved CMA credential, OAuth Bearer by default, session authtoken only as the last rung (authenticate-cma), AND api_key for the target stack. Both Studio API and CMA expect these two headers together. Know the region: regional host map in reference_studio_provisioning_api. NA defaults: CMA api.contentstack.io, Studio API composable-studio-api.contentstack.com. Non-prod (the non-prod domain your team uses) has no region shortcut. Every host is <env>-<service>.<non-prod-domain> and must be set explicitly. The full table is install-studio § Non-prod hosts. Establish which data center you’re on first via analyze-project-fit § 2b: Determine the data center. Getting this wrong here provisions a CT, tokens and a Studio project in the wrong data center, with no error. The calls succeed against prod.

    MCP-first execution. Discover tools and verify the target with a read call per install-contentstack-mcp. When supported tools are callable, the CMA steps here run as MCP tool calls with no pasted authtoken: step 8 CT-create (POST then PUT) becomes create_a_content_type plus update_content_type, step 4 environment-create becomes create_an_environment, and verify becomes get_a_single_content_type. The MCP does not cover the Studio API or tokens: steps 5 (enable Live Preview), 6 (delivery+preview token), 10 (/v1/projects register), and stack-create stay raw HTTP, authenticated with the same OAuth credential (-H "$CS_AUTH"), not a pasted session token. Step 10 (/v1/projects) has no MCP tool, but try the OAuth token there first (Authorization: Bearer). A session authtoken is the fallback only if the API rejects it. See step 10’s callout. The MCP also can’t discover org_id / stack api_key. Supply them (Playwright MCP CS_RECENT_STACK_API_KEY, or paste). A missing readiness flag requires discovery; use HTTP only for unavailable operations or a recorded connection limitation.

  2. FIRST: does a Studio project already exist? List, then ASK. Never create blind.

    This skill creates a project. Run it after an OAuth login without looking and you get a duplicate project against the same stack. Nothing errors, and from then on half the tooling points at one project and half at the other. The stack step below already reuses-or-creates. The project needs the same treatment, plus an explicit question, because “reuse” vs “new” is the user’s call and not inferable.

    curl -s "https://<studio-api-host>/v1/projects" \
      -H "Authorization: Bearer <oauth-access-token>" \
      -H "organization_uid: <org-uid>" | jq -r \
      '.projects[] | "\(.uid)  \(.name)  stack=\(.connectedStackApiKey)  ct=\(.contentTypeUid)"'

    (Auth ladder + host map: step 10’s callout. organization_uid, not a stack api_key. Projects are org-scoped.)

    ResultWhat to do
    A project’s connectedStackApiKey matches the target stackAlmost certainly the one they want. Say so and default to reusing it: “Project <name> (<uid>) is already connected to this stack with contentTypeUid: <ct>. Reuse it, or create a second project against the same stack?” A second project on one stack is legal but rarely intended. It splits compositions across two projects
    Projects exist, none on this stackPresent the numbered list below and ask: reuse one (repointing it, see the warning at the end) or create new. Don’t assume new just because the stack differs
    No projects at allProceed to create, and say that’s what you’re doing
    GET fails (401/422)Stop. An auth failure is not evidence that no project exists. Resolve auth per step 10’s ladder, then re-check

    Present the list as a numbered choice: never ask for a project id

    You already fetched every project in the org, uid included. Do not ask the user to find or paste a project id. Show a numbered list and accept a number (or the name). You map the choice back to the uid yourself:

    Found 3 Studio projects in your organization:
    
      1. Visual Experience Showcase   ← already connected to this stack (compositions)
      2. Marketing Site               stack=blt9f3…  ct=compositions
      3. Docs Playground              stack=blt21c…  ct=doc_compositions
    
      4. Create a NEW project instead
    
    Reply with a number (1–4).

    Rules for the prompt: mark the one already connected to the target stack, always offer ”create new” as an explicit numbered option so reuse is never the only path, and show stack= / ct= so the choice is informed. Accept a name if they type one instead of a number. Only ask for a raw uid if the GET itself failed and they’re supplying it from the UI.

    If the user picks an existing project: what actually happens next

    Reusing changes the rest of this skill. It does not simply skip to the end.

    StepOn reuse
    10: register the projectSkipped entirely. The project exists. Do not POST /v1/projects
    7: pick the compositions CT uidNo longer a free choice. Adopt the project’s existing contentTypeUid from the listing. Inventing a new uid here creates a second compositions CT that the project doesn’t point at. The canvas then loads nothing while the CT sits there looking correct
    8 / 9: create + verify the CTVerify first (step 9’s GET /v3/content_types/<uid>). If it exists with the right field shape, skip the create. If it’s missing or mis-shaped, fix it against the adopted uid
    3 to 6: stack, Live Preview, environment, tokensStill run, still reuse-or-create. An existing project does not imply Live Preview is enabled or that a preview token exists. Those are stack-level and are the most common thing missing on a project someone created in the UI
    11 to 12: publish + record metadataUnchanged

    Then hand the chosen uid + contentTypeUid to configure-studio, which reads back Environment / Language / Canvas URL rather than assuming a UI-created project has them set. Record the decision so a later agent doesn’t re-ask.

    Don’t repoint an existing project at a different stack casually. If the user picks a project whose connectedStackApiKey is some other stack, say so and confirm before changing it. Every composition already authored there resolves against the old stack’s content.

  3. Ensure the stack exists (CMA). Either reuse via GET /v3/stacks?organization_uid=<org>&query={"name":"…"} or create via POST /v3/stacks (body: { stack: { name, description, master_locale: "en-us" } }, plus header organization_uid). Capture api_key.

  4. Create the environment (CMA) if it doesn’t exist: POST /v3/environments with { environment: { name: "preview", urls: [{ locale: "en-us", url: "<canvas-app-url>" }] } }. The environment URL must match where the host app renders (the canvas-app or visitor app). Save environment.uid. Studio’s /v1/projects configuration takes the uid, not the name.

  5. Enable Live Preview on the stack (CMA, critical, easy to miss). Read-modify-write: siblings like visual_builder must survive the write.

    GET  /v3/stacks/settings     (headers: api_key, $CS_AUTH)
    POST /v3/stacks/settings     (headers: api_key, $CS_AUTH)
    { "stack_settings": { ...existing,
        "live_preview": { ...existing.live_preview,
          "enabled": true, "default-env": "<environment uid>", "default-url": "<canvas-app-url>" } } }

    Not PUT /v3/stacks. It answers 200 Stack updated successfully and silently discards the payload: wrong endpoint, wrong envelope, enable not enabled. Live Preview stays off, silently. Saving Visual Experience in the Contentstack UI issues this same POST /v3/stacks/settings, measured at 201.

    enabled: true alone is rejected 422 error_code 150, asking for a default URL and environment. That is why step 4 comes first: its uid goes in default-env.

    A 200 is not proof. Re-read and assert live_preview.enabled. Without it the canvas iframe hangs on “Loading composition…” because the canvas-app’s calls to rest-preview.contentstack.* fail CORS preflight.

  6. Create the delivery token, then the preview token (CMA). Two calls. scripts/provision.ts is the reference implementation. ?create_with_preview_token=true returns token.preview_token on the same call, measured at 201 with a 26-char cs… value. Query param only: in the body it is accepted and the token is silently absent. Either route works; assert the token is non-empty, because an empty one 401s every Preview call silently. Use the dedicated endpoint below:

    POST /v3/stacks/delivery_tokens
    { "token": { "name": "...", "scope": [
        { "module": "environment", "environments": ["<env-name>"], "acl": { "read": true } },
        { "module": "branch",      "branches":     ["main"],       "acl": { "read": true } }
    ] } }

    Then create the preview token from its own endpoint. Live Preview must already be enabled on the stack, or this call fails:

    POST /v3/stacks/delivery_tokens/<uid>/preview_token

    It returns token.preview_token (a cs… string). Capture both. If it comes back empty, stop rather than continue: an empty preview token 401s every Preview API call and the canvas fails with “Component Loading Error / API error (0)”.

  7. Pick the compositions CT uid. Convention: <project>_compositions (lowercase, underscored). Studio resolves the compositions CT by this uid.

  8. Create the compositions CT: two calls, not one. CMA validates reference targets at creation time, so linked_sections (a self-reference) must be added on a follow-up PUT. In the initial POST it returns error_code 115: linked_sections.reference_to: content type does not exist. The CT can’t reference itself before it exists.

    POST /v3/content_types    (body = every field in the table below EXCEPT linked_sections)
    PUT  /v3/content_types/<uid>    (body = every field including linked_sections)

    Every field is required by the editor or runtime. Do not drop, rename, or change cardinality.

Compositions CT: required fields

uiddata_typemandatorymultipleRequired byPurpose
titletextyesnoCMS UXAuthor-visible title. Unique recommended.
urltextnonoRuntimeComposition URL pattern (e.g. /products/{{entry.url}}). Empty for freeform.
composable_uidtextyesnoRuntime + editorContract key: runtime queries compositions by composable_uid, NOT entry uid. Must be unique.
ui_previewfilenonoEditorThumbnail shown in Studio’s composition list.
connected_content_typetextnonoEditor + runtimeThe page CT uid this composition is connected to (Connected Template). Empty for Freeform.
uitext (multiline)nonoEditor + runtimezlib:<base64>-encoded composition node tree. Written by Studio canvas. Written by author-composition-via-api for headless authoring.
data_sourcestext (multiline)nonoRuntimeJSON object holding resolvedReferences etc. for reference resolution at render.
static_valuegroupnonoEditorStatic-value sub-bindings. See § static_value sub-schema below. Required as-is for the picker to enumerate static-value types.
schema_versiontextnonoEditorComposition schema version stamp written by Studio, a semver-shaped string like "1.0.0" (also seen: "", "1", "2").
place_composition_astextnonoEditor"page" for top-level template compositions (both Connected and Freeform). "section" for section compositions. "template" is legal but not what Studio writes for the top-level entry.
linked_schemasgroup, multiple=truenoyesEditorPer-section template-binding override. One entry per content type the section accepts. Must be group with multiple: true. The editor expects an array. Sub-fields on the group: content_type_uid (text, required, always populated) plus selected_field (text, optional, populated only when the section restricts to a specific field via selectedField) and display_name (text, optional). Runtime stored shape: [{ "content_type_uid": "blog_post", "_metadata": { "uid": "<system-generated>" } }, …]. If this is a single group (multiple=false) or a flat text field, editor crashes during section-context build.
url_metadatagroupnonoEditorurl_source (text), url_queries (text). Drives the URL pattern editor. Both sub-fields must exist. If the group (or either sub-field) is missing, the CMA silently drops url_metadata on every composition write: 201 returned, field reads back absent, and every template composition falls back to legacy_url semantics with no error anywhere. See author-composition-via-api § url + url_metadata, a matched pair.
linked_sectionsreference, multiple=true, reference_to: noyesEditorSelf-reference to other composition entries that are templates this composition links. Must be a reference field. The editor’s CDA call uses ?include[]=linked_sections, which only works on reference fields. If modeled as text/group, the editor’s Template Did Not Load error appears.

static_value sub-schema

Add a group field named static_value (multiple=false) with 13 sub-groups (each multiple=true), one per scalar binding type the picker offers. Each sub-group has a uniform { key: text, value: text } shape, EXCEPT boolean (value: boolean) and choice (value: text but multiple: true: exposed choice props write an array like ["center"]. A single-value choice.value rejects the write with “should be a single value instead of multiple”):

static_value (group)
├─ text        (group[]) { key: text, value: text (multiline) }
├─ html_rte    (group[]) { key: text, value: text (multiline) }
├─ array       (group[]) { key: text, value: text (multiline) }
├─ object      (group[]) { key: text, value: text (multiline) }
├─ number      (group[]) { key: text, value: text (multiline) }
├─ href        (group[]) { key: text, value: text (multiline) }
├─ textarea    (group[]) { key: text, value: text (multiline) }
├─ any         (group[]) { key: text, value: text (multiline) }
├─ json_rte    (group[]) { key: text, value: text (multiline) }
├─ datestring  (group[]) { key: text, value: text (multiline) }
├─ boolean     (group[]) { key: text, value: boolean }
├─ imageurl    (group[]) { key: text, value: text (multiline) }
└─ choice      (group[]) { key: text, value: text (multiline, **multiple: true**) }

The picker’s “Static Value” branch enumerates these sub-groups by uid. Missing sub-groups produce missing options in the picker.

  1. Verify the CT exists. GET /v3/content_types/<uid> and confirm the field list matches the table above. Catch shape errors early. Fixing a wrong cardinality after entries exist requires deleting and re-creating the CT.

  2. Register the Studio project (Studio API). Two calls.

Auth for /v1/projects: try OAuth FIRST. A session authtoken is the last resort. Work down this ladder and stop at the first that succeeds:

  1. Authorization: Bearer <oauth-access-token>, from install-contentstack-mcp‘s --auth, or csdx --oauth. Runtime-confirmed on prod NA: GET /v1/projects with this token plus an organization_uid header returns 200 and the project list. Also reported working on a non-prod data center after COMS-1710. Always attempt this before asking for anything else. Pair it with organization_uid, not a stack api_key, because projects are org-scoped.
  2. Create the project in the Studio UI, no token at all, and it onboards the stack in the same action.
  3. Session authtoken, only after OAuth is observed to fail with one of: authtoken: <oauth> returns 401 error_code 105 “authtoken is not valid”. Authorization: Bearer <oauth> returns 422 error_code 21 “Stack not found”. (A management token as authtoken: returns 401 “not logged in”, never the right credential here.)

A session authtoken is a full user-session credential: every org, every stack, every permission that user holds, valid until the session ends. Pasting it leaves copies in shell history, logs and process lists. Don’t request one speculatively: attempt OAuth, read the response, and only then escalate. If you do use one, scope it to this single call and discard it. Don’t write it to .env.

Sending the OAuth token in the wrong header is itself a 401 (error_code 105): OAuth goes in Authorization: Bearer, authtoken: is for session tokens only. Capability is still rolling out per environment, so branch on the response rather than assuming. See install-contentstack-mcp § What it does, and does NOT do.

POST  https://<studio-api-host>/v1/projects     (headers: organization_uid, $CS_AUTH — OAuth Bearer resolves this on prod)
{
  "name": "<Project Name>",
  "description": "...",
  "connectedStackApiKey": "<stack api_key from step 3>",
  "contentTypeUid":       "<compositions CT uid from step 7>",
  "canvasUrl":            "/canvas"   // route on the canvas-app that mounts <StudioCanvas/>
}

Then configure it (this is where environment + locale + Freeform attach):

PUT https://<studio-api-host>/v1/projects/<projectUid>
{
  "name": "<Project Name>",
  "canvasUrl": "/canvas",
  "connectedStackApiKey": "<api_key>",
  "contentTypeUid":       "<compositions CT uid>",
  "settings": {
    "configuration": { "environment": "<env UID from step 4>", "locale": "en-us" },
    "isFreeformEnabled": true
  }
}

The environment value MUST be the env uid (looked up via GET /v3/environments), not the name. The Studio API regional hosts mirror the CMA: eu-composable-studio-api.contentstack.com, azure-na-composable-studio-api.contentstack.com, azure-eu-composable-studio-api.contentstack.com.

  1. Publish at least one composition entry to the chosen environment before testing rendering. Unpublished compositions return 404 from the CDA, presenting as a blank canvas or “Composition Not Found.” See author-composition-via-api § Publish for the publish call.

  2. Record the project metadata (CT uid, environment, delivery token, preview token, Studio project uid, branch) in the project’s .env.example so install-studio can wire the host app consistently.

Acceptance

  • Live Preview enabled: GET /v3/stacks/settings returns stack_settings.live_preview.enabled === true.
  • Environment exists. Delivery token + preview token captured.
  • GET /v1/projects lists the project with connectedStackApiKey === <stack api_key> and the right contentTypeUid.
  • GET /v3/content_types/<uid> returns all 13 fields from the table with exact data_type, mandatory, multiple.
  • linked_schemas is group with multiple: true. (Single-group is the most common provisioning bug, re-check.)
  • linked_sections is reference with multiple: true and reference_to: [<this CT uid>].
  • composable_uid is mandatory + unique.
  • static_value group contains all 13 sub-groups with correct shape.
  • One composition entry exists and is published. CDA GET /v3/content_types/<uid>/entries?query={"composable_uid":"<your-uid>"}&environment=<env> returns it.
  • Preview token is set on the environment. ?live_preview=<hash> flows through sdk.fetchCompositionData.

Common Pitfalls

PitfallWhy it bitesFix
Naming a group sub-field uidThe CMA rejects the whole PUT with 422 schema.N.schema.0.uid: "has a restricted value 'uid', which is not allowed". Easy to hit on linked_schemas, where the natural guess for the content-type key is uidThe sub-field is content_type_uid, never uid. See the linked_schemas row above. uid is reserved by the CMA inside groups
linked_schemas modelled as group with multiple: falseEditor expects an array. Section-context build crashes on .map of a single objectRe-create CT with multiple: true. Or PATCH the CT field if no entries exist yet
linked_sections modelled as text/group instead of referenceEditor’s ?include[]=linked_sections returns nothing. Template Did Not Load on canvas openMust be reference field with reference_to: [<same CT uid>]. See troubleshoot-canvas Template-Did-Not-Load row
composable_uid left non-uniqueRuntime lookup picks an arbitrary entry. One composition renders for many URLsMark mandatory + unique at CT creation time
Forgetting static_value sub-groupsPicker’s Static Value root shows fewer (or zero) type optionsAdd the 13 sub-groups exactly as schema’d above
Composition entry not publishedCDA returns 404. Canvas blankPublish to the environment after entry create (P5)
Preview token missing / set to literal "undefined"Canvas iframe gets HTTP 401 with no CORS headers, “Component Loading Error”See troubleshoot-canvas preview-token row
JSON-RTE static values fail to embed correctlyJSON-RTE embeds carry attrs.locale, missing in API-authored payloadsSet attrs.locale on every embedded entry/asset in static_value.json_rte[*].value (P6)
Wrong region hostCT creation 404sUse region-specific CMA host. See reference_studio_provisioning_api
Live Preview not enabled on stackCanvas iframe hangs on “Loading composition…”. Rest-preview CORS preflight failsPOST /v3/stacks/settings with live_preview.enabled, default-env and default-url (step 5). PUT /v3/stacks answers 200 and changes nothing
Studio project configured with environment name instead of uidStudio API accepts the PUT but environment binding silently misses. Compositions never resolveLook up env uid via GET /v3/environments, pass settings.configuration.environment = <uid>
Re-using a Studio project across stacks (name match, stale connectedStackApiKey)Project surfaces last stack’s compositions. Current stack invisible in canvasOn every provision, PUT the project with the current connectedStackApiKey to re-bind
Mixed CMA + Studio API hosts (e.g. NA Studio API with EU CMA)401/404s with no clear errorPick a region and use BOTH region-matched hosts
Including linked_sections in the initial POST /v3/content_typeserror_code 115: linked_sections.reference_to: content type does not exist. CMA validates reference targets at create time. A CT can’t self-reference before it existsTwo-step: POST without linked_sections, then PUT with the full schema (step 8)
Delivery-token scope trimmed to environment-only (branch scope dropped)"Delivery Token creation failed. Please try again." (no HTTP error code, no field diagnostic) a natural “simplification” of the step 6 body silently failsBoth scope entries are REQUIRED: { module: "environment", ... } AND { module: "branch", branches: ["main"], acl: { read: true } }. Keep the two-entry body from step 6 verbatim
PUT /v3/environments/<uid> to update an environment (using the UID in the path)error_code 248: could not find environment. This CMA endpoint keys on env name in the path, not UIDUse the env name: PUT /v3/environments/preview. Studio /v1/projects config (step 10) instead takes the env UID from step 4
Creating the stack (POST /v3/stacks) in an org where the user lacks stack-create rightsHTTP 403, error_code 316, "You don't have the permission to do this operation." (verified), a bare 403, no field diagnosticDon’t crash. Tell the user: get stack-create rights in the org, OR switch to an org where they can (re-auth if using the MCP: there’s no org-picker, switch the active org first), OR reuse an existing stack. --auth captures the active org, so switching active org before login targets the right one

See Also