> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sapt.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# CMS and asset APIs

> Create, update, and delete content records, and manage the library files they reference.

CMS content records and library files are separate resources. Deleting a record
preserves its files; deleting a library asset removes the stored file and asset
record. Use both operations when your pipeline owns both resources and no other
consumer needs the file.

All paths below are relative to `https://api.sapt.ai` and accept
`Authorization: ApiKey <key>` or an OAuth bearer token.

## Content endpoints

| Method | Path | Operation |
| - | - | - |
| `GET` | `/projects/{projectId}/cms/content/{contentTypeSlug}` | List content, with `limit` and `offset` |
| `GET` | `/projects/{projectId}/cms/content/{contentTypeSlug}/{slug}` | Read content by slug |
| `POST` | `/projects/{projectId}/cms/items` | Create content |
| `GET` | `/projects/{projectId}/cms/items/{contentItemId}` | Read content by stable ID |
| `PATCH` | `/projects/{projectId}/cms/items/{contentItemId}` | Update supplied fields |
| `DELETE` | `/projects/{projectId}/cms/items/{contentItemId}` | Delete the CMS record |

The content type must already exist. `content` must match that type's schema.
Create and update use the same validation and permissions as the dashboard:
ordinary content requires `cms_items:write`, and reads require `cms_items:read`.
Create/update also read the type definition using `cms_types:read`. Meta planner
content uses its own `meta_planner` permissions.

### Create content

This example assumes an existing `clip` content type with a `title` field. Replace
the type slug and content fields with your project's schema.

```bash theme={null}
curl --fail-with-body -X POST \
  "https://api.sapt.ai/projects/$PROJECT_ID/cms/items" \
  -H "Authorization: ApiKey $SAPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contentTypeSlug":"clip","slug":"clip-001","name":"Clip 001","content":{"title":"First clip"},"status":"draft"}'
```

Returns HTTP `201` with `{ "item": { ... } }`. Store `item.id` for future mutations.
Creating content does not schedule a social-platform post. Use the
[social publishing API](/docs/mintlify/social/scheduling) for that.

### Update content

```bash theme={null}
curl --fail-with-body -X PATCH \
  "https://api.sapt.ai/projects/$PROJECT_ID/cms/items/$CONTENT_ITEM_ID" \
  -H "Authorization: ApiKey $SAPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Updated clip","content":{"title":"New title"}}'
```

Returns HTTP `200` with `{ "item": { ... } }`. Omitted fields stay unchanged.
Nested content objects merge according to the content schema; arrays and scalar
values replace. The content type cannot be changed through this endpoint.

`scheduledAt` and `publishedAt` accept ISO 8601 timestamps or `null`. Clearing
`scheduledAt` returns a scheduled CMS item to draft. These are CMS publishing
dates, separate from a social post's schedule. Responses serialize dates as ISO
strings. Invalid content returns `400` with field errors in `error.details.errors`;
slug conflicts and concurrent edits return `409`.

### Delete content

```bash theme={null}
curl --fail-with-body -X DELETE \
  "https://api.sapt.ai/projects/$PROJECT_ID/cms/items/$CONTENT_ITEM_ID" \
  -H "Authorization: ApiKey $SAPT_API_KEY"
```

Returns HTTP `200` with `{ "success": true }`. A missing item returns `404`.
This removes only the CMS record. Library assets and scheduled-post records
remain; delete them through their own endpoints when appropriate.

## Find library assets

```bash theme={null}
curl --fail-with-body \
  "https://api.sapt.ai/projects/$PROJECT_ID/assets?feature=cms&assetType=video&limit=100&offset=0" \
  -H "Authorization: ApiKey $SAPT_API_KEY"
```

Returns `{ "assets": [...], "total": 90 }`. Each asset contains `id`, `url`,
`filename`, `mimeType`, `sizeBytes`, `feature`, `assetType`, `tags`, and `createdAt`.
Use `id` for updates and deletion. `search` matches filenames and tags;
`untagged=true` selects files with no tags. `limit` is 1–100, default 50.

Collect the complete set of IDs before deleting a paginated batch: deleting while
advancing `offset` can skip records as the remaining pages shift. Read one asset
with `GET /projects/{projectId}/assets/{assetId}`; it returns `{ "asset": { ... } }`
or `404`. Both reads require `assets:read`.

## Update library assets

`PATCH /projects/{projectId}/assets/{assetId}` updates `feature` or `tags` and
returns `{ "asset": { ... } }`. It does not replace the file bytes.

```json theme={null}
{ "tags": ["cleanup-batch-001"], "tagOperation": "add" }
```

`tagOperation` supports `replace` (default), `add`, and `remove`. Updates require
`assets:write`; changing another uploader's asset also requires `assets:manage`.

## Delete library assets

```bash theme={null}
curl --fail-with-body -X DELETE \
  "https://api.sapt.ai/projects/$PROJECT_ID/assets/$ASSET_ID" \
  -H "Authorization: ApiKey $SAPT_API_KEY"
```

Requires `assets:manage`. Returns HTTP `200` with `{ "deletedCount": 1 }` after
deleting the origin file and asset record, or `{ "deletedCount": 0 }` if the ID
is already absent from this project. A failed storage deletion leaves the record
available for retry. CDN or browser caches may continue serving previously cached
bytes after origin deletion.

<Warning>
  Asset deletion does not check shared references. Confirm that other content, pending posts, and
  retries no longer need the file. Existing content and post records are preserved, so their
  references can become unavailable. Published email delivery images are protected and return `400`
  if deletion is attempted.
</Warning>

For the full clip lifecycle, including a cleanup job 24 hours after publication,
see [Media retention and cleanup](/docs/mintlify/social/cleanup).
