# Content API clients

> The content API clients in @uniformdev/canvas let you read Uniform content for frontend apps such as Next.js, and write content from automations, CLIs, and backend services.

Source: https://docs.uniform.app/docs/sdk/content-api-clients

The Content API clients in `@uniformdev/canvas` let you read and write Uniform content (compositions, entries, component definitions, content types, and routes). The most common use case is reading content for frontend applications such as Next.js apps. You can also call them from any server-side context: an [automation](https://docs.uniform.app/docs/guides/automations) handler, a CLI, a script, or a backend service.

Pick a delivery or a management client based on what you need to do:

- **Delivery** clients fetch content from Uniform's [Edge Delivery APIs](https://docs.uniform.app/docs/api). Use them to render or serve compositions, entries, and routes. They are read-only.
- **Management** clients create, update, publish, and delete content through Uniform's [Management APIs](https://docs.uniform.app/docs/api). Use them from automations, CLIs, and backend services.

> **Minimum SDK version:**
>
> The clients on this page require `@uniformdev/canvas` **version 20.74.7 or later**. If you pass them to helpers from other Uniform packages (for example `@uniformdev/tms-sdk`), upgrade those packages to the same version.

## The clients

| Client | Mode | Methods | Host / shape |
| --- | --- | --- | --- |
| `CompositionDeliveryClient` | delivery | `get` `list` | edge, resolved |
| `CompositionManagementClient` | management | `get` `list` `save` `saveAndPublish` `unpublish` `remove` `history` | origin, canonical |
| `EntryDeliveryClient` | delivery | `get` `list` | edge, resolved |
| `EntryManagementClient` | management | `get` `list` `save` `saveAndPublish` `unpublish` `remove` `history` | origin, canonical |
| `ComponentDefinitionClient` | management | `get` `list` `save` `remove` | origin |
| `ContentTypeClient` | management | `get` `list` `save` `remove` | origin |
| `RouteClient` | delivery | `get` | edge, resolved |

## Constructing a client

Supply `projectId` and either `apiKey` or `bearerToken`:

```ts
import { CompositionManagementClient, CompositionDeliveryClient } from '@uniformdev/canvas';

const management = new CompositionManagementClient({ apiKey, projectId });
const delivery = new CompositionDeliveryClient({ apiKey, projectId });
```

Delivery clients additionally accept `edgeApiHost` (defaults to `https://uniform.global`) and `disableSWR`, which sends `x-disable-swr` to skip stale-while-revalidate on data-resource caches. Management clients accept `apiHost` (defaults to `https://uniform.app`). For EU projects, see [Configure SDKs for EU](https://docs.uniform.app/docs/guides/regions#configure-sdks-for-eu).

## Reading

Select what to read with a **selector**, then layer read options on top.

```ts
// compositions: by id (+ optional editionId / versionId), slug, or project map node path
await compositions.get({ compositionId, editionId });
await compositions.get({ slug: '/home' });

// entries: by id (+ optional editionId / versionId) or slug
await entries.get({ entryId });

// list returns a page of results, and takes filtering and paging options
// alongside the same read options as get
await entries.list({ limit: 10 });
```

- **`state` is optional and has a per-mode default.** Delivery defaults to published, management defaults to draft.
- **Editions are derived, not asked for.** An [edition](https://docs.uniform.app/docs/guides/composition/editions) is a locale-targeted variant of a composition or entry. On a management `get`, a bare id returns that composition or entry (`raw`). Passing a `locale` resolves the locale-active edition (`auto`). Passing an `editionId` fetches that edition. That way a later save targets the same entity you just read. You can force `editions: 'raw' | 'auto'` if you need to. On `list`, pass the full `editions: 'auto' | 'all' | 'raw'` enum.

## Filtering

`list` on the entry and composition clients supports structured filtering. Filters are passed as key-value pairs where the key includes the field path and operator.

### Filter syntax

```
filters.<field>[<operator>]=<value>
```

- **`field`**: The field to filter by. Use `type` for content type, `fields.<fieldName>` for custom fields on entries, `parameters.<parameterName>` for composition parameters, or system properties like `name`, `slug`, `created`, `modified`.
- **`operator`**: The comparison operator.
- **`value`**: The literal value to compare against.

### Example: filter by content type and field value

```ts
const response = await entries.list({
  filters: {
    "type[eq]": "brand",
    "fields.brandName[match]": "adidas",
  },
});
```

The SDK accepts each filter in two equivalent forms: a flat key that includes the operator, or a nested object keyed by operator. Use whichever reads better; the examples on this page use the flat form.

```ts
filters: { "type[eq]": "brand" }   // flat key
filters: { type: { eq: "brand" } } // nested object
```

### Available operators

| Operator | Description |
| --- | --- |
| `eq` | Equals |
| `neq` | Not equal |
| `match` | Contains (text search) match |
| `starts` | Starts with. Value limited to letters, numbers, `_`, `.`, `-`, and spaces |
| `lt` / `lte` | Less than / less than or equal to |
| `gt` / `gte` | Greater than / greater than or equal to |
| `in` | Matches any value in a comma-separated list (OR) |
| `nin` | Does not match any value in a list |
| `all` | List-valued fields (multi-select fields, `labels`, `labelGroups`) must contain every value in a comma-separated list (AND) |
| `def` | `true` or `false`; whether the field has a value at all |

### Supported operators by field

Not every operator is valid for every field; the allowed set depends on the field's type. An unsupported combination returns a 400 error listing the supported operators.

#### Metadata fields (entries and compositions)

| Field | Supported operators |
| --- | --- |
| `entityId`, `type`, `uiStatus`, `locale`, `creatorSubject`, `authorSubject` | `eq`, `neq`, `in`, `nin` |
| `editionId`, `releaseId`, `patternId`, `workflowId`, `workflowStageId`, `categoryId` | `eq`, `neq`, `in`, `nin`, `def` |
| `created`, `modified` | `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`, `nin` |
| `name`, `slug` | `match`, `starts`, `eq`, `neq`, `in`, `nin`, `def` |
| `creator`, `author` | `match`, `starts`, `eq`, `neq`, `in`, `nin` |
| `projectMapId`, `projectMapNodeId` (compositions) | `eq`, `neq`, `in`, `nin`, `def` |
| `labels`, `labelGroups` | `eq`, `neq`, `in`, `nin`, `all`, `def` |

#### Content fields (`fields.*`)

| Field type | Supported operators |
| --- | --- |
| Text, select | `match`, `starts`, `eq`, `neq`, `in`, `nin`, `def` |
| Number, date, datetime | `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`, `nin`, `def` |
| Checkbox | `eq`, `neq`, `def` |
| Multi-select | `eq`, `neq`, `in`, `nin`, `all`, `def` |
| Rich text | `match`, `starts`, `def` |
| Reference, asset (by ID) | `eq`, `neq`, `in`, `nin`, `def` |

#### Sub-properties of reference, asset, and link fields

Reference, asset, and link fields can also be filtered by sub-properties of the item they point to (for example `fields.speaker.slug`). Text-valued sub-properties take the text operators; ID-valued sub-properties take the same operators as reference fields:

| Sub-property | Supported operators |
| --- | --- |
| Reference `.name`, `.slug`; asset `.url`, `.title`, `.description`; link path | `match`, `starts`, `eq`, `neq`, `in`, `nin`, `def` |
| Reference `.type`, `.uiStatus`; link `.type`, `.projectMapNodeId`; asset `.mediaType` | `eq`, `neq`, `in`, `nin`, `def` |

### Filtering reference fields

For reference fields, you can filter by the referenced entry's properties:

```ts
const response = await entries.list({
  filters: {
    "type[eq]": "eventSession",
    "fields.speaker.slug[eq]": "jane-doe",
  },
});
```

### Combining multiple filters

Combine multiple filters to narrow results:

```ts
const response = await entries.list({
  filters: {
    "type[eq]": "product",
    "fields.category[eq]": "electronics",
    "fields.price[gte]": 50,
    "fields.price[lte]": 500,
  },
});
```

### Filtering compositions

Composition `list` uses the same operators, with `parameters` in place of `fields`:

```ts
const response = await compositions.list({
  filters: {
    "type[eq]": "landingPage",
    "parameters.heroTitle[match]": "summer",
  },
});
```

## Selecting fields with data projection

The `select` option fetches a subset of each entry or composition instead of the whole thing. The API prunes fields, field types, and slots before values are resolved, so pruned content skips asset resolution, rich text reference expansion, and data resource fetches — reducing both payload size and response time.

`select` accepts a `ProjectionSpec` object, which the client serializes into `select.*` query parameters (mirroring the `filters.*` syntax). The full wire-level projection syntax is documented in the _For the US region:_

_For the EU region:_

.

```ts
const response = await entries.list({
  filters: { "type[eq]": "article" },
  select: {
    fields: { only: ["title", "coverImage"] },
  },
});
```

Every article in the response contains only its `title` and `coverImage` fields. All other fields (for example body, tags, and author references) are removed. System properties such as `_id`, `_name`, and `type` stay in the response.

### Projection buckets

A projection spec has three buckets:

| Bucket | Selects by | Example |
| --- | --- | --- |
| `fields` | Field name | `fields: { only: ["title", "slug"] }` |
| `fieldTypes` | Field type ID (e.g. `text`, `richText`, `asset`) | `fieldTypes: { except: ["richText"] }` |
| `slots` | Slot name (compositions only) | `slots: { only: ["hero"] }` |

#### `fields` operators

| Operator | Type | Description |
| --- | --- | --- |
| `only` | `string[]` | Keep only fields whose name matches one of these patterns |
| `except` | `string[]` | Drop fields whose name matches one of these patterns |
| `locales` | `string[]` | For matching fields that survive filtering, return the full per-locale value map instead of only the requested locale's value |
| `blockDepth` | `number` or `"preserveAll"` | Limit how many levels of block field children are kept. `0` removes all block fields; `"preserveAll"` prevents projection from trimming fields inside block children |

#### `fieldTypes` operators

| Operator | Type | Description |
| --- | --- | --- |
| `only` | `string[]` | Keep only fields of the named types |
| `except` | `string[]` | Drop fields of the named types |

#### `slots` operators

| Operator | Type | Description |
| --- | --- | --- |
| `only` | `string[]` | Keep only the named slots |
| `except` | `string[]` | Drop the named slots |
| `depth` | `number` | Limit how many slot levels are kept, counted from the root. `0` removes all slots on the root; `1` keeps the root's slots but not the slots of their children |
| `named` | `{ [slotName]: { depth } }` | Per-slot depth caps; override the container-wide `depth` for that slot |

### Projection behavior

- **Wildcards**: values accept a single `*` wildcard matching zero or more characters — `seo_*`, `*Title`, and `meta*Published` are all legal.
- **Recursive by default**: projection applies at every component and block in the returned tree, not just the root, and is forwarded into entries resolved through reference fields.
- **Exclusion wins**: when operators combine, all `only` sets are intersected first, then `except` sets are subtracted. If rules contradict, the exclusion applies.
- **Unknown names are silent no-ops**: asking for a field a node doesn't have produces an empty field bag, not an error. The tree shape is preserved; non-matching content is simply absent.
- **Empty `only` strips everything**: `fields: { only: [] }` removes every field; `slots: { only: [] }` flattens the component tree. `except: ["*"]` is equivalent.
- **Depth resets across references**: `depth` and `blockDepth` count nesting within a single fetched tree and reset inside referenced entries.

### Example: strip rich text everywhere

Drop rich text fields you can't render, without needing to know their names — including inside entries resolved through reference fields:

```ts
const response = await entries.list({
  filters: { "type[eq]": "article" },
  select: {
    fieldTypes: { except: ["richText"] },
  },
});
```

### Example: per-locale slugs for hreflang tags

Fetch a lean payload but keep every locale's value on the `slug` field:

```ts
const response = await entries.list({
  filters: { "type[eq]": "article" },
  select: {
    fields: { only: ["title", "slug"], locales: ["slug"] },
  },
});
```

The `select` option is available on `get` and `list` of the entry and composition clients (on compositions, the `slots` bucket controls the component tree), and on [`RouteClient.get`](https://docs.uniform.app/docs/sdk/route-client#fetching-a-subset-with-select).

## Writing

Writing is available on the management clients only.

```ts
const { modified } = await entries.save(entryBody); // create/update the draft
await entries.saveAndPublish(entryBody); // draft + publish in one call
await entries.unpublish({ entryId }); // drop the published state
await entries.remove({ entryId }); // delete the whole thing
await entries.remove({ entryId, editionId }); // delete just one edition (all its states)
```

> **Use an AI coding assistant:**
>
> The [`uniform-sdk`](https://github.com/uniformdev/agent-skills/tree/main/skills/uniform-sdk) agent skill teaches coding assistants such as Claude Code, Codex, Cursor, and Copilot how to authenticate with Uniform, configure the CLI, resolve routes, and read and write compositions, entries, and content types with the content API clients. To install it, see [Uniform agent skills](https://docs.uniform.app/docs/guides/ai/agent-skills).

## Migrating from the old clients

The first migration decision for every call site is **delivery or management?** If the code reads to render or serve, use delivery. If it reads in order to mutate and save, or writes at all, use management.

| Old | New |
| --- | --- |
| `CanvasClient` (compositions) | `CompositionDeliveryClient` + `CompositionManagementClient` |
| `CanvasClient` (component definitions) | `ComponentDefinitionClient` |
| `ContentClient` (entries) | `EntryDeliveryClient` + `EntryManagementClient` |
| `ContentClient` (content types) | `ContentTypeClient` |
| `Uncached*Client` | `bypassCache: true` |
| `RouteClient.getRoute` | `RouteClient.get` (same client, renamed method) |

### Delivery read

```ts
// before
const canvas = new CanvasClient({ apiKey, projectId });
const composition = await canvas.getCompositionBySlug({ slug: '/home', state: CANVAS_PUBLISHED_STATE });

// after: delivery client defaults state to published
const compositions = new CompositionDeliveryClient({ apiKey, projectId });
const composition = await compositions.get({ slug: '/home' });
```

### Read, modify, write

For example, an automation reacting to a draft event. The old approach needed six options to avoid losing data:

```ts
// before
const canvas = new UncachedCanvasClient({ bearerToken, projectId });
const composition = await canvas.getCompositionById({
  compositionId: event.editionId ?? event.compositionId,
  editions: 'raw',
  releaseId: event.releaseId,
  state: CANVAS_DRAFT_STATE,
  skipDataResolution: true,
  skipPatternResolution: true,
  skipOverridesResolution: true,
  withComponentIDs: true,
});
// ...mutate...
await canvas.updateComposition({ composition: mutated }); // save draft
await canvas.updateComposition({ composition: mutated, state: CANVAS_PUBLISHED_STATE }); // publish

// after: the management client is canonical, draft, and fresh by construction
const compositions = new CompositionManagementClient({ bearerToken, projectId });
const composition = await compositions.get({
  compositionId: event.compositionId,
  editionId: event.editionId,
  releaseId: event.releaseId,
});
// ...mutate...
await compositions.saveAndPublish({
  composition: composition.composition,
  editionId: event.editionId,
  releaseId: event.releaseId,
});
```

### Content types and component definitions

```ts
// before
const content = new ContentClient({ apiKey, projectId });
await content.upsertContentType({ contentType });

// after
const contentTypes = new ContentTypeClient({ apiKey, projectId });
await contentTypes.save({ contentType });
```

## Renamed methods on the other clients

The remaining clients keep their class but standardized their verbs on the same `list`, `save`, and `remove` vocabulary. Only the method names changed, and the old names remain as `@deprecated` aliases.

| Client | Deprecated → new |
| --- | --- |
| `CategoryClient` | `getCategories` → `list`, `upsertCategories` → `save`, `removeCategory` → `remove` |
| `LabelClient` | `getLabels` → `list`, `upsertLabel` → `save`, `removeLabel` → `remove` |
| `ProjectClient` | `getProjects` → `list`, `upsert` → `save`, `delete` → `remove` |
| `DataSourceClient` | `getList` → `list`, `upsert` → `save` |
| `DataTypeClient` | `get` → `list`, `upsert` → `save` |
| `LocaleClient` | `get` → `list`, `upsert` → `save` |
| `WorkflowClient` | `get` → `list`, `upsert` → `save` |
| `ReleaseClient` | `get` → `list`, `upsert` → `save` |
| `RelationshipClient` | `get` → `list` |
| `ReleaseContentsClient` | `get` → `list` |
| `EntityReleasesClient` | `get` → `list` |

Note the list-returning `get` → `list` renames. `get` now consistently means a single fetch.

## Using the clients from an automation

An [automation](https://docs.uniform.app/docs/guides/automations/code-automations) can call these clients as its own Uniform identity. Pass `context.uniformCredentials` to the client constructor. It carries the `projectId` and `bearerToken` the clients expect:

```ts
const compositions = new CompositionManagementClient(context.uniformCredentials);
```
