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

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

filters: { "type[eq]": "brand" } // flat key filters: { type: { eq: "brand" } } // nested object
OperatorDescription
eqEquals
neqNot equal
matchContains (text search) match
startsStarts with. Value limited to letters, numbers, _, ., -, and spaces
lt / lteLess than / less than or equal to
gt / gteGreater than / greater than or equal to
inMatches any value in a comma-separated list (OR)
ninDoes not match any value in a list
allList-valued fields (multi-select fields, labels, labelGroups) must contain every value in a comma-separated list (AND)
deftrue or false; whether the field has a value at all

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.

FieldSupported operators
entityId, type, uiStatus, locale, creatorSubject, authorSubjecteq, neq, in, nin
editionId, releaseId, patternId, workflowId, workflowStageId, categoryIdeq, neq, in, nin, def
created, modifiedeq, neq, lt, lte, gt, gte, in, nin
name, slugmatch, starts, eq, neq, in, nin, def
creator, authormatch, starts, eq, neq, in, nin
projectMapId, projectMapNodeId (compositions)eq, neq, in, nin, def
labels, labelGroupseq, neq, in, nin, all, def
Field typeSupported operators
Text, selectmatch, starts, eq, neq, in, nin, def
Number, date, datetimeeq, neq, lt, lte, gt, gte, in, nin, def
Checkboxeq, neq, def
Multi-selecteq, neq, in, nin, all, def
Rich textmatch, starts, def
Reference, asset (by ID)eq, neq, in, nin, def

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-propertySupported operators
Reference .name, .slug; asset .url, .title, .description; link pathmatch, starts, eq, neq, in, nin, def
Reference .type, .uiStatus; link .type, .projectMapNodeId; asset .mediaTypeeq, neq, in, nin, def

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

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

Combine multiple filters to narrow results:

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

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

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

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 entries endpoint OpenAPI specification.

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.

A projection spec has three buckets:

BucketSelects byExample
fieldsField namefields: { only: ["title", "slug"] }
fieldTypesField type ID (e.g. text, richText, asset)fieldTypes: { except: ["richText"] }
slotsSlot name (compositions only)slots: { only: ["hero"] }
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 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
OperatorTypeDescription
onlystring[]Keep only fields of the named types
exceptstring[]Drop fields of the named types
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; override the container-wide depth for that slot
  • 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.

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

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

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

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.

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);