Import the doc

Embed
Preview of the card
<a href="https://forumulate.com/reference/forumulate-api-import-importfiles"><img src="https://forumulate.com/reference/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/8042f004-e3d9-445f-b7a4-b6c1f86836e6/pages/cf8930e2-3ede-4f07-8551-5bbed1b64007/social-card.svg?v=3e7ae33eec8b" alt="Import the doc" width="600" height="315"></a>
POSThttps://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/docs/import

Writes a batch of pages from Markdown or block JSON in one transaction, creating the collections they need. The door a migration or a git sync drives; it reports what it would do first if you ask for a dry run.

Requires token scope: write (the coarse method gate) for Atlas key (ak_), Personal Access Token (cft_) or OAuth (cfo_) auth — in addition to any catalog scope (GET /pats/scopes) the route’s capability needs. Session/cookie auth is governed by RBAC capabilities instead.

onExisting is fail | replace | skip | rename | duplicate and defaults to replace (this importer syncs authored files per file, and the first import of a repo routinely lands on the starter page a new space ships with). An unrecognised value is now a 422 — it used to fall through to replace, so a typo chose the destructive option. fail refuses to write over an existing page, counts it in failed, lists its slug in refused[], and keeps prune from deleting it.

On the multipart doors, dryRun and the rest are read exactly as strictly as the JSON fields of the same name: true/false/1/0/yes/no/on/off, and anything else is a 422. dryRun=maybe used to run a REAL import.

Parameters
orgIdstringpathrequired
spaceIdstringpathrequired
forumulate-versionstringheader
API version negotiation. Send forumulate-version: 2026-12-01 on every request from a new integration; it is optional only so that integrations written before versioning keep working. Each version includes everything before it:
Idempotency-Keystringheader
Makes this write safe to retry. Optional — send any unique string (a UUID is ideal) and the first request runs while its response is remembered under that key for 24 hours; an identical retry replays that same status and body instead of acting twice, so a timeout or a 5xx from the edge can be retried without creating a second row. A retry sent while the first request is still in flight is refused with 409. The key is scoped to the method and path, so the same key on a different endpoint is a different request. Error responses of 5xx are never cached — those retries re-run. A key is bound to the BODY it was first used with: reusing it with a different body is a 409 idempotency.key_reused, not a replay, because replaying would tell you a write landed that never ran. Generate one key per request, never per process or per session.
Request body application/json

A Markdown file may declare its own address. Two frontmatter keys are read, and unknown keys are still ignored:

  • slug: — the address this file lands at, instead of the one its filename spells. Slugified, so Billing & Plans and billing-and-plans are the same address; a value with nothing a URL can hold in it is refused with the file named, never quietly renamed. Two files declaring one slug is a 422.

  • redirect_from: — the addresses this page REPLACES, comma-separated, each rooted and relative to the space's mount (/old-page, never /docs/old-page — the reader prepends the mount itself). The import writes a redirect rule for each one and moves the page that answered there to the trash, because a rule alone leaves the old page in the sitemap, llms.txt and search. Both are reported on the response as redirects and retired. A self-redirect, a duplicate or a loop is a 422, exactly as it is on PUT …/redirects; an operator's own manual rule for that address is left alone.

filesobject[]required
Show child parameters
namestringrequired
contentstringrequired
Up to 2,000,000 characters of Markdown — the same ceiling the importer, the create and the update doors all use, so an imported page can always be written back.
collectionstring
collectionIdstring
replaceCollectionboolean
Empty the target collection BEFORE importing (hard reset). Prefer prune for sync.
onExistingstring
What to do about a slug the SPACE already has (resolved space-wide, never per-collection): replace (the default) updates the existing page in place; fail writes nothing over it and lists its slug under refused (the default of POST …/openapi/import, offered here so one explicit value makes both importers behave identically); skip leaves it untouched; rename is the ONLY value that auto-suffixes to a free slug like getting-started-2 (opt-in — a page at a URL nobody wrote is the worst docs-as-code outcome); duplicate is the legacy spelling of rename. An unrecognised value is a 422, never a silent replace.
fail | replace | skip | rename | duplicate
prunestring
Sync mode. After importing, soft-delete (to trash) every live page in this scope the run did not create/replace/skip — so deleting a source file makes its published page disappear. collection scopes to the target collection; space to the whole space.
space | collection
dryRunboolean
Plan only. Returns what WOULD happen — the same counts (imported counts creates AND replaces, so imported+skipped+failed == files.length and replaced is a subset of imported), a per-file plan, cross-collection collisions, and the prunedSlugs a prune would remove — and writes nothing. dryRun: true sets dryRun on the response.
{
  "files": [
    {
      "name": "string",
      "content": "string"
    }
  ],
  "collection": "string",
  "collectionId": "string",
  "replaceCollection": true,
  "onExisting": "fail",
  "prune": "space",
  "dryRun": true
}
Code samples
cURL
curl -X POST "https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/docs/import" \
  -H "forumulate-version: <value>" \
  -H "Idempotency-Key: <value>" \
  -H "Content-Type: application/json" \
  -d '{"files":[{"name":"string","content":"string"}],"collection":"string","collectionId":"string","replaceCollection":true,"onExisting":"fail","prune":"space","dryRun":true}'
TypeScript
const res = await fetch('https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/docs/import', {
  method: 'POST',
  headers: {
    'forumulate-version': '<value>',
    'Idempotency-Key': '<value>',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"files":[{"name":"string","content":"string"}],"collection":"string","collectionId":"string","replaceCollection":true,"onExisting":"fail","prune":"space","dryRun":true}),
});
const data = await res.json();
Python
import requests

res = requests.request(
    "POST",
    "https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/docs/import",
    headers={
    "forumulate-version": "<value>",
    "Idempotency-Key": "<value>",
    "Content-Type": "application/json",
    },
    json={"files": [{"name": "string", "content": "string"}], "collection": "string", "collectionId": "string", "replaceCollection": True, "onExisting": "fail", "prune": "space", "dryRun": True},
)
data = res.json()
Go
package main

import (
	"fmt"
	"io"
	"net/http"
	"strings"
)

func main() {
	req, _ := http.NewRequest("POST", "https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/docs/import", strings.NewReader(`{"files":[{"name":"string","content":"string"}],"collection":"string","collectionId":"string","replaceCollection":true,"onExisting":"fail","prune":"space","dryRun":true}`))
	req.Header.Set("forumulate-version", "<value>")
	req.Header.Set("Idempotency-Key", "<value>")
	req.Header.Set("Content-Type", "application/json")
	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)
	fmt.Println(string(body))
}
PHP
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/docs/import');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['forumulate-version: <value>', 'Idempotency-Key: <value>', 'Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, '{"files":[{"name":"string","content":"string"}],"collection":"string","collectionId":"string","replaceCollection":true,"onExisting":"fail","prune":"space","dryRun":true}');
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Ruby
require 'net/http'
require 'uri'

uri = URI('https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/docs/import')
req = Net::HTTP::Post.new(uri)
req['forumulate-version'] = '<value>'
req['Idempotency-Key'] = '<value>'
req['Content-Type'] = 'application/json'
req.body = '{"files":[{"name":"string","content":"string"}],"collection":"string","collectionId":"string","replaceCollection":true,"onExisting":"fail","prune":"space","dryRun":true}'
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(req)
end
puts res.body
Java
import java.net.URI;
import java.net.http.*;
import java.net.http.HttpRequest.BodyPublishers;

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/docs/import"))
    .header("forumulate-version", "<value>")
    .header("Idempotency-Key", "<value>")
    .header("Content-Type", "application/json")
    .method("POST", BodyPublishers.ofString("{\"files\":[{\"name\":\"string\",\"content\":\"string\"}],\"collection\":\"string\",\"collectionId\":\"string\",\"replaceCollection\":true,\"onExisting\":\"fail\",\"prune\":\"space\",\"dryRun\":true}"))
    .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Responses
200 Import counters. files is how many files the request carried — the denominator. created (new pages), replaced (existing pages rewritten in place), skipped (left untouched) and failed (could not be turned into a page) are DISJOINT and satisfy created + replaced + skipped + failed === files. imported is NOT "files processed": it is a convenience total meaning created + replaced — the pages this run actually wrote — so it EXCLUDES skipped and failed, and it OVERLAPS replaced. That overlap is why three files all rewritten answer files: 3, created: 0, replaced: 3, imported: 3: three pages, not six. Sum the disjoint four; never add imported to replaced. (An earlier version of this page documented imported + replaced + skipped + failed === files.length; that invariant is withdrawn — it stopped being true once a replace began counting as an import.) collisions/prunedSlugs/plan/dryRun appear when the request asks for them. redirects and retired appear when a file DECLARED an address change: see the request body for slug: and redirect_from:. redirects lists the rules the run wrote into the space's redirect table; retired lists the slugs it moved to the trash because a rule took over their address (reversible with POST …/{pageId}/restore). On a dry run both say what WOULD happen and nothing is written.
Example
{
  "brokenLinks": [
    {
      "pageSlug": "string",
      "url": "string"
    }
  ],
  "collection": {
    "created": true,
    "exists": true,
    "id": "string",
    "name": "string"
  },
  "collisions": [
    "string"
  ],
  "created": 0,
  "dryRun": true,
  "failed": 0,
  "failures": [
    {
      "file": "string",
      "reason": "string"
    }
  ],
  "files": 0,
  "imported": 0,
  "plan": [
    {
      "action": "create",
      "collision": true,
      "slug": "string",
      "title": "string"
    }
  ],
  "pruned": 0,
  "prunedSlugs": [
    "string"
  ],
  "redirects": [
    {
      "from": "string",
      "to": "string"
    }
  ],
  "refused": [
    "string"
  ],
  "replaced": 0,
  "retired": [
    "string"
  ],
  "skipped": 0,
  "warnings": [
    "tags: \"Launch Day\" is stored as \"launch-day\""
  ]
}
Schema
brokenLinksobject[]
Show child parameters
pageSlugstringrequired
urlstringrequired
collectionobject
Show child parameters
createdbooleanrequired
existsbooleanrequired
idstring
namestringrequired
collisionsstring[]
creatednumberrequired
dryRunboolean
failednumberrequired
failuresobject[]
Show child parameters
filestringrequired
reasonstringrequired
filesnumberrequired
importednumberrequired
planPlanEntry[]
Show child parameters
actionstringrequired
create | refuse | replace | skip
collisionbooleanrequired
slugstringrequired
titlestringrequired
prunednumber
prunedSlugsstring[]
redirectsobject[]
Show child parameters
fromstringrequired
tostringrequired
refusedstring[]
replacednumberrequired
retiredstring[]
skippednumberrequired
warningsstring[]
Values we ACCEPTED and then rewrote, one sentence each — a slugified tag, a normalised colour, a clamped range, a slug that was taken. Absent when nothing was rewritten, never []. The same sentences are on the X-Field-Warnings response header. If this is present, something you sent is not what is stored; read the resource back rather than assuming your own value.
400 The request could not be understood — a malformed header, path or query value (request.malformed). A value that was understood but is not acceptable is a 422 validation.failed.
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
401 No credential, or one that is expired or revoked (auth.required). Send a Bearer token or the session cookie.
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
403 Authenticated, and not allowed — always rbac.forbidden, whether what is missing is the capability, the token scope, or a resource the token is pinned away from. The message names the capability and, for a scope refusal, the scopes that would grant it. A refusal because of the PLAN is a 402 with quota.exceeded, not this.
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
404 No such thing — one of the ids in the path does not resolve, or resolves to something this caller may not see (*.not_found).
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
413 The archive or repository is over the import size limit. The message carries the limit; split the import or upload a ZIP of the subtree you want. A 422 here means the bytes could not be read as an archive at all, which is a different problem.
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
422 The body was understood and a value is unacceptable (validation.failed). error.details names the field paths.
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
429 Rate limited — see the RateLimit-* headers.
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
default Error — the stable { error: { code, message } } envelope.
Example
{
  "error": {
    "code": "string",
    "message": "string",
    "details": {},
    "docs": "string",
    "reason": "string",
    "slugs": [
      "string"
    ],
    "conflict": {
      "etag": "string",
      "ops": [
        null
      ]
    }
  },
  "requestId": "string"
}
Schema
errorobjectrequired
Show child parameters
codestringrequired
Stable, machine-readable error code, e.g. rbac.forbidden.
messagestringrequired
Human-readable explanation.
detailsobject
Field-level validation problems, keyed by field path (422 only).
docsstring
Where the vocabulary or format this error is about is published — an absolute URL, or an API route such as GET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.
reasonstring
Why a credential was refused, as one stable word, where one status has several causes — e.g. an external_jwt redeem answers iat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.
slugsstring[]
The exact slugs that collided, on a slug-conflict 409 — space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.
conflictobject
On a 409 from a content entry field write (PATCH …/entries/{entryId}/fields) whose If-Match was stale.
Show child parameters
etagstringrequired
The entry's current fields etag.
opsarrayrequired
The field operations since your If-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.
requestIdstring
The id of this request — the same value as the x-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.
The Forumulate teamUpdated