Replace the front page in a space
PUT
https://forumulate.com/api /orgs/{orgId}/spaces/{spaceId}/front-page Composes the space root as one block document — hero, live feed, category tiles, the type’s own bands, an optional sidebar — instead of the arrangement the type generates. The whole document is replaced; DELETE returns the space to its default layout.
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.
Parameters
orgIdstringpathrequiredspaceIdstringpathrequiredforumulate-versionstringheaderAPI 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-KeystringheaderMakes 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
docobjectrequiredShow child parameters
typestringrequiredcontentobject[]requiredsidebarobjectShow child parameters
positionstringrequiredwidthstringstartAfterintegergapnumber | stringdividerobjectShow child parameters
stylestringrequiredcolorstringwidthintegerphonestring | objectsourcestringdocobjectrequiredShow child parameters
typestringrequiredcontentobject[]requiredasideobjectShow child parameters
widthstringphonestringdocobjectrequiredShow child parameters
typestringrequiredcontentobject[]requiredcustomizationobjectShow child parameters
customCssstringCSS for THIS piece only, after the space theme so it wins. Sanitised and scoped to the reader exactly like the space
customCss (@import dropped, no way out of the <style>).themeobjectA partial theme for this piece — colours, palettes, fonts, typography, colorScheme, components, contentWidth, layout, motion, and (on a blog post)
post — layered over the space’s (which is over the site’s). On a post, release, event or legal document PATCH it MERGES key by key like the space theme’s PATCH, and null clears at any depth ({"paletteDark": null}); a page’s metadata replaces it whole. Unknown keys are refused by name. null clears all of it.Show child parameters
primaryColorstringaccentColorstringpaletteobjectpaletteDarkobjectfontstringcustomFontFamilystringheadingFontFamilystringbodyFontFamilystringmonoFontFamilystringcolorSchemestringcontentWidthstringlayoutobjectcomponentsobjectmotionobjectchangelogobjecttypographyobjectpostobjectshareImageUrlstringThe og:image / twitter:image for this piece, separate from its cover. Wins over the cover and over the generated social card.
ogTitlestringog:title / twitter:title.
ogDescriptionstringog:description / twitter:description.
twitterCardstringsummary (thumbnail) or summary_large_image (the default).jsonLdstringStructured data for this piece, as JSON (object or array of objects). Replaces the automatic markup on this piece (Article/BlogPosting, Event, DiscussionForumPosting/QAPage, the roadmap item’s Article).
null restores the automatic one.presetstringhideTitlebooleanwidthstring | integergapstringpaddingTopstring | number | objectpaddingBottomstring | numbermarkdownstringThe body as Markdown (CommonMark + tables/task lists), converted server-side. Alternative to
content. Blocks beyond plain Markdown (callouts, tabs, cards, steppers, columns, buttons, …) are written as :::name{attrs} directives — every directive, its attributes and how it closes: GET /docs/markdown-dialect. 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.bodystringA synonym for
markdown — the name a page write uses for the same thing.{
"doc": {
"type": "string",
"content": [
{}
]
},
"sidebar": {
"position": "none",
"width": "narrow",
"startAfter": 0,
"gap": 0,
"divider": {
"style": "solid",
"color": "string",
"width": 0
},
"phone": "after",
"source": "doc",
"doc": {
"type": "string",
"content": [
null
]
}
},
"aside": {
"width": "narrow",
"phone": "after",
"doc": {
"type": "string",
"content": [
null
]
}
},
"customization": {
"customCss": "string",
"theme": {
"primaryColor": null,
"accentColor": null,
"palette": null,
"paletteDark": null,
"font": null,
"customFontFamily": null,
"headingFontFamily": null,
"bodyFontFamily": null,
"monoFontFamily": null,
"colorScheme": null,
"contentWidth": null,
"layout": null,
"components": null,
"motion": null,
"changelog": null,
"typography": null,
"post": null
},
"shareImageUrl": null,
"ogTitle": "string",
"ogDescription": "string",
"twitterCard": "summary",
"jsonLd": "string"
},
"preset": "string",
"hideTitle": true,
"width": "list",
"gap": "none",
"paddingTop": "string",
"paddingBottom": "string",
"markdown": "string",
"body": "string"
}Code samples
cURL
curl -X PUT "https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/front-page" \
-H "forumulate-version: <value>" \
-H "Idempotency-Key: <value>" \
-H "Content-Type: application/json" \
-d '{"doc":{"type":"string","content":[{}]},"sidebar":{"position":"none","width":"narrow","startAfter":0,"gap":0,"divider":{"style":"solid","color":"string","width":0},"phone":"after","source":"doc","doc":{"type":"string","content":[null]}},"aside":{"width":"narrow","phone":"after","doc":{"type":"string","content":[null]}},"customization":{"customCss":"string","theme":{"primaryColor":null,"accentColor":null,"palette":null,"paletteDark":null,"font":null,"customFontFamily":null,"headingFontFamily":null,"bodyFontFamily":null,"monoFontFamily":null,"colorScheme":null,"contentWidth":null,"layout":null,"components":null,"motion":null,"changelog":null,"typography":null,"post":null},"shareImageUrl":null,"ogTitle":"string","ogDescription":"string","twitterCard":"summary","jsonLd":"string"},"preset":"string","hideTitle":true,"width":"list","gap":"none","paddingTop":"string","paddingBottom":"string","markdown":"string","body":"string"}'TypeScript
const res = await fetch('https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/front-page', {
method: 'PUT',
headers: {
'forumulate-version': '<value>',
'Idempotency-Key': '<value>',
'Content-Type': 'application/json',
},
body: JSON.stringify({"doc":{"type":"string","content":[{}]},"sidebar":{"position":"none","width":"narrow","startAfter":0,"gap":0,"divider":{"style":"solid","color":"string","width":0},"phone":"after","source":"doc","doc":{"type":"string","content":[null]}},"aside":{"width":"narrow","phone":"after","doc":{"type":"string","content":[null]}},"customization":{"customCss":"string","theme":{"primaryColor":null,"accentColor":null,"palette":null,"paletteDark":null,"font":null,"customFontFamily":null,"headingFontFamily":null,"bodyFontFamily":null,"monoFontFamily":null,"colorScheme":null,"contentWidth":null,"layout":null,"components":null,"motion":null,"changelog":null,"typography":null,"post":null},"shareImageUrl":null,"ogTitle":"string","ogDescription":"string","twitterCard":"summary","jsonLd":"string"},"preset":"string","hideTitle":true,"width":"list","gap":"none","paddingTop":"string","paddingBottom":"string","markdown":"string","body":"string"}),
});
const data = await res.json();Python
import requests
res = requests.request(
"PUT",
"https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/front-page",
headers={
"forumulate-version": "<value>",
"Idempotency-Key": "<value>",
"Content-Type": "application/json",
},
json={"doc": {"type": "string", "content": [{}]}, "sidebar": {"position": "none", "width": "narrow", "startAfter": 0, "gap": 0, "divider": {"style": "solid", "color": "string", "width": 0}, "phone": "after", "source": "doc", "doc": {"type": "string", "content": [None]}}, "aside": {"width": "narrow", "phone": "after", "doc": {"type": "string", "content": [None]}}, "customization": {"customCss": "string", "theme": {"primaryColor": None, "accentColor": None, "palette": None, "paletteDark": None, "font": None, "customFontFamily": None, "headingFontFamily": None, "bodyFontFamily": None, "monoFontFamily": None, "colorScheme": None, "contentWidth": None, "layout": None, "components": None, "motion": None, "changelog": None, "typography": None, "post": None}, "shareImageUrl": None, "ogTitle": "string", "ogDescription": "string", "twitterCard": "summary", "jsonLd": "string"}, "preset": "string", "hideTitle": True, "width": "list", "gap": "none", "paddingTop": "string", "paddingBottom": "string", "markdown": "string", "body": "string"},
)
data = res.json()Go
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
req, _ := http.NewRequest("PUT", "https://forumulate.com/api/orgs/{orgId}/spaces/{spaceId}/front-page", strings.NewReader(`{"doc":{"type":"string","content":[{}]},"sidebar":{"position":"none","width":"narrow","startAfter":0,"gap":0,"divider":{"style":"solid","color":"string","width":0},"phone":"after","source":"doc","doc":{"type":"string","content":[null]}},"aside":{"width":"narrow","phone":"after","doc":{"type":"string","content":[null]}},"customization":{"customCss":"string","theme":{"primaryColor":null,"accentColor":null,"palette":null,"paletteDark":null,"font":null,"customFontFamily":null,"headingFontFamily":null,"bodyFontFamily":null,"monoFontFamily":null,"colorScheme":null,"contentWidth":null,"layout":null,"components":null,"motion":null,"changelog":null,"typography":null,"post":null},"shareImageUrl":null,"ogTitle":"string","ogDescription":"string","twitterCard":"summary","jsonLd":"string"},"preset":"string","hideTitle":true,"width":"list","gap":"none","paddingTop":"string","paddingBottom":"string","markdown":"string","body":"string"}`))
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}/front-page');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
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, '{"doc":{"type":"string","content":[{}]},"sidebar":{"position":"none","width":"narrow","startAfter":0,"gap":0,"divider":{"style":"solid","color":"string","width":0},"phone":"after","source":"doc","doc":{"type":"string","content":[null]}},"aside":{"width":"narrow","phone":"after","doc":{"type":"string","content":[null]}},"customization":{"customCss":"string","theme":{"primaryColor":null,"accentColor":null,"palette":null,"paletteDark":null,"font":null,"customFontFamily":null,"headingFontFamily":null,"bodyFontFamily":null,"monoFontFamily":null,"colorScheme":null,"contentWidth":null,"layout":null,"components":null,"motion":null,"changelog":null,"typography":null,"post":null},"shareImageUrl":null,"ogTitle":"string","ogDescription":"string","twitterCard":"summary","jsonLd":"string"},"preset":"string","hideTitle":true,"width":"list","gap":"none","paddingTop":"string","paddingBottom":"string","markdown":"string","body":"string"}');
$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}/front-page')
req = Net::HTTP::Put.new(uri)
req['forumulate-version'] = '<value>'
req['Idempotency-Key'] = '<value>'
req['Content-Type'] = 'application/json'
req.body = '{"doc":{"type":"string","content":[{}]},"sidebar":{"position":"none","width":"narrow","startAfter":0,"gap":0,"divider":{"style":"solid","color":"string","width":0},"phone":"after","source":"doc","doc":{"type":"string","content":[null]}},"aside":{"width":"narrow","phone":"after","doc":{"type":"string","content":[null]}},"customization":{"customCss":"string","theme":{"primaryColor":null,"accentColor":null,"palette":null,"paletteDark":null,"font":null,"customFontFamily":null,"headingFontFamily":null,"bodyFontFamily":null,"monoFontFamily":null,"colorScheme":null,"contentWidth":null,"layout":null,"components":null,"motion":null,"changelog":null,"typography":null,"post":null},"shareImageUrl":null,"ogTitle":"string","ogDescription":"string","twitterCard":"summary","jsonLd":"string"},"preset":"string","hideTitle":true,"width":"list","gap":"none","paddingTop":"string","paddingBottom":"string","markdown":"string","body":"string"}'
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(req)
end
puts res.bodyJava
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}/front-page"))
.header("forumulate-version", "<value>")
.header("Idempotency-Key", "<value>")
.header("Content-Type", "application/json")
.method("PUT", BodyPublishers.ofString("{\"doc\":{\"type\":\"string\",\"content\":[{}]},\"sidebar\":{\"position\":\"none\",\"width\":\"narrow\",\"startAfter\":0,\"gap\":0,\"divider\":{\"style\":\"solid\",\"color\":\"string\",\"width\":0},\"phone\":\"after\",\"source\":\"doc\",\"doc\":{\"type\":\"string\",\"content\":[null]}},\"aside\":{\"width\":\"narrow\",\"phone\":\"after\",\"doc\":{\"type\":\"string\",\"content\":[null]}},\"customization\":{\"customCss\":\"string\",\"theme\":{\"primaryColor\":null,\"accentColor\":null,\"palette\":null,\"paletteDark\":null,\"font\":null,\"customFontFamily\":null,\"headingFontFamily\":null,\"bodyFontFamily\":null,\"monoFontFamily\":null,\"colorScheme\":null,\"contentWidth\":null,\"layout\":null,\"components\":null,\"motion\":null,\"changelog\":null,\"typography\":null,\"post\":null},\"shareImageUrl\":null,\"ogTitle\":\"string\",\"ogDescription\":\"string\",\"twitterCard\":\"summary\",\"jsonLd\":\"string\"},\"preset\":\"string\",\"hideTitle\":true,\"width\":\"list\",\"gap\":\"none\",\"paddingTop\":\"string\",\"paddingBottom\":\"string\",\"markdown\":\"string\",\"body\":\"string\"}"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());Responses
200 Success.
Example
{
"contrastWarnings": [
{
"code": "contrast.low",
"key": "palette.muted",
"message": "palette.muted (#9aa0a8) on palette.background (#f3f1ea) is 2.33:1 in light mode; text needs 4.5:1 (WCAG AA)."
}
],
"frontPage": {
"aside": {
"doc": {
"content": [
null
],
"type": "doc"
},
"phone": "after",
"width": "default"
},
"customization": {},
"doc": {
"content": [
{
"attrs": null,
"content": null,
"type": null
}
],
"type": "doc"
},
"gap": "lg",
"hideTitle": true,
"paddingBottom": "string",
"paddingTop": "string",
"preset": "string",
"sidebar": {
"divider": {
"color": "string",
"style": "dashed",
"width": 0
},
"doc": {
"content": [
null
],
"type": "doc"
},
"gap": "string",
"phone": "hide",
"position": "left",
"source": "doc",
"startAfter": 0,
"width": "default"
},
"width": 0
},
"warnings": [
"tags: \"Launch Day\" is stored as \"launch-day\""
]
}Schema
contrastWarningsThemeWarning[]Show child parameters
codestringrequiredkeystringrequiredmessagestringrequiredfrontPageobjectrequiredShow child parameters
asideobjectShow child parameters
docobjectrequiredphonestringwidthstringcustomizationobjectdocobjectrequiredShow child parameters
contentobject[]requiredtypestringrequiredgapstringhideTitlebooleanpaddingBottomstring | numberpaddingTopstring | number | IndexFrameStepspresetstringsidebarobjectShow child parameters
dividerobjectdocobjectrequiredgapstring | numberphonestring | objectpositionstringrequiredsourcestringstartAfternumberwidthstringwidthnumber | stringwarningsstring[]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
errorobjectrequiredShow child parameters
codestringrequiredStable, machine-readable error code, e.g.
rbac.forbidden.messagestringrequiredHuman-readable explanation.
detailsobjectField-level validation problems, keyed by field path (422 only).
docsstringWhere 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.reasonstringWhy 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.conflictobjectOn a 409 from a content entry field write (
PATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredThe entry's current fields etag.
opsarrayrequiredThe 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.requestIdstringThe 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
errorobjectrequiredShow child parameters
codestringrequiredStable, machine-readable error code, e.g.
rbac.forbidden.messagestringrequiredHuman-readable explanation.
detailsobjectField-level validation problems, keyed by field path (422 only).
docsstringWhere 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.reasonstringWhy 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.conflictobjectOn a 409 from a content entry field write (
PATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredThe entry's current fields etag.
opsarrayrequiredThe 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.requestIdstringThe 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
errorobjectrequiredShow child parameters
codestringrequiredStable, machine-readable error code, e.g.
rbac.forbidden.messagestringrequiredHuman-readable explanation.
detailsobjectField-level validation problems, keyed by field path (422 only).
docsstringWhere 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.reasonstringWhy 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.conflictobjectOn a 409 from a content entry field write (
PATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredThe entry's current fields etag.
opsarrayrequiredThe 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.requestIdstringThe 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
errorobjectrequiredShow child parameters
codestringrequiredStable, machine-readable error code, e.g.
rbac.forbidden.messagestringrequiredHuman-readable explanation.
detailsobjectField-level validation problems, keyed by field path (422 only).
docsstringWhere 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.reasonstringWhy 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.conflictobjectOn a 409 from a content entry field write (
PATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredThe entry's current fields etag.
opsarrayrequiredThe 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.requestIdstringThe 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
errorobjectrequiredShow child parameters
codestringrequiredStable, machine-readable error code, e.g.
rbac.forbidden.messagestringrequiredHuman-readable explanation.
detailsobjectField-level validation problems, keyed by field path (422 only).
docsstringWhere 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.reasonstringWhy 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.conflictobjectOn a 409 from a content entry field write (
PATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredThe entry's current fields etag.
opsarrayrequiredThe 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.requestIdstringThe 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
errorobjectrequiredShow child parameters
codestringrequiredStable, machine-readable error code, e.g.
rbac.forbidden.messagestringrequiredHuman-readable explanation.
detailsobjectField-level validation problems, keyed by field path (422 only).
docsstringWhere 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.reasonstringWhy 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.conflictobjectOn a 409 from a content entry field write (
PATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredThe entry's current fields etag.
opsarrayrequiredThe 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.requestIdstringThe 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
errorobjectrequiredShow child parameters
codestringrequiredStable, machine-readable error code, e.g.
rbac.forbidden.messagestringrequiredHuman-readable explanation.
detailsobjectField-level validation problems, keyed by field path (422 only).
docsstringWhere 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.reasonstringWhy 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.conflictobjectOn a 409 from a content entry field write (
PATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredThe entry's current fields etag.
opsarrayrequiredThe 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.requestIdstringThe 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