Projects: the five project routes

View as Markdown
Last updated September 8, 2026

A Studio project is the record that binds a stack, a composition content type, an environment, and a locale together. Every composition route resolves its target through a project, which is why these routes come first. Same envelope as the rest of the chapter: /v1 prefix, organization_uid header, the { error_message, error_code, errors } envelope. See Endpoint reference for the shared conventions.

Preview: not yet GA. Paths and response shapes may still change, and the service may not be enabled on your region or tenant yet.

Why projects matter to every other route

Composition routes take a projectUid and nothing else about your stack. That works because the project record carries the rest:

The project holdsWhat reads it
connectedStackApiKeyEvery access check, and the CMA calls that compositions make
contentTypeUidWhere compositions are stored as entries
settings.configuration.localeThe default locale for composition reads
settings.configuration.environmentThe environment for published reads
canvasUrlWhich app route Studio loads to preview sections

So a composition call failing with an access error is often a project problem, not a composition problem: check the project's connected stack first.

The project record

FieldTypeNotes
uidstringAssigned by the service.
namestringRequired on create. Max 256 characters.
descriptionstringOptional. Max 1024 characters. Defaults to "".
canvasUrlstringOptional. Max 100 characters, shorter than it looks, and the most common validation surprise here. Defaults to "". Empty means the project uses Playground Canvas.
connectedStackApiKeystringRequired on create. The stack this project authors against.
contentTypeUidstringRequired on create. The content type compositions are stored in.
settingsobject{ configuration: { environment, locale } }. Defaults to both empty strings.
organizationUidstringSet from the organization_uid header, not from the body.
createdAt / updatedAtISO dateMaintained by the service.
createdBy / updatedBystringUser UID, taken from the caller's identity.
deletedAtfalse | ISO datefalse while live. Deletion sets a timestamp, see section 5.
deletedBystringSet on delete.

A note on the published schema. The service's Swagger output describes the string length limits with maximum rather than maxLength. That's an annotation slip in the source, not a different rule: the constraints enforced at runtime are the character counts in the table above.

Only six fields are writable: name, description, canvasUrl, connectedStackApiKey, contentTypeUid, settings. Everything else is service-maintained, and sending it is silently ignored.

1. List projects

GET /v1/projects

Returns the projects in the organization that you can see. The scope is not simply "all projects in the org". It is the union of two sets:

  • projects whose connectedStackApiKey is a stack you have access to, and
  • projects you created, whether or not you still have access to their stack.

The second half is what stops a project from vanishing from its owner's list after a stack permission change.

Response: 200 OK

{
  "projects": [
    {
      "uid": "blt9f2c…",
      "name": "Marketing site",
      "description": "",
      "canvasUrl": "/studio",
      "connectedStackApiKey": "blt1234567890",
      "contentTypeUid": "compositions",
      "settings": { "configuration": { "environment": "production", "locale": "en-us" } },
      "createdAt": "2026-01-01T00:00:00.000Z",
      "deletedAt": false
    }
  ]
}

An empty organization returns { "projects": [] }, not a 404.

curl 'https://<host>/v1/projects' \
  -H "$CS_AUTH" -H 'organization_uid: <org-uid>'

2. Fetch a project

GET /v1/projects/{uid}

ParamInRequiredNotes
uidpathyesUID of the project.

Scoped to your organization and to live projects only (deletedAt: false). Access is re-checked after the lookup: if you can neither reach the project's stack nor own the project, the response is 404 project_not_found (1) rather than a 403: the service doesn't confirm that a project you can't see exists.

Response: 200 OK

{ "project": { "uid": "blt9f2c…", "name": "Marketing site", … } }

The missing-deletedAt trap. The lookup filters on deletedAt: false, and that does not match a document where the field is absent: an older record written before the field existed returns 404 here while still appearing in the list route. A project that lists but won't fetch has this shape.

curl 'https://<host>/v1/projects/<uid>' \
  -H "$CS_AUTH" -H 'organization_uid: <org-uid>'

3. Create a project

POST /v1/projects

Request body

FieldTypeRequiredNotes
namestringYesMax 256 characters.
connectedStackApiKeystringYesYou need write access to this stack, checked before anything is written.
contentTypeUidstringYesThe content type compositions are stored in.
descriptionstring-Max 1024 characters.
canvasUrlstring-Max 100 characters. Omit to use Playground Canvas.
settingsobject-{ configuration: { environment, locale } }.

organizationUid, createdBy, and updatedBy are set from your identity and the organization_uid header, sending them has no effect.

Response: 201 Created

{
  "notice": "Project created successfully",
  "project": { "uid": "blt9f2c…", "name": "Marketing site", "deletedAt": false, … }
}

Notable errors

422 stack_not_found (21) when you lack write access to connectedStackApiKey: this is the access failure, not a claim that the stack doesn't exist. 400 for DTO-shape problems, in the framework's { statusCode, message, error } shape. 422 project_create_failed (2) if the authorization SDK fails during the call.

curl -X POST 'https://<host>/v1/projects' \
  -H "$CS_AUTH" -H 'organization_uid: <org-uid>' \
  -H 'Content-Type: application/json' \
  -d '{ "name": "Marketing site", "connectedStackApiKey": "blt1234567890",
        "contentTypeUid": "compositions",
        "settings": { "configuration": { "environment": "production", "locale": "en-us" } } }'

4. Update a project

PUT /v1/projects/{uid}

A partial update: every writable field is optional, and omitted fields keep their stored values.

Write access is checked against the project's stored connectedStackApiKey before the update runs. Repointing a project at a different stack therefore checks your access to the current stack, not the new one.

Response: 200 OK

{ "notice": "Project updated successfully", "project": { /* the updated record */ } }

404 project_not_found (1) if the uid isn't a live project in your organization. 422 stack_not_found (21) on access denial. 422 project_update_failed (3) on an authorization-SDK failure.

curl -X PUT 'https://<host>/v1/projects/<uid>' \
  -H "$CS_AUTH" -H 'organization_uid: <org-uid>' \
  -H 'Content-Type: application/json' \
  -d '{ "canvasUrl": "/studio" }'

5. Delete a project

DELETE /v1/projects/{uid}

A soft delete: the record stays and deletedAt becomes a timestamp, which removes it from every lookup in this chapter. There is no undelete route, and no force parameter.

Unlike deleting a composition, this is not reference-guarded: the project's compositions are not checked, counted, or removed. They remain as entries in the connected stack, orphaned from Studio's point of view.

Response: 200 OK

{ "notice": "Project deleted successfully" }

404 project_not_found (1) if it isn't a live project in your organization. 422 stack_not_found (21) on access denial. 422 project_delete_failed (4) on an authorization-SDK failure.

Send the DELETE without a Content-Type header. A body-less DELETE carrying Content-Type: application/json is rejected with a 400 before the handler runs.

curl -X DELETE 'https://<host>/v1/projects/<uid>' \
  -H "$CS_AUTH" -H 'organization_uid: <org-uid>'

See also