Create a webhook in an organization

Embed
Preview of the card
<a href="https://forumulate.com/reference/forumulate-api-webhook-create"><img src="https://forumulate.com/reference/_api/public/orgs/046a9bc0-d530-43ae-9bae-af7945008cda/spaces/8042f004-e3d9-445f-b7a4-b6c1f86836e6/pages/d4c28dec-0514-4387-aa63-294a4be4d4a2/social-card.svg?v=3e7ae33eec8b" alt="Create a webhook in an organization" width="600" height="315"></a>
POSThttps://forumulate.com/api/orgs/{orgId}/webhooks

Registers an HTTPS endpoint for a set of events and returns the signing secret once. The alternative to polling: subscribe to page.published or item.updated and keep a downstream index fresh as writes happen.

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
orgIdstringpathrequired
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
urlstringrequired
Where deliveries are POSTed. Must be https outside development, and is called with a signature header your receiver verifies — see the webhooks guide for the scheme.
eventsstring[]required
The events this endpoint should hear, by name. At least one; GET /webhooks/events publishes the whole vocabulary with a sample payload for each.
spaceIdstring
Confine the endpoint to one space. Omitted or null means organization-wide, including spaces created after this endpoint. For a content library use libraryId.
libraryIdstring
Confine the endpoint to one content library: content.* events from that library only. Not together with spaceId. Omitted leaves the scope as it is; null means organization-wide.
formatstring
The body shape. json (the default) sends our own event payload; slack and discord send a message that platform renders natively, so an incoming-webhook URL from either can be pasted in with nothing in between.
json | slack | discord
categoryIdsstring[]
Narrow delivery to events whose payload names one of these forum categories. Omitted or null means no filter — the endpoint hears the event wherever it happens.
tagIdsstring[]
Narrow delivery to events whose payload carries one of these tags. Omitted or null means no filter. Tags are matched by name, not by id.
groupIdsstring[]
Narrow group, user and badge events to these groups. Omitted or null means no filter; it has no effect on events that name no group.
contentTypesstring[]
Narrow content.* events to entries of these content types (by key). Omitted or null means no filter. A model change to the whole model is always delivered.
localesstring[]
Narrow content.* events to entries in these locales (BCP-47 codes, or default for a space's source language). Omitted or null means no filter.
bodyEncodingstring
How the delivery body is encoded. json (the default) POSTs application/json; form POSTs application/x-www-form-urlencoded, for a receiver that cannot parse JSON bodies.
json | form
signatureVersionstring
Which signature headers each delivery carries. v1 is the timestamped scheme alone and is the default for a NEW endpoint; both adds the legacy body-only sha256=, which is replayable forever and exists only for receivers not yet migrated.
both | v1
kindstring
webhook (the default) delivers signed events. deploy_hook POSTs a Vercel, Netlify or Cloudflare Pages deploy-hook URL to rebuild a static site: unsigned (the platforms take no signature), on content.* events only, and debounced to one build 30 s after the last change in a content library. Fixed at create.
webhook | deploy_hook
{
  "url": "string",
  "events": [
    "page.created"
  ],
  "spaceId": "string",
  "libraryId": "string",
  "format": "json",
  "categoryIds": [
    "string"
  ],
  "tagIds": [
    "string"
  ],
  "groupIds": [
    "string"
  ],
  "contentTypes": [
    "string"
  ],
  "locales": [
    "string"
  ],
  "bodyEncoding": "json",
  "signatureVersion": "both",
  "kind": "webhook"
}
Code samples
cURL
curl -X POST "https://forumulate.com/api/orgs/{orgId}/webhooks" \
  -H "forumulate-version: <value>" \
  -H "Idempotency-Key: <value>" \
  -H "Content-Type: application/json" \
  -d '{"url":"string","events":["page.created"],"spaceId":"string","libraryId":"string","format":"json","categoryIds":["string"],"tagIds":["string"],"groupIds":["string"],"contentTypes":["string"],"locales":["string"],"bodyEncoding":"json","signatureVersion":"both","kind":"webhook"}'
TypeScript
const res = await fetch('https://forumulate.com/api/orgs/{orgId}/webhooks', {
  method: 'POST',
  headers: {
    'forumulate-version': '<value>',
    'Idempotency-Key': '<value>',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({"url":"string","events":["page.created"],"spaceId":"string","libraryId":"string","format":"json","categoryIds":["string"],"tagIds":["string"],"groupIds":["string"],"contentTypes":["string"],"locales":["string"],"bodyEncoding":"json","signatureVersion":"both","kind":"webhook"}),
});
const data = await res.json();
Python
import requests

res = requests.request(
    "POST",
    "https://forumulate.com/api/orgs/{orgId}/webhooks",
    headers={
    "forumulate-version": "<value>",
    "Idempotency-Key": "<value>",
    "Content-Type": "application/json",
    },
    json={"url": "string", "events": ["page.created"], "spaceId": "string", "libraryId": "string", "format": "json", "categoryIds": ["string"], "tagIds": ["string"], "groupIds": ["string"], "contentTypes": ["string"], "locales": ["string"], "bodyEncoding": "json", "signatureVersion": "both", "kind": "webhook"},
)
data = res.json()
Go
package main

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

func main() {
	req, _ := http.NewRequest("POST", "https://forumulate.com/api/orgs/{orgId}/webhooks", strings.NewReader(`{"url":"string","events":["page.created"],"spaceId":"string","libraryId":"string","format":"json","categoryIds":["string"],"tagIds":["string"],"groupIds":["string"],"contentTypes":["string"],"locales":["string"],"bodyEncoding":"json","signatureVersion":"both","kind":"webhook"}`))
	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}/webhooks');
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, '{"url":"string","events":["page.created"],"spaceId":"string","libraryId":"string","format":"json","categoryIds":["string"],"tagIds":["string"],"groupIds":["string"],"contentTypes":["string"],"locales":["string"],"bodyEncoding":"json","signatureVersion":"both","kind":"webhook"}');
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Ruby
require 'net/http'
require 'uri'

uri = URI('https://forumulate.com/api/orgs/{orgId}/webhooks')
req = Net::HTTP::Post.new(uri)
req['forumulate-version'] = '<value>'
req['Idempotency-Key'] = '<value>'
req['Content-Type'] = 'application/json'
req.body = '{"url":"string","events":["page.created"],"spaceId":"string","libraryId":"string","format":"json","categoryIds":["string"],"tagIds":["string"],"groupIds":["string"],"contentTypes":["string"],"locales":["string"],"bodyEncoding":"json","signatureVersion":"both","kind":"webhook"}'
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}/webhooks"))
    .header("forumulate-version", "<value>")
    .header("Idempotency-Key", "<value>")
    .header("Content-Type", "application/json")
    .method("POST", BodyPublishers.ofString("{\"url\":\"string\",\"events\":[\"page.created\"],\"spaceId\":\"string\",\"libraryId\":\"string\",\"format\":\"json\",\"categoryIds\":[\"string\"],\"tagIds\":[\"string\"],\"groupIds\":[\"string\"],\"contentTypes\":[\"string\"],\"locales\":[\"string\"],\"bodyEncoding\":\"json\",\"signatureVersion\":\"both\",\"kind\":\"webhook\"}"))
    .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
Responses
201 Created.
Example
{
  "secret": "string",
  "webhook": {
    "active": true,
    "bodyEncoding": "form",
    "categoryIds": [
      "string"
    ],
    "contentTypes": [
      "string"
    ],
    "createdAt": "2024-01-01T00:00:00Z",
    "deploy": {
      "lastError": null,
      "lastStatusCode": null,
      "lastTriggeredAt": null,
      "provider": "cloudflare_pages"
    },
    "events": [
      "string"
    ],
    "format": "discord",
    "groupIds": [
      "string"
    ],
    "health": {
      "consecutiveFailures": 0,
      "disabledAt": "2024-01-01T00:00:00Z",
      "disabledReason": "string",
      "failingSince": "2024-01-01T00:00:00Z"
    },
    "id": "string",
    "kind": "deploy_hook",
    "libraryId": "string",
    "locales": [
      "string"
    ],
    "signatureVersion": "both",
    "spaceId": "string",
    "tagIds": [
      "string"
    ],
    "url": "string"
  },
  "data": {
    "active": true,
    "bodyEncoding": "form",
    "categoryIds": [
      "string"
    ],
    "contentTypes": [
      "string"
    ],
    "createdAt": "2024-01-01T00:00:00Z",
    "deploy": {
      "lastError": null,
      "lastStatusCode": null,
      "lastTriggeredAt": null,
      "provider": "cloudflare_pages"
    },
    "events": [
      "string"
    ],
    "format": "discord",
    "groupIds": [
      "string"
    ],
    "health": {
      "consecutiveFailures": 0,
      "disabledAt": "2024-01-01T00:00:00Z",
      "disabledReason": "string",
      "failingSince": "2024-01-01T00:00:00Z"
    },
    "id": "string",
    "kind": "deploy_hook",
    "libraryId": "string",
    "locales": [
      "string"
    ],
    "signatureVersion": "both",
    "spaceId": "string",
    "tagIds": [
      "string"
    ],
    "url": "string"
  },
  "warnings": [
    "tags: \"Launch Day\" is stored as \"launch-day\""
  ]
}
Schema
secretstringrequired
webhookobjectrequired
Show child parameters
activebooleanrequired
bodyEncodingstringrequired
form | json
categoryIdsarrayrequired
contentTypesarrayrequired
createdAtstringrequired
deployDeployHookStatusrequired
Show child parameters
lastErrorstringrequired
lastStatusCodenumberrequired
lastTriggeredAtstringrequired
providerstringrequired
cloudflare_pages | netlify | vercel
eventsstring[]required
formatstringrequired
discord | json | slack
groupIdsarrayrequired
healthobjectrequired
Show child parameters
consecutiveFailuresnumberrequired
disabledAtstringrequired
disabledReasonstringrequired
failingSincestringrequired
idstringrequired
kindstringrequired
deploy_hook | webhook
libraryIdstringrequired
localesarrayrequired
signatureVersionstringrequired
both | v1
spaceIdstringrequired
tagIdsarrayrequired
urlstringrequired
dataobject
The created resource again, under a key that is the same on every create — so a generic client needs one parser rather than one per endpoint. It is the SAME object as the key named after the resource (page, article, topic, …), which remains the canonical name.
Show child parameters
activebooleanrequired
bodyEncodingstringrequired
form | json
categoryIdsarrayrequired
contentTypesarrayrequired
createdAtstringrequired
deployDeployHookStatusrequired
Show child parameters
lastErrorstringrequired
lastStatusCodenumberrequired
lastTriggeredAtstringrequired
providerstringrequired
cloudflare_pages | netlify | vercel
eventsstring[]required
formatstringrequired
discord | json | slack
groupIdsarrayrequired
healthobjectrequired
Show child parameters
consecutiveFailuresnumberrequired
disabledAtstringrequired
disabledReasonstringrequired
failingSincestringrequired
idstringrequired
kindstringrequired
deploy_hook | webhook
libraryIdstringrequired
localesarrayrequired
signatureVersionstringrequired
both | v1
spaceIdstringrequired
tagIdsarrayrequired
urlstringrequired
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.
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