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 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. 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. 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.

ClientModeMethodsHost / shape
CompositionDeliveryClientdeliveryget listedge, resolved
CompositionManagementClientmanagementget list save saveAndPublish unpublish remove historyorigin, canonical
EntryDeliveryClientdeliveryget listedge, resolved
EntryManagementClientmanagementget list save saveAndPublish unpublish remove historyorigin, canonical
ComponentDefinitionClientmanagementget list save removeorigin
ContentTypeClientmanagementget list save removeorigin
RouteClientdeliverygetedge, resolved

Supply projectId and either apiKey or bearerToken:

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.

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

// 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 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.

list accepts filters, a full-text search, and an orderBy sort. Patterns are listed through the same clients with the pattern and patternType options.

await entries.list({ search: 'summer', filters: { type: { eq: 'article' }, 'fields.featured': { eq: true } }, orderBy: ['relevance_DESC'], }) await compositions.list({ pattern: true, patternType: 'component' })

For the filter syntax, operators, supported field types, and assets, see search and filter.

The select option on get and list fetches a subset of each entry or composition instead of the whole thing. The API prunes fields, field types, and slots before it resolves values. Pruned content skips asset resolution, rich text reference expansion, and data resource fetches. This reduces both payload size and response time.

select accepts a ProjectionSpec object, which the client serializes into select.* query parameters. The full wire-level projection syntax is documented in the entries endpoint OpenAPI specification.

await entries.list({ filters: { type: { eq: 'article' } }, select: { fields: { only: ['title', 'coverImage'] }, }, })

Every article in the response contains only its title and coverImage fields. System properties such as _id, _name, and type stay in the response.

A projection spec has three buckets:

BucketSelects byExample
fieldsField or parameter namefields: { only: ["title", "slug"] }
fieldTypesField type ID (for example text, richText, asset)fieldTypes: { except: ["richText"] }
slotsSlot name (compositions only)slots: { only: ["hero"] }

fields supports these operators:

OperatorTypeDescription
onlystring[]Keep only fields whose name matches one of these patterns
exceptstring[]Drop fields whose name matches one of these patterns
localesstring[]For matching fields that survive filtering, return the full per-locale value map instead of only the requested locale's value
blockDepthnumber | "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 supports only and except with field type IDs. slots supports:

OperatorTypeDescription
onlystring[]Keep only the named slots
exceptstring[]Drop the named slots
depthnumberLimit 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 that override the container-wide depth for that slot

Projection behaves as follows:

  • Wildcards: values accept a single * wildcard matching zero or more characters. seo_*, *Title, and meta*Published are all valid.
  • Recursive by default: projection applies at every component and block in the returned tree, and is forwarded into entries resolved through reference fields.
  • Exclusion wins: all only sets are intersected first, then except sets are subtracted.
  • Unknown names are ignored: a name that doesn't match a field on a node doesn't cause an error. If none of the names in only match, the node is returned without fields.
  • Empty only strips everything: fields: { only: [] } removes every field, and 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.

For example, to drop rich text fields everywhere, including inside referenced entries:

await entries.list({ filters: { type: { eq: 'article' } }, select: { fieldTypes: { except: ['richText'] } }, })

To return only title and slug, and keep every locale's value of slug for hreflang tags:

await entries.list({ filters: { type: { eq: 'article' } }, select: { fields: { only: ['title', 'slug'], locales: ['slug'], }, }, })

The same option is available on RouteClient.get.

Writing is available on the management clients only.

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 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.

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.

OldNew
CanvasClient (compositions)CompositionDeliveryClient + CompositionManagementClient
CanvasClient (component definitions)ComponentDefinitionClient
ContentClient (entries)EntryDeliveryClient + EntryManagementClient
ContentClient (content types)ContentTypeClient
Uncached*ClientbypassCache: true
RouteClient.getRouteRouteClient.get (same client, renamed method)
// 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' })

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

// 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, })
// before const content = new ContentClient({ apiKey, projectId }) await content.upsertContentType({ contentType }) // after const contentTypes = new ContentTypeClient({ apiKey, projectId }) await contentTypes.save({ contentType })

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.

ClientDeprecated → new
CategoryClientgetCategories → list, upsertCategories → save, removeCategory → remove
LabelClientgetLabels → list, upsertLabel → save, removeLabel → remove
ProjectClientgetProjects → list, upsert → save, delete → remove
DataSourceClientgetList → list, upsert → save
DataTypeClientget → list, upsert → save
LocaleClientget → list, upsert → save
WorkflowClientget → list, upsert → save
ReleaseClientget → list, upsert → save
RelationshipClientget → list
ReleaseContentsClientget → list
EntityReleasesClientget → list

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

An automation 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:

const compositions = new CompositionManagementClient(context.uniformCredentials)