Get the setup in an organization
GET
https://forumulate.com/api /orgs/{orgId}/sites/{siteId}/domains/{domainId}/setup What to configure for a site hostname and whether traffic reaches us yet — DNS for a whole host, the one proxy rule for a path, and whether its www twin is covered. The same answer a space domain gets; read it before and after activating.
Requires token scope: read (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
orgIdstringpathrequiredsiteIdstringpathrequireddomainIdstringpathrequiredforumulate-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:Code samples
cURL
curl -X GET "https://forumulate.com/api/orgs/{orgId}/sites/{siteId}/domains/{domainId}/setup" \
-H "forumulate-version: <value>"TypeScript
const res = await fetch('https://forumulate.com/api/orgs/{orgId}/sites/{siteId}/domains/{domainId}/setup', {
method: 'GET',
headers: {
'forumulate-version': '<value>',
},
});
const data = await res.json();Python
import requests
res = requests.request(
"GET",
"https://forumulate.com/api/orgs/{orgId}/sites/{siteId}/domains/{domainId}/setup",
headers={
"forumulate-version": "<value>",
},
)
data = res.json()Go
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
req, _ := http.NewRequest("GET", "https://forumulate.com/api/orgs/{orgId}/sites/{siteId}/domains/{domainId}/setup", nil)
req.Header.Set("forumulate-version", "<value>")
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}/sites/{siteId}/domains/{domainId}/setup');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['forumulate-version: <value>']);
$response = curl_exec($ch);
curl_close($ch);
echo $response;Ruby
require 'net/http'
require 'uri'
uri = URI('https://forumulate.com/api/orgs/{orgId}/sites/{siteId}/domains/{domainId}/setup')
req = Net::HTTP::Get.new(uri)
req['forumulate-version'] = '<value>'
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}/sites/{siteId}/domains/{domainId}/setup"))
.header("forumulate-version", "<value>")
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());Responses
200 What to configure for this site hostname (records, or the one proxy rule for a path) and whether traffic is reaching us yet — the same answer as a space domain’s setup.
Example
{
"mode": "dns",
"served": true,
"crawlerBlocked": true,
"message": "string",
"dns": {
"found": [
"string"
],
"expected": "string",
"reachesUs": true,
"cnameTarget": "string"
},
"wwwNotCovered": true,
"www": {
"hostname": "string",
"resolves": true,
"covered": true,
"how": "redirect-by-us",
"detail": "string"
},
"readerEndpoints": {
"url": "string",
"kind": "platform",
"primary": true,
"apiBase": "string",
"jwtRedeemUrl": "string",
"signOutUrl": "string"
},
"forward": {
"path": "string",
"origin": "string",
"snippets": {
"nginx": "string",
"caddy": "string",
"cloudflare": "string"
}
}
}Schema
modestringrequiredWhich configuration step this domain is on: 'verified' = already reaching our edge (nothing to do); 'dns' = a whole host that must point at us; 'pending' = we are not answering this address yet, either because it is verified but NOT ACTIVATED (your own application still serves it — call
POST {domainId}/activate when ready) or because it was activated moments ago and routing is still settling; 'path' = an ACTIVATED subfolder that still does not reach us and needs one proxy rule; 'unverified' = a subfolder not verified yet. Only 'path' means "add a proxy rule" — read message for which case pending is.servedbooleanrequiredTrue when traffic already reaches our edge and the domain is serving now.
crawlerBlockedbooleanTrue when a CDN in front of the domain blocks crawler/AI User-Agents, so the (public) docs are invisible to search engines and AI tools until the owner allows bot traffic.
messagestringrequiredA one-line, human-readable summary of what to do next (or that nothing is needed).
dnsobjectShow child parameters
foundstring[]requiredThe A records the hostname currently resolves to.
expectedstringAn edge address we accept, when the deployment pins one.
reachesUsbooleanrequiredTrue once the hostname resolves to (or CNAMEs to) our edge.
cnameTargetstringThe CNAME target the hostname points at, when it is a CNAME.
wwwNotCoveredbooleanTrue when
www.<hostname> resolves in DNS but does NOT reach this address — readers who type www land on whatever else answers that name. Fix it with alsoRedirectWww, or a redirect in your own app. Absent for a hostname that already starts with www..wwwSetupWwwDtoWhat we found at the
www. twin of this hostname, and how it is covered.Show child parameters
hostnamestringrequiredThe twin we looked at:
www.<hostname>.resolvesbooleanrequiredThe twin resolves in DNS at all.
coveredbooleanrequiredReaders who type the twin end up at this address.
howstringrequiredHow it is covered: 'redirect-by-us' (alsoRedirectWww is on), 'registered' (the twin is a live address of its own here), 'redirects-here' (something — usually your app — already redirects it to this address), 'served-by-us' (it already reaches our edge); or why not: 'not-resolving' (no DNS record, nothing to cover), 'not-covered' (it resolves and something else answers).
detailstringrequiredOne sentence a person can act on.
readerEndpointsSetupReaderEndpointsDtoThe same-origin reader-session endpoints at this address (space domains only — a site domain's are per space, on
GET …/spaces/{spaceId}/reader-auth → readerEndpoints).Show child parameters
urlstringrequiredThe reader root at this address.
kindstringrequiredprimarybooleanrequiredapiBasestringrequiredThe same-origin API base here:
<mount>/_api.jwtRedeemUrlstringrequiredRedeem an external_jwt here (
POST {jwt, returnTo} or GET ?jwt=…&returnTo=…) so the reader cookies are set on THIS host. Redeeming on forumulate.com sets them there, where your docs never see them.signOutUrlstringrequiredPOST here to revoke the reader session and clear every reader cookie on this host.forwardSetupForwardDtoThe single reverse-proxy rule to add, when a path-based domain does not yet reach us.
Show child parameters
pathstringrequiredThe path prefix to forward to us.
originstringrequiredThe edge origin the proxy must target (origin.forumulate.com), NOT the marketing apex.
snippetsobjectrequiredShow child parameters
nginxstringrequirednginx location block.
caddystringrequiredCaddy reverse_proxy block.
cloudflarestringrequiredCloudflare Origin Rule / Worker guidance.
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.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