Orchestrator-safe writes
A provisioning tool, a CI job or an agent can retry a request whose first answer it never saw. The API is built so that retry is safe. A write can be made idempotent, rehearsed without side effects, and its shown-once secret sealed for a single recipient. A sandbox organization can be removed outright once its run is over.
Provision the whole organization: the manifest
Rather than calling each create in turn, a provisioning run can send one document: PUT /orgs/{orgId}/manifest. It is Forumulate's own provisioning contract — drive it from a script, or the owner's own tooling.
Units. Besides spaces (themes, redirects, collections, categories), a
"version": 2manifest declaresdomains,reader_auth,consent,connectors,executor,webhooks,status_monitorsandbrand, naming spaces and sites by slug. These block names are exactly as the API takes them:reader_authandstatus_monitorsare the two snake_case keys in an otherwise camelCase document (readerAuthis a different key, the one-word shorthandreaderAuth: atlas). Every key is listed in the version 2 reference.Plan, then apply. Without
?apply=truethe answer is a plan:stepsof{unit, ref, action, diff, requires, id}, whereidis the resource the step would change (ornullfor a create). With?apply=trueeach step answers{unit, ref, id, action, status, readBack}, the apply runs as a job (job, thenGET /orgs/{orgId}/manifest/runs/{runId}), and what only the customer can do (DNS records) is listed undermanual.Re-runs are safe. Each unit's resource is recorded as it lands, so a run killed half-way and started again creates nothing twice.
Adopt what exists.
GET /orgs/{orgId}/manifest?export=trueanswers the organization as it is now, as aversion: 2manifest with everyid. Applying it unchanged is a no-op that records those ids.
External references
The creates an orchestrator runs most take externalRef: organizations, sites, spaces, domains, webhooks, personal access tokens, integration tokens, connectors, status monitors and the privacy executor key. It is your own name for the thing. The older spelling metadata.forgedeck_ref is still accepted as the same field, so a run written against it keeps working.
The first create with a given externalRef makes the resource and answers 201 as usual. A repeat finds that resource and answers 200 with idempotent: true. Nothing is duplicated, and a secret shown at creation is not shown again (the field is null). Two creates racing on one reference both answer the same resource, because the loser discards its own row.
Lost the secret of a resource you made this way? Rotate it. For a personal access token that is POST /pats/{id}/rotate, with the id from the 200 answer. The rotated token keeps its externalRef and tags, so the next run finds it again rather than minting a second one. See get-or-create with externalRef.
Idempotency-Key
Any authenticated write also takes an Idempotency-Key header. Use a fresh value (a UUID) for each logical request, and send the same value when you retry it.
The first request with a key runs. Its
2xxanswer is stored for 24 hours, scoped to you (the user or token that sent it), encrypted at rest.A repeat of that request with the same body replays the stored status and body, with
Idempotent-Replayed: true, and writes nothing.A repeat while the first is still running is
409 idempotency.in_flight. Wait a moment and send the same request again.The same key with a different body is
409 idempotency.key_reused: a key names one request, not a session.
A refused or failed attempt (4xx, 5xx) is not stored. Fix the request and retry it under the same key.
Dry runs
?dryRun=true works on every authenticated write that has no dry run of its own. The request is authenticated, authorized and validated exactly as it would be, then answers 200 and writes nothing. A 422 on a dry run is the 422 the real write would get.
"Dry run" is spelled five ways, and they do not all mean the same thing:
Where | Spelling | The default | What a dry run answers |
|---|---|---|---|
Any authenticated write without its own |
| writes |
|
The manifest, |
| dry run | The per-unit plan: |
| the command name |
| The same plan, from a |
|
| writes | The plan for the folder (create, update, move, prune), and nothing sent. Against a public space it needs no token. |
|
| dry run | The plan per piece; |
|
| sends | The request the CLI would send — method, URL, headers, body — printed, not sent. Where the operation has a dry run of its own it is passed through instead. |
Rollback
There is no single undo; each kind of thing has its own way back. All of them are ordinary writes, so they take Idempotency-Key and ?dryRun=true like any other.
What changed | Way back | Operation |
|---|---|---|
A page's published text | Republish an earlier revision (list them with |
|
A page's draft | Restore a named checkpoint ( |
|
A deleted page | Take it out of the trash ( |
|
A whole docs space | Restore a snapshot you took earlier ( |
|
A deleted space | Take it out of the trash |
|
A blog post, forum topic or release you deleted | Recover it |
|
A content-library entry | Restore one of its revisions; a trashed entry from the library's trash |
|
A content release | Roll the release back |
|
A space's or site's theme draft | Discard the draft; the live theme is untouched until a draft is published |
|
The whole organization | Restore a backup: brings back what is missing, deletes nothing made since |
|
Site config, a live theme, SEO settings and manifest applies keep no history of their own. To be able to go back, read the current state first — GET /orgs/{orgId}/manifest?export=true answers the organization as a version: 2 manifest — keep that file, and apply it again to return.
Sealed secrets
Send X-Secret-Recipient: x25519:<base64 public key> on a write that mints a shown-once secret (a token, a webhook signing secret, an executor key, a connector token, a preview link). The answer then carries the secret only as sealed_secret, a libsodium sealed box that only the holder of the matching private key can open, so it never sits in a log in plain text. A malformed key is a 422 and nothing is minted.
Metadata tags
metadata is a flat map of your own tags: up to 50 keys of letters, digits and _ . : -, each a string (up to 500 characters), number, boolean or null. Set it at create. The lists of organizations, spaces, sites, domains, webhooks, tokens, connectors and status monitors filter on it with ?metadata[env]=staging. A malformed filter is a 422, never a filter that silently matched everything.
To change the tags later, send metadata in a PATCH. It replaces the tags whole, {} clears them, and the externalRef stays as it was set at create:
Resource | Request |
|---|---|
Organization |
|
Space |
|
Site |
|
Domain |
|
Webhook |
|
Connector |
|
Status monitor |
|
Personal access token |
|
Integration token |
|
A PATCH that sends only metadata changes nothing else. On a space it does not fire space.updated. A domain is re-tagged only through the space, site or library it is attached to; any other path is a 404. The domain route takes metadata and nothing else, because a domain's other settings have their own routes (www, redirect, ip-allow-list). On the two token routes any other field is a 422. Tags are labels, and a token's grants stay as they were minted. A revoked integration token is a 404.
Sandbox delete
An organization created for a test run can be removed for good:
DELETE /orgs/{orgId}?sandbox=true&confirm=<the org's slug>The organization must be tagged a sandbox (
metadata.sandbox: true, or the older aliasmetadata.forgedeck_sandbox: true, set atPOST /orgs). Otherwise the answer is409 sandbox.not_tagged. UseDELETE /orgs/{orgId}withoutsandboxfor the ordinary, recoverable delete.confirmmust be the slug. Otherwise the answer is422 validation.failedwithreason: confirm_required.A reference that does not cascade is
409 sandbox.delete_blocked, naming the constraint.
Each refusal changes nothing. A delete that goes through removes the organization and everything in it. It cannot be undone, and it is recorded outside the organization (its own audit log goes with it), with the actor, the token and the request's trace id.
In the API
Every operation below is also reachable from the other surfaces without a dedicated command. forumulate api <Operation> calls it from the CLI, the JavaScript SDK has a typed method for each one (the last column names it; the Python and Go SDKs are generated from the same spec), and an agent reaches the organization-scoped ones through the MCP agent server — its api_* family tools, or call_api. Where a CLI command or a hand-made MCP tool exists too, the last column names it. Operation names link to the API reference.
Domain Tags
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Replace a space domain’s tags |
| |
| Replace a content library domain’s tags |
| |
| Replace a site domain’s tags |
|
Manifest
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| Export the organization as a manifest (adopt) |
| |
| Apply a declarative site manifest (dry run by default) |
| |
| Get a manifest apply run |
|
Releases
Operation | Request | What it does | Shortcuts |
|---|---|---|---|
| List releases |
| |
| Create a release |
| |
| Find drafts to add to a release |
| |
| Get a release |
| |
| Rename a release |
| |
| Delete a release |
| |
| Add a draft to a release |
| |
| Remove an item from a release |
| |
| Schedule a release |
| |
| Unschedule a release |
| |
| Publish a release now |
| |
| Roll a release back |
| |
| Preview a release |
|