Replace the library's content model
PUT
https://forumulate.com/api /orgs/{orgId}/content/libraries/{libraryId}/model Saves the library-wide shape and the content types, and records a new model version with the declared ops. A renameField/renameType/renameBlockType moves stored values to the new name; a removeField hides them for 30 days (undo with restoreField). Values are rewritten behind the response: GET …/model/versions shows migratedAt once done.
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
orgIdstringpathrequiredlibraryIdstringpathrequiredforumulate-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
shapeobjectThe library-wide shape every content type inherits. Omitted: unchanged.
Show child parameters
spaceobjectShow child parameters
indexstringorderstringnavstringwhoCreatesstringsummarystring[]sectionsobjectShow child parameters
addobject[]omitstring[]orderstring[]setobjectunsetobjectparamsobjectShow child parameters
addobject[]omitstring[]orderstring[]setobjectunsetobjectblockTypesobjectShow child parameters
addobject[]omitstring[]orderstring[]setobjectunsetobjecttypesobjectEvery content type, by key. REPLACES the set; omitted: unchanged.
opsobject[]What changed, where a diff cannot tell: a rename is not a removal plus an addition. Checked against both models.
{
"shape": {
"space": {
"index": "landing",
"order": "manual",
"nav": "tree",
"whoCreates": "staff",
"summary": [
"string"
]
},
"sections": {
"add": [
{
"key": null,
"label": null,
"kind": null,
"required": null,
"hint": null,
"example": null,
"schemaProperty": null,
"render": null
}
],
"omit": [
"string"
],
"order": [
"string"
],
"set": {},
"unset": {}
},
"params": {
"add": [
{
"key": null,
"label": null,
"type": null,
"options": null,
"relationSpaceId": null,
"hint": null,
"placeholder": null,
"internal": null,
"facet": null,
"meta": null,
"required": null,
"default": null,
"schemaProperty": null,
"min": null,
"max": null,
"visibility": null,
"localized": null,
"unique": null,
"indexed": null,
"sortable": null,
"minLength": null,
"maxLength": null,
"pattern": null,
"condition": null,
"expr": null,
"schema": null,
"allowedBlocks": null,
"allowed": null,
"allowedGroups": null,
"to": null,
"hasMany": null,
"rowLabelField": null,
"language": null,
"from": null,
"accept": null,
"admin": null,
"fields": null
}
],
"omit": [
"string"
],
"order": [
"string"
],
"set": {},
"unset": {}
},
"blockTypes": {
"add": [
{
"key": null,
"label": null,
"description": null,
"icon": null,
"group": null,
"fields": null,
"variants": null
}
],
"omit": [
"string"
],
"order": [
"string"
],
"set": {},
"unset": {}
}
},
"types": {},
"ops": [
{
"op": "string",
"type": "string",
"blockType": "string",
"variant": "string",
"from": "string",
"to": "string"
}
]
}Code samples
cURL
curl -X PUT "https://forumulate.com/api/orgs/{orgId}/content/libraries/{libraryId}/model" \
-H "forumulate-version: <value>" \
-H "Idempotency-Key: <value>" \
-H "Content-Type: application/json" \
-d '{"shape":{"space":{"index":"landing","order":"manual","nav":"tree","whoCreates":"staff","summary":["string"]},"sections":{"add":[{"key":null,"label":null,"kind":null,"required":null,"hint":null,"example":null,"schemaProperty":null,"render":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}},"params":{"add":[{"key":null,"label":null,"type":null,"options":null,"relationSpaceId":null,"hint":null,"placeholder":null,"internal":null,"facet":null,"meta":null,"required":null,"default":null,"schemaProperty":null,"min":null,"max":null,"visibility":null,"localized":null,"unique":null,"indexed":null,"sortable":null,"minLength":null,"maxLength":null,"pattern":null,"condition":null,"expr":null,"schema":null,"allowedBlocks":null,"allowed":null,"allowedGroups":null,"to":null,"hasMany":null,"rowLabelField":null,"language":null,"from":null,"accept":null,"admin":null,"fields":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}},"blockTypes":{"add":[{"key":null,"label":null,"description":null,"icon":null,"group":null,"fields":null,"variants":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}}},"types":{},"ops":[{"op":"string","type":"string","blockType":"string","variant":"string","from":"string","to":"string"}]}'TypeScript
const res = await fetch('https://forumulate.com/api/orgs/{orgId}/content/libraries/{libraryId}/model', {
method: 'PUT',
headers: {
'forumulate-version': '<value>',
'Idempotency-Key': '<value>',
'Content-Type': 'application/json',
},
body: JSON.stringify({"shape":{"space":{"index":"landing","order":"manual","nav":"tree","whoCreates":"staff","summary":["string"]},"sections":{"add":[{"key":null,"label":null,"kind":null,"required":null,"hint":null,"example":null,"schemaProperty":null,"render":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}},"params":{"add":[{"key":null,"label":null,"type":null,"options":null,"relationSpaceId":null,"hint":null,"placeholder":null,"internal":null,"facet":null,"meta":null,"required":null,"default":null,"schemaProperty":null,"min":null,"max":null,"visibility":null,"localized":null,"unique":null,"indexed":null,"sortable":null,"minLength":null,"maxLength":null,"pattern":null,"condition":null,"expr":null,"schema":null,"allowedBlocks":null,"allowed":null,"allowedGroups":null,"to":null,"hasMany":null,"rowLabelField":null,"language":null,"from":null,"accept":null,"admin":null,"fields":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}},"blockTypes":{"add":[{"key":null,"label":null,"description":null,"icon":null,"group":null,"fields":null,"variants":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}}},"types":{},"ops":[{"op":"string","type":"string","blockType":"string","variant":"string","from":"string","to":"string"}]}),
});
const data = await res.json();Python
import requests
res = requests.request(
"PUT",
"https://forumulate.com/api/orgs/{orgId}/content/libraries/{libraryId}/model",
headers={
"forumulate-version": "<value>",
"Idempotency-Key": "<value>",
"Content-Type": "application/json",
},
json={"shape": {"space": {"index": "landing", "order": "manual", "nav": "tree", "whoCreates": "staff", "summary": ["string"]}, "sections": {"add": [{"key": None, "label": None, "kind": None, "required": None, "hint": None, "example": None, "schemaProperty": None, "render": None}], "omit": ["string"], "order": ["string"], "set": {}, "unset": {}}, "params": {"add": [{"key": None, "label": None, "type": None, "options": None, "relationSpaceId": None, "hint": None, "placeholder": None, "internal": None, "facet": None, "meta": None, "required": None, "default": None, "schemaProperty": None, "min": None, "max": None, "visibility": None, "localized": None, "unique": None, "indexed": None, "sortable": None, "minLength": None, "maxLength": None, "pattern": None, "condition": None, "expr": None, "schema": None, "allowedBlocks": None, "allowed": None, "allowedGroups": None, "to": None, "hasMany": None, "rowLabelField": None, "language": None, "from": None, "accept": None, "admin": None, "fields": None}], "omit": ["string"], "order": ["string"], "set": {}, "unset": {}}, "blockTypes": {"add": [{"key": None, "label": None, "description": None, "icon": None, "group": None, "fields": None, "variants": None}], "omit": ["string"], "order": ["string"], "set": {}, "unset": {}}}, "types": {}, "ops": [{"op": "string", "type": "string", "blockType": "string", "variant": "string", "from": "string", "to": "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}/content/libraries/{libraryId}/model", strings.NewReader(`{"shape":{"space":{"index":"landing","order":"manual","nav":"tree","whoCreates":"staff","summary":["string"]},"sections":{"add":[{"key":null,"label":null,"kind":null,"required":null,"hint":null,"example":null,"schemaProperty":null,"render":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}},"params":{"add":[{"key":null,"label":null,"type":null,"options":null,"relationSpaceId":null,"hint":null,"placeholder":null,"internal":null,"facet":null,"meta":null,"required":null,"default":null,"schemaProperty":null,"min":null,"max":null,"visibility":null,"localized":null,"unique":null,"indexed":null,"sortable":null,"minLength":null,"maxLength":null,"pattern":null,"condition":null,"expr":null,"schema":null,"allowedBlocks":null,"allowed":null,"allowedGroups":null,"to":null,"hasMany":null,"rowLabelField":null,"language":null,"from":null,"accept":null,"admin":null,"fields":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}},"blockTypes":{"add":[{"key":null,"label":null,"description":null,"icon":null,"group":null,"fields":null,"variants":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}}},"types":{},"ops":[{"op":"string","type":"string","blockType":"string","variant":"string","from":"string","to":"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}/content/libraries/{libraryId}/model');
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, '{"shape":{"space":{"index":"landing","order":"manual","nav":"tree","whoCreates":"staff","summary":["string"]},"sections":{"add":[{"key":null,"label":null,"kind":null,"required":null,"hint":null,"example":null,"schemaProperty":null,"render":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}},"params":{"add":[{"key":null,"label":null,"type":null,"options":null,"relationSpaceId":null,"hint":null,"placeholder":null,"internal":null,"facet":null,"meta":null,"required":null,"default":null,"schemaProperty":null,"min":null,"max":null,"visibility":null,"localized":null,"unique":null,"indexed":null,"sortable":null,"minLength":null,"maxLength":null,"pattern":null,"condition":null,"expr":null,"schema":null,"allowedBlocks":null,"allowed":null,"allowedGroups":null,"to":null,"hasMany":null,"rowLabelField":null,"language":null,"from":null,"accept":null,"admin":null,"fields":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}},"blockTypes":{"add":[{"key":null,"label":null,"description":null,"icon":null,"group":null,"fields":null,"variants":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}}},"types":{},"ops":[{"op":"string","type":"string","blockType":"string","variant":"string","from":"string","to":"string"}]}');
$response = curl_exec($ch);
curl_close($ch);
echo $response;Ruby
require 'net/http'
require 'uri'
uri = URI('https://forumulate.com/api/orgs/{orgId}/content/libraries/{libraryId}/model')
req = Net::HTTP::Put.new(uri)
req['forumulate-version'] = '<value>'
req['Idempotency-Key'] = '<value>'
req['Content-Type'] = 'application/json'
req.body = '{"shape":{"space":{"index":"landing","order":"manual","nav":"tree","whoCreates":"staff","summary":["string"]},"sections":{"add":[{"key":null,"label":null,"kind":null,"required":null,"hint":null,"example":null,"schemaProperty":null,"render":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}},"params":{"add":[{"key":null,"label":null,"type":null,"options":null,"relationSpaceId":null,"hint":null,"placeholder":null,"internal":null,"facet":null,"meta":null,"required":null,"default":null,"schemaProperty":null,"min":null,"max":null,"visibility":null,"localized":null,"unique":null,"indexed":null,"sortable":null,"minLength":null,"maxLength":null,"pattern":null,"condition":null,"expr":null,"schema":null,"allowedBlocks":null,"allowed":null,"allowedGroups":null,"to":null,"hasMany":null,"rowLabelField":null,"language":null,"from":null,"accept":null,"admin":null,"fields":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}},"blockTypes":{"add":[{"key":null,"label":null,"description":null,"icon":null,"group":null,"fields":null,"variants":null}],"omit":["string"],"order":["string"],"set":{},"unset":{}}},"types":{},"ops":[{"op":"string","type":"string","blockType":"string","variant":"string","from":"string","to":"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}/content/libraries/{libraryId}/model"))
.header("forumulate-version", "<value>")
.header("Idempotency-Key", "<value>")
.header("Content-Type", "application/json")
.method("PUT", BodyPublishers.ofString("{\"shape\":{\"space\":{\"index\":\"landing\",\"order\":\"manual\",\"nav\":\"tree\",\"whoCreates\":\"staff\",\"summary\":[\"string\"]},\"sections\":{\"add\":[{\"key\":null,\"label\":null,\"kind\":null,\"required\":null,\"hint\":null,\"example\":null,\"schemaProperty\":null,\"render\":null}],\"omit\":[\"string\"],\"order\":[\"string\"],\"set\":{},\"unset\":{}},\"params\":{\"add\":[{\"key\":null,\"label\":null,\"type\":null,\"options\":null,\"relationSpaceId\":null,\"hint\":null,\"placeholder\":null,\"internal\":null,\"facet\":null,\"meta\":null,\"required\":null,\"default\":null,\"schemaProperty\":null,\"min\":null,\"max\":null,\"visibility\":null,\"localized\":null,\"unique\":null,\"indexed\":null,\"sortable\":null,\"minLength\":null,\"maxLength\":null,\"pattern\":null,\"condition\":null,\"expr\":null,\"schema\":null,\"allowedBlocks\":null,\"allowed\":null,\"allowedGroups\":null,\"to\":null,\"hasMany\":null,\"rowLabelField\":null,\"language\":null,\"from\":null,\"accept\":null,\"admin\":null,\"fields\":null}],\"omit\":[\"string\"],\"order\":[\"string\"],\"set\":{},\"unset\":{}},\"blockTypes\":{\"add\":[{\"key\":null,\"label\":null,\"description\":null,\"icon\":null,\"group\":null,\"fields\":null,\"variants\":null}],\"omit\":[\"string\"],\"order\":[\"string\"],\"set\":{},\"unset\":{}}},\"types\":{},\"ops\":[{\"op\":\"string\",\"type\":\"string\",\"blockType\":\"string\",\"variant\":\"string\",\"from\":\"string\",\"to\":\"string\"}]}"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());Responses
200 Success.
Example
{
"model": {
"shape": {},
"types": {},
"version": 0
},
"warnings": [
"tags: \"Launch Day\" is stored as \"launch-day\""
]
}Schema
modelobjectrequiredShow child parameters
shapeobjectrequiredtypesobjectrequiredversionnumberrequiredwarningsstring[]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