@forumulate/sdk

中文文档
0.13.8发布于 2026/11/13MITTypeScriptCommonJS零依赖近 30 天下载 5025

Typed TypeScript client for the Forumulate REST API.

原始使用文档
文档目录20 节
  1. Install
  2. Authenticate
  3. Quickstart
  4. Every operation is a method
  5. Pagination
  6. Common tasks
  7. 1. Write and publish a page from markdown
  8. 2. List everything in a space, with content
  9. 3. Search
  10. 4. Start a forum topic and reply
  11. 5. Verify a webhook
  12. 6. Translations, any kind of piece
  13. 7. Gate sign-up and sign-in on your published policies
  14. Every endpoint: typed()
  15. Errors
  16. Retries and idempotency
  17. Options
  18. Links
  19. Maintainers: regenerating the types
  20. License

@forumulate/sdk

A typed TypeScript client for the Forumulate REST API: docs, forum, blog and everything else in an organization. It has no runtime dependencies and runs in Node 18+, Bun, Deno and the browser (global fetch).

Install

npm install @forumulate/sdk

Authenticate

Mint a Personal Access Token at Account → Tokens (forumulate.com/account/tokens). A token needs read to read and read + write to change anything. API tokens and scopes explains the two gates a token passes.

import { ForumulateClient } from '@forumulate/sdk';

// Reads FORUMULATE_TOKEN and FORUMULATE_URL (default https://forumulate.com).
const forumulate = ForumulateClient.fromEnv();

// …or explicitly. The API base is the site plus /api.
const explicit = new ForumulateClient({
  baseUrl: 'https://forumulate.com/api',
  token: process.env.FORUMULATE_TOKEN,
});
VariableDefaultMeaning
FORUMULATE_TOKENnonePersonal Access Token, sent as Authorization: Bearer.
FORUMULATE_URLhttps://forumulate.comThe site. The API is its /api. A value already ending in /api is used as-is.
FORUMULATE_API_URLnoneExact API base, for a local API with no /api prefix (http://localhost:4000).

The CLI and the MCP server read the same variables.

Quickstart

const { user } = await forumulate.auth.me();
const { orgs } = await forumulate.orgs.list();
const orgId = orgs[0]!.id; // ids and slugs are both accepted wherever an org id is

// Every list is an async iterator over every row, across every page.
for await (const space of forumulate.spaces.list({ orgId })) {
  console.log(space.slug, space.type);
}

Every operation is a method

Each of the API's ~1,800 operations is a method named after its operationId: ForumTopics_get is forumulate.forum.topics.get, Webhook_rotate is forumulate.webhooks.rotate. Paths, query, body and response are typed from the OpenAPI spec, and the namespaces are generated from it, so a route in the API reference is a method here.

// One argument: path params at the top level, `query` and `body` nested.
const { space } = await forumulate.spaces.get({ orgId, spaceId });
await forumulate.blog.create({ orgId, spaceId, body: { title: 'v2 is out', content } });
await forumulate.webhooks.update({ orgId, webhookId, body: { active: false } });
await forumulate.webhooks.delete({ orgId, webhookId }); // `delete` aliases the API's `remove`
await forumulate.tokens.rotate({ id: patId });
await forumulate.forum.taxonomy.listCategories({ orgId, spaceId }); // a sub-namespace per area of a namespace
AreaNamespaces
Organizationorgs, spaces, sites, members, invites, roles, grants, groups, tokens, users, billing, sso, scim
Docsitems, pages, collections, shapes, snapshots, changeRequests, docs, openapi, comments
Community and publishingblog, forum, community, moderation, chat, messages, changelog, roadmap, newsletter
Space typesstatus, events, courses, faq, careers, legal, forms, questions
Platformwebhooks, domains, themes, extensions, integrations, notifications, mail, assets, embeds, privacy
Search, AI and analyticssearch, ai, analytics
Headless CMScontent (libraries, entries, model, locales, keys, assets…), delivery
Public reader (no token)public, and the public… sub-namespace of each area: forum.publicTopic, status.public, blog.public…

method.operationId tells you which operation a method calls. By id, with the same types:

const { space } = await forumulate.request('Space_get', { orgId, spaceId });
for await (const hook of forumulate.paginate('Webhook_list', { orgId })) console.log(hook.url);

The 0.5 positional helpers (spaces.list(orgId), items.create(orgId, spaceId, input)…) still work unchanged, except auth.register and auth.login, removed in 0.6: their routes went away when sign-in moved to Atlas, so they failed for every caller. Authenticate with a Personal Access Token. Where one shares a name with a generated method it is the same operation, and the member takes either call shape.

Pagination

A list returns a PagePromise: await it for the first page as the API answered it, for await it for every row.

const first = await forumulate.blog.list({ orgId, spaceId, query: { limit: 50 } });
first.page.nextCursor; // the envelope: page.{limit, hasMore, nextCursor, total}, rows under data

for await (const post of forumulate.blog.list({ orgId, spaceId })) {
  /* every post */
}
for await (const page of forumulate.blog.list({ orgId, spaceId }).pages()) {
  /* every page */
}

The walk follows page.nextCursor, and ?page= for the ranked lists that have no cursor. The generated methods send Forumulate-Version (the version the SDK was generated at, API_VERSION, unless you set apiVersion) because only a versioned answer carries the envelope.

Common tasks

1. Write and publish a page from markdown

items is the type-agnostic surface: the same calls write a docs page, a blog post or a forum topic.

const { item } = await forumulate.items.create(orgId, spaceId, {
  title: 'Install',
  slug: 'install',
  body: '# Install\n\nRun `npm i acme`.',
});
await forumulate.items.publish(orgId, spaceId, item.id);

// Later: replace the body and republish.
await forumulate.items.setContent(orgId, spaceId, item.id, { body: '# Install\n\n…' });
await forumulate.items.publish(orgId, spaceId, item.id);

2. List everything in a space, with content

const res = await forumulate.items.list(orgId, spaceId, { limit: 100, include: ['body'] });
for (const it of res.items) console.log(it.id, it.title, it.contentIn);
// res.page.nextCursor pages onward; res.sync.checkpoint drives an incremental sync.

Ask for body. sections is filled only for pieces written against a shape, so a space of free-form pages answers include: ['sections'] alone with empty objects.

// Everything you can see in an org: docs, forum, blog.
const { hits } = await forumulate.search.unified(orgId, 'rate limits', { limit: 10 });

// A public docs space, no token needed.
const anon = new ForumulateClient({ baseUrl: 'https://forumulate.com/api' });
const { hits: pub } = await anon.public.search(orgId, spaceId, 'rate limits');

4. Start a forum topic and reply

const doc = (text: string) => ({
  type: 'doc' as const,
  content: [{ type: 'paragraph', content: [{ type: 'text', text }] }],
});
const { topic } = await forumulate.forum.createTopic(orgId, forumSpaceId, {
  title: 'v2 is out',
  content: doc('Here is what changed.'),
});
const topicId = (topic as { id: string }).id;
await forumulate.forum.reply(orgId, forumSpaceId, topicId, doc('Upgrade notes are in the docs.'));

5. Verify a webhook

import { constructWebhookEvent } from '@forumulate/sdk';

// Verifies the signature and its timestamp, THEN parses; throws if either fails.
const event = await constructWebhookEvent({
  secret: process.env.FORUMULATE_WEBHOOK_SECRET!,
  body: rawBody, // the exact bytes received, before JSON.parse
  signature: req.headers['x-forumulate-signature'] as string,
  timestamp: req.headers['x-forumulate-timestamp'] as string, // deliveries older than 5 min fail
});

verifyWebhookSignature (also forumulate.webhooks.verifySignature) is the same check returning a boolean. Create the endpoint with forumulate.webhooks.create({ orgId, body: { url, events } }); webhooks.rotate, webhooks.ping, webhooks.deliveries and webhooks.replay manage it after.

6. Translations, any kind of piece

client.translations picks the right route from the space's type: a docs page's language twin, a post's, topic's or release's translation, or an incident's. You only name the original piece and the language. Machine translation uses your organization's own DeepL key or AI provider. See Translations everywhere.

const cov = await client.translations.status(orgId, spaceId); // piece × language → missing/draft/published/outdated
await client.translations.locales.set(orgId, { spaceId }, [{ code: 'es', label: 'Español' }]);
await client.translations.auto(orgId, spaceId, pageId, 'es');
const es = await client.translations.get(orgId, spaceId, pageId, 'es'); // { title, markdown, state }
await client.translations.put(orgId, spaceId, pageId, 'es', { markdown: '…', published: true });

7. Gate sign-up and sign-in on your published policies

client.policies and three standalone functions are the server half of the policy acceptance gate: your Legal space's published Terms, Privacy Policy and so on, accepted by your users in any app, whatever signs them in.

import { ForumulateClient, signPolicyTicket, verifyAcceptanceToken, policyGatePageUrl } from '@forumulate/sdk';

const client = ForumulateClient.fromEnv();
const space = { orgId: process.env.FORUMULATE_ORG!, spaceId: process.env.FORUMULATE_LEGAL_SPACE! };

// What does this user still have to accept before signing in?
const status = await client.policies.status({ ...space, subject: user.id, event: 'sign_in' });

if (!status.satisfied) {
  // Send them to the hosted acceptance page with a ticket you sign yourself (no API call).
  const ticket = await signPolicyTicket(
    { ...space, subject: user.id, event: 'sign_in', returnUrl: 'https://app.example.com/policy/return' },
    process.env.FORUMULATE_POLICY_GATE_KEYS!, // "pgk_…:pgs_…"
  );
  return redirect(policyGatePageUrl({ ...space, ticket }));
}

// On the way back (or from the widget): verify the acceptance token offline, with key rotation.
const r = await verifyAcceptanceToken(token, {
  keys: process.env.FORUMULATE_POLICY_GATE_KEYS,
  subject: user.id,
  requireSatisfied: true,
});

Also client.policies.accept / claim / verify / acceptances / export / erase, and client.policies.list({ spaceId, secret }) to read Forumulate policy feed v1. verifyPolicyWebhook(headers, rawBody, secret) checks a policy.published / policy.retired notice, and signPolicyFeedRequest / verifyPolicyFeedRequest implement the feed's request signing, so any platform can consume the feed or serve its own. Recipes for Auth0, Clerk, Supabase, Firebase, Auth.js and a custom backend: Policy gate recipes.

Every endpoint: typed()

The helpers above cover the common paths. typed(method, path, init) covers every route in the OpenAPI spec, with the path, params, query, body and response all inferred, and autocomplete on path and method:

const { spaces } = await forumulate.typed('get', '/orgs/{orgId}/spaces', {
  params: { orgId },
  query: { limit: 50 },
});

await forumulate.typed('post', '/orgs/{orgId}/spaces', {
  params: { orgId },
  body: { name: 'Docs', slug: 'docs', type: 'DOCS', visibility: 'PUBLIC' },
});

Pass raw: true for the untouched Response (binary downloads). For anything else, request(method, path, { body, query, headers }) is the untyped escape hatch that typed and the helpers use.

Operations that answer a file (PDF, PNG, ZIP) or a stream resolve to the Response; text answers (CSV, Markdown, iCal) resolve to a string.

Errors

Every non-2xx response throws a ForumulateError carrying the API's error envelope: status, code, message, details (field-level, for validation.failed), requestId (quote it in a support request) and retryAfter (seconds, when the API said).

import { ForumulateError } from '@forumulate/sdk';

try {
  await forumulate.orgs.create({ body: { name: '', slug: '' } });
} catch (err) {
  if (err instanceof ForumulateError && err.code === 'validation.failed') {
    console.log(err.details); // { name: ['Required'], … }
  }
}

A 401 means the token is missing or revoked. A 403 means it lacks a scope or capability; see API tokens and scopes. A network failure that outlives its retries is a ForumulateError with status: 0 and code: 'network.error'.

Retries and idempotency

Requests that are safe to repeat are retried on 408, 429, 502, 503, 504 and network failures: two retries by default, waiting what Retry-After says (a wait over a minute is surfaced rather than slept through), else exponential backoff with jitter. A 500 is not retried.

Every POST and PATCH carries an Idempotency-Key, generated once per call and reused by its retries, so a write that timed out is retried without running twice. Pass your own to dedupe across processes:

await forumulate.items.create({ orgId, spaceId, body, idempotencyKey: message.id });

Each call also takes headers, signal (an AbortSignal) and maxRetries.

Options

new ForumulateClient({
  baseUrl: 'https://forumulate.com/api',
  token,
  apiVersion: '2027-02-01', // Forumulate-Version for every request (default: generated methods only)
  maxRetries: 2, // 0 turns retries off
  autoIdempotencyKey: true, // generated Idempotency-Key on POST/PATCH
  userAgent: 'my-app/1.0', // default forumulate-sdk-js/<version> outside a browser
  headers: { 'X-Trace': id }, // sent on every request
  fetch: customFetch, // any fetch-compatible function
  credentials: true, // browser on the same origin: send the session cookie instead of a token
});

The default User-Agent matters outside a browser: the edge firewall challenges default library User-Agents.

The package has no runtime dependencies and is marked side-effect free; the route table behind the namespaces is about 100 KB unminified (about 20 KB gzipped) and namespaces are built on first use.

Maintainers: regenerating the types

src/schema.ts backs typed(). It is generated and types-only; do not hand-edit it. Refresh it from the deployed spec:

curl -s https://forumulate.com/api/openapi.json -o packages/sdk/openapi.json
pnpm --filter @forumulate/sdk generate-schema   # → src/schema.ts, then src/operations.gen.ts

src/operations.gen.ts is the route table behind the namespaces (generate-operations rebuilds it alone). Placement lives in scripts/generate-operations.mjs: a short map from operationId tag prefixes to namespaces, so a new tag lands somewhere without an edit. src/coverage.test.ts fails when the table is stale, when an operation has no method, or when a 0.5 helper and a generated method share a name but not an operation.

License

MIT

版本与发布记录

@forumulate/sdk 共发布 28 个版本,首次发布于 2026/10/03;过去 12 个月发布了 28 个版本。

版本发布日期距今
0.13.8latest2026/11/132 天前
0.13.72026/11/133 天前
0.13.62026/11/133 天前
0.13.52026/11/123 天前
0.13.42026/11/124 天前
0.13.32026/11/124 天前

关于 @forumulate/sdk 的常见问题

@forumulate/sdk 是什么?

Typed TypeScript client for the Forumulate REST API.

如何安装 @forumulate/sdk?

在项目目录运行 npm install @forumulate/sdk,或使用 pnpm add / yarn add / bun add。

@forumulate/sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@forumulate/sdk 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@forumulate/sdk 可以商用吗?

@forumulate/sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@forumulate/sdk 的下载量有多少?

根据 npm 官方下载统计,@forumulate/sdk 在 2026/10/16 – 2026/11/14 这 30 天内被下载 5,025 次,日均约 168 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@forumulate/sdk 还在维护吗?

最新版本 0.13.8 发布于 2026/11/13(2 天前);至今共发布 28 个版本,其中过去 12 个月发布了 28 个;首次发布于 2026/10/03。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。