SDK Getting Started
The @formecms/sdk package is a typed TypeScript HTTP client for the Forme APIs. It provides a clean, namespace-based interface for all Management and Delivery operations.
Installation
pnpm add @formecms/sdk
# or
npm install @formecms/sdk
# or
yarn add @formecms/sdk
Creating a client
The SDK auto-detects your key type and returns the appropriate client:
import { createClient } from "@formecms/sdk";
// Secret Key → ManagementClient
const mgmt = createClient({
baseUrl: "https://management.forme.sh",
apiKey: process.env.SECRET_KEY!,
});
// mgmt.type === "management"
// Read Key → DeliveryClient
const delivery = createClient({
baseUrl: "https://delivery.forme.sh",
apiKey: process.env.READ_KEY!,
});
// delivery.type === "delivery"
| Config option | Required | Description |
|---|---|---|
apiKey | Yes | Your ce_secret_... or ce_read_... key |
baseUrl | Yes | API base URL (e.g., https://management.forme.sh or https://delivery.forme.sh) |
Token type detection is based on the key prefix:
ce_secret_→ManagementClientwith 7 namespacesce_read_→DeliveryClientwith 4 namespaces
Error handling
Every SDK method returns an ApiResult<T> — a discriminated union that is either a success or error:
const result = await mgmt.entries.list();
if (result.ok) {
// Success — result.data is typed
console.log(result.data.items); // readonly MgmtEntry[]
console.log(result.data.total); // number
} else {
// Error — result.error has details
console.error(result.status); // HTTP status code (e.g., 401, 404)
console.error(result.error.code); // Error code string
console.error(result.error.message); // Human-readable message
}
This pattern avoids try/catch for API errors while keeping network errors typed. A status of 0 indicates a network error (server unreachable).
Management Client
The Management Client has 7 namespaces for write operations. Requires a Secret Key (ce_secret_).
contentModels
// List content models
const models = await mgmt.contentModels.list({ limit: 10, offset: 0 });
// Get a single content model
const model = await mgmt.contentModels.get("<MODEL_ID>");
// Create a content model
const created = await mgmt.contentModels.create({
apiId: "BlogPost",
name: "Blog Post",
type: "page",
fields: [
{ apiId: "title", name: "Title", type: "shortText", required: true, localized: true },
{ apiId: "slug", name: "Slug", type: "shortText", required: true, unique: true },
{ apiId: "body", name: "Body", type: "richText", localized: true },
],
});
// Update a content model
const updated = await mgmt.contentModels.update("<MODEL_ID>", {
name: "Article",
fields: [
/* updated fields array */
],
});
// Delete a content model
await mgmt.contentModels.delete("<MODEL_ID>");
entries
// List entries (with optional filters)
const entries = await mgmt.entries.list({
contentModelId: "<MODEL_ID>",
status: "published",
locale: "en-US",
limit: 20,
offset: 0,
});
// Field-level filtering — equality shorthand and operator objects
const post = await mgmt.entries.list({
contentModelId: "<MODEL_ID>",
fields: { slug: "my-post" }, // → fields.slug=my-post
});
const recent = await mgmt.entries.list({
contentModelId: "<MODEL_ID>",
fields: {
publishedDate: { gte: "2026-01-01" }, // → fields.publishedDate[gte]=2026-01-01
category: { in: ["tech", "news"] }, // → fields.category[in]=tech,news
},
});
// Get a single entry
const entry = await mgmt.entries.get("<ENTRY_ID>");
// Get with locale resolution (flat fields instead of locale map)
const localized = await mgmt.entries.get("<ENTRY_ID>", { locale: "de-DE" });
// Create an entry
const newEntry = await mgmt.entries.create({
contentModelId: "<MODEL_ID>",
fields: {
title: { "en-US": "Hello World" },
slug: "hello-world",
body: { "en-US": "# Welcome\n\nFirst post." },
},
});
// Update an entry (PUT — full replacement; for partial updates prefer .patch())
const updated = await mgmt.entries.update("<ENTRY_ID>", {
fields: { title: { "en-US": "Updated Title" } },
});
// PATCH — shallow merge (omitted keys preserved, null clears, localized
// fields merge at locale key)
const patched = await mgmt.entries.patch("<ENTRY_ID>", {
fields: { slug: "new-slug" },
});
// PATCH with optimistic concurrency
const result = await mgmt.entries.patch(
"<ENTRY_ID>",
{ fields: { slug: "v2" } },
{ ifMatch: previous.headers.etag }, // 412 if patched by someone else
);
// PATCH with full locale-map replacement (wipes unlisted locales)
await mgmt.entries.patch(
"<ENTRY_ID>",
{ fields: { title: { "en-US": "Only EN" } } },
{ locale: "*" },
);
// Publish / Unpublish
await mgmt.entries.publish("<ENTRY_ID>");
await mgmt.entries.unpublish("<ENTRY_ID>");
// Version history
const versions = await mgmt.entries.versions("<ENTRY_ID>", { limit: 10 });
// Delete
await mgmt.entries.delete("<ENTRY_ID>");
entries.actions (Intelligent Rewrite)
Tone-aware AI rewriting for shortText and longText fields. The server reads the stored field value — your client only identifies which field to rewrite and which tone to apply.
// Rewrite a field on an entry
const result = await mgmt.entries.actions.rewrite("<ENTRY_ID>", {
fieldName: "title",
tone: "formal", // "formal" | "casual" | "friendly" | "technical"
locale: "en-US", // optional; required for localized fields
});
if (result.ok) {
console.log(result.data.outputValue); // the proposed rewrite
console.log(result.data.auditId); // pass to approve() / discard()
console.log(result.data.model); // e.g. "claude-haiku-4-5"
}
// The rewrite is suggested, not applied. Record the editor's decision:
await mgmt.entries.actions.approve("<ENTRY_ID>", result.data.auditId);
// or
await mgmt.entries.actions.discard("<ENTRY_ID>", result.data.auditId);
// If approved, your client patches the entry with the output:
await mgmt.entries.patch("<ENTRY_ID>", {
fields: { title: result.data.outputValue },
});
Cancelling an in-flight rewrite. Pass an AbortSignal as the third argument to cancel the underlying fetch. The call resolves with { ok: false, error.code: "ABORTED" } — typically silent; no toast needed.
const controller = new AbortController();
// Example: cancel on unmount in a React component
useEffect(() => () => controller.abort(), []);
const result = await mgmt.entries.actions.rewrite(
"<ENTRY_ID>",
{ fieldName: "title", tone: "casual" },
{ signal: controller.signal },
);
Error handling. Typical codes: RATE_LIMITED / PROVIDER_RATE_LIMITED (429; backoff honoring Retry-After), CONTEXT_TOO_LARGE (400; field too big), INVALID_OUTPUT (400; model output failed validation after one retry), EMPTY_FIELD (400; stored value is empty), ALREADY_DECIDED (409; idempotent approve/discard), PROVIDER_ERROR (502; upstream outage).
See API Reference — Intelligent Actions for the full HTTP contract.
Reading response headers (ETag concurrency)
Every SDK call returns { ok, status, data, headers }. The headers map has lowercased keys, so use result.headers.etag to read the concurrency token:
const result = await mgmt.entries.patch("<ID>", { fields: { slug: "v1" } });
if (result.ok) {
const etag = result.headers.etag; // → '"5"' (strong ETag)
// Echo it on the next write to detect stale updates
const next = await mgmt.entries.patch("<ID>", { fields: { slug: "v2" } }, { ifMatch: etag });
if (!next.ok && next.status === 412) {
// Someone else patched the entry — current ETag is echoed for retry
const fresh = next.headers.etag;
}
}
assets
// List assets
const assets = await mgmt.assets.list({ status: "published", limit: 10 });
// Upload a file (browser or Node.js)
const formData = new FormData();
formData.append("file", fileBlob, "photo.jpg");
formData.append("title", "Hero Image");
formData.append("alt", "A beautiful landscape");
const uploaded = await mgmt.assets.upload(formData);
// Update metadata (PUT)
await mgmt.assets.update("<ASSET_ID>", {
title: "Updated Title",
alt: "Updated alt text",
});
// PATCH metadata with optimistic concurrency
const patched = await mgmt.assets.patch(
"<ASSET_ID>",
{ title: "New title" },
{ ifMatch: prior.headers.etag }, // 412 if changed elsewhere
);
// Replace the file
const replaceForm = new FormData();
replaceForm.append("file", newFileBlob, "photo-v2.jpg");
await mgmt.assets.replaceFile("<ASSET_ID>", replaceForm);
// Publish / Unpublish
await mgmt.assets.publish("<ASSET_ID>");
await mgmt.assets.unpublish("<ASSET_ID>");
// Version history
const versions = await mgmt.assets.versions("<ASSET_ID>");
// Delete
await mgmt.assets.delete("<ASSET_ID>");
environments
// List environments
const envs = await mgmt.environments.list();
// Returns: readonly MgmtEnvironment[] (not paginated)
// Create an environment
const staging = await mgmt.environments.create({
name: "staging",
slug: "staging",
});
// Update
await mgmt.environments.update("<ENV_ID>", { name: "Staging (v2)" });
// Delete (returns 409 if it has content — use force to cascade)
await mgmt.environments.delete("<ENV_ID>", { force: true });
locales
// List locales
const locales = await mgmt.locales.list();
// Returns: readonly MgmtLocale[] (not paginated)
// Create a locale
await mgmt.locales.create({
code: "de-DE",
name: "German",
fallbackLocaleId: "<DEFAULT_LOCALE_ID>",
});
// Update
await mgmt.locales.update("de-DE", { name: "Deutsch" });
// Delete
await mgmt.locales.delete("de-DE");
workspace
// Get workspace info
const ws = await mgmt.workspace.get();
// Returns: MgmtWorkspace { id, name, slug, accountId, accountName, ... }
// Update workspace name
await mgmt.workspace.update({ name: "My Project" });
// Get Intelligent credit usage for the current billing month
const usage = await mgmt.workspace.aiUsage();
if (usage.ok) {
console.log(usage.data.creditsUsed); // 34
console.log(usage.data.creditsLimit); // 1000
console.log(usage.data.resetsAt); // "2026-05-01T00:00:00.000Z"
console.log(usage.data.breakdown); // { rewrite: 34 }
}
apiKeys
// List API keys (shows key hints, not full keys)
const keys = await mgmt.apiKeys.list();
// Each key: { id, label, keyHint, keyType, prefix, revokedAt, ... }
// Revoke a key
await mgmt.apiKeys.revoke("<KEY_ID>");
Delivery Client
The Delivery Client has 4 namespaces for read-only access to published content. Requires a Read Key (ce_read_).
entries
// List published entries
const entries = await delivery.entries.list({ limit: 10 });
// Filter by content model
const posts = await delivery.entries.list({
contentModelId: "<MODEL_ID>",
locale: "en-US",
});
// Include linked entries and assets
const withIncludes = await delivery.entries.list({ include: 1 });
if (withIncludes.ok && withIncludes.data.includes) {
console.log("Linked entries:", withIncludes.data.includes.entries);
console.log("Linked assets:", withIncludes.data.includes.assets);
}
// Get a single entry (with locale resolution)
const entry = await delivery.entries.get("<ENTRY_ID>", { locale: "de-DE" });
contentModels
// List content models
const models = await delivery.contentModels.list({ limit: 10 });
// Get a single content model
const model = await delivery.contentModels.get("<MODEL_ID>");
assets
// List published assets
const assets = await delivery.assets.list({ limit: 10 });
// Get a single asset
const asset = await delivery.assets.get("<ASSET_ID>");
// asset.url is a public URL for safe MIME types (image, video, audio, pdf, font):
// "https://storage.googleapis.com/forme-assets-cdn/550e8400-.../v3/hero.jpg"
// (or "https://cdn.forme.build/..." when CDN mode is enabled)
// For unsafe types (HTML, SVG) it's the auth-gated delivery path:
// "/delivery/assets/<ASSET_ID>/file"
// Treat asset.url as opaque — use it directly in <img> tags, no proxy needed.
console.log(asset.url);
locales
// List available locales
const locales = await delivery.locales.list();
// Returns: readonly DlvLocale[] (not paginated)
Key types
The SDK exports these TypeScript types for use in your application:
| Type | Description |
|---|---|
ClientConfig | Configuration for createClient() |
ManagementClient | Client with 7 write namespaces |
DeliveryClient | Client with 4 read-only namespaces |
ApiResult<T> | Success or error response union |
ApiResponse<T> | Success response (ok: true) |
ApiErrorResponse | Error response (ok: false) |
PaginatedResult<T> | { items, total, limit, offset } |
MgmtContentModel | Management API content model shape |
MgmtEntry | Management API entry shape |
MgmtAsset | Management API asset shape |
MgmtEnvironment | Management API environment shape |
MgmtLocale | Management API locale shape |
MgmtWorkspace | Management API workspace shape |
MgmtApiKey | Management API key shape |
AiUsageStats | Monthly credit usage stats |
Import types from the package:
import type { MgmtEntry, PaginatedResult, ApiResult } from "@formecms/sdk";
React hooks
The @content-engine/react package provides React hooks for data fetching and mutations, used internally by the Admin UI. Public documentation for the hooks layer will be available when the composable UI library ships in a future release.
Next steps
- Live Showcase — see the SDK in action, a Next.js app powered by Forme (source on GitHub)
- API Reference — authentication, pagination, and error handling details
- Core Concepts — understand the data model behind the SDK
- Quickstart — end-to-end tutorial using the SDK