Docs/SDK Getting Started

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 optionRequiredDescription
apiKeyYesYour ce_secret_... or ce_read_... key
baseUrlYesAPI base URL (e.g., https://management.forme.sh or https://delivery.forme.sh)

Token type detection is based on the key prefix:

  • ce_secret_ManagementClient with 7 namespaces
  • ce_read_DeliveryClient with 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:

TypeDescription
ClientConfigConfiguration for createClient()
ManagementClientClient with 7 write namespaces
DeliveryClientClient with 4 read-only namespaces
ApiResult<T>Success or error response union
ApiResponse<T>Success response (ok: true)
ApiErrorResponseError response (ok: false)
PaginatedResult<T>{ items, total, limit, offset }
MgmtContentModelManagement API content model shape
MgmtEntryManagement API entry shape
MgmtAssetManagement API asset shape
MgmtEnvironmentManagement API environment shape
MgmtLocaleManagement API locale shape
MgmtWorkspaceManagement API workspace shape
MgmtApiKeyManagement API key shape
AiUsageStatsMonthly 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