HTTP API
Errors & rate limits
What Sedgemark returns when something is wrong, how to tell the causes apart, and the request budget every public surface shares.
Two error shapes
Everything except form submission returns a single error string:
{ "error": "Not found" }
Form endpoints return an ok flag as well, because a 201 can also mean “silently discarded” and a validation failure
needs to name every field at once:
{
"ok": false,
"error": "Validation failed",
"errors": [
{ "field": "email", "message": "email must be a valid email address" }
]
}
Some errors carry an extra code key: token_invalid, token_expired, submitted_too_fast on form submission, and the plan codes below. When a code is present, branch on it rather than on the message text; messages are
written for people and may be reworded.
Status codes
| Status | Means | What to do |
|---|---|---|
| 200 | Success. | Nothing to do. |
| 201 | A form submission was accepted. | Check id: a null id means the honeypot was tripped and nothing was stored. |
| 302 | A media id resolved to a signed file URL. | Follow it. Every HTTP client does this by default. |
| 400 | Malformed request: an invalid media id, unparseable JSON, or form validation failure. | Fix the request. Retrying it unchanged will not help. |
| 401 | A private collection or the MCP endpoint was reached with no valid API key. | Send Authorization: Bearer with a valid key. See Authentication. |
| 402 | The workspace’s plan does not cover this action. | Only on authenticated dashboard surfaces. See below. |
| 403 | A request to a form endpoint carried an Origin outside that form’s allowlist (the descriptor GET as well as the submit POST). | Add the origin in the dashboard, or call it server-side with no Origin header. See Forms → CORS. |
| 404 | No such collection, entry, media asset, or form; or no such workspace. | Read the body to tell those apart. See below. |
| 413 | A form submission body exceeded 64 KB. | Send less. The cap is enforced while reading, before parsing. |
| 429 | Over the shared per-IP rate limit. | Back off and retry; it is the only status worth retrying automatically. |
| 500 | Something failed inside Sedgemark. | Retry once; if it persists the detail is in the server logs, deliberately not in the response. |
Telling 404s apart
A 404 means one of several things, and the body is what distinguishes them.
| Body | Means |
|---|---|
| { "error": "Unknown tenant" } | The hostname names no workspace. Your path is fine: check the subdomain for a typo, and check that the workspace still exists. |
| { "error": "Not found" } | The host resolved, but the collection, entry or media asset does not exist, or the entry exists as a draft, which this API will not serve. |
| { "ok": false, "error": "Not found" } | A form endpoint. Deliberately ambiguous: unknown slug, inactive form, plan-locked form, and forms disabled for the workspace are all identical here. |
If Sedgemark cannot tell which workspace a request was for at all (as opposed to being
told about one that does not exist), that is a server misconfiguration and it is
reported as a 500. Reporting it as “that workspace does not
exist” would send you looking in exactly the wrong place.
Plan-related responses
Every workspace is on a plan, which caps how many of certain resources it may have: collections, forms and user accounts. The current plan, its caps and your usage against them are all shown on the billing screen in the dashboard; they are not readable over the API and are deliberately not printed here, because they are set per-workspace and would go stale on a static page.
Two codes appear on authenticated dashboard surfaces when the plan is the reason something
failed. Both are 402, never 403, because a 403 would be
indistinguishable from a feature being switched off, which paying cannot resolve.
| Code | Means |
|---|---|
| plan_limit_reached | You are at the cap for this resource and cannot add another one. |
| plan_resource_locked | The resource exists and is intact, but the current plan no longer covers it. Nothing has been deleted; upgrading restores access immediately. |
/api/v1 is unauthenticated, so it never reports plan state. A locked
collection or form returns exactly the same 404 as one that never
existed. If a public endpoint stops working and nothing about your code changed, check
the dashboard: it will say plainly what is locked and why.
Identity codes
Identity is a per-workspace beta. These appear on the public /api/v1/accounts/* endpoints and on the dashboard account-management
surfaces:
| Code | Meaning |
|---|---|
| email_taken | 409: signup, or a staff-created account: that address already has one. |
| user_disabled | 403: login only, and only after the password verified. Naming it before that would let a caller probe for which addresses exist. |
| identity_disabled | 403: a dashboard account-management call while the workspace's Identity beta is off. The public account endpoints answer a uniform 404 instead. See below. |
| user_session_required | See Content API → Visibility. A gated read with no valid account session. |
| access_rule_not_satisfied | See Content API → Visibility. A gated read from a signed-in account the attached rule rejects. |
| access_rule_in_use | 409: dashboard only. Deleting a rule still attached to a collection is refused rather than silently un-gating it; detach it first. |
When a workspace does not have Identity enabled, every public account endpoint
(signup, login, everything under /api/v1/accounts) answers a plain 404, identical to an endpoint that does not exist. The public must not be
able to read a workspace's identity configuration off a status code. Dashboard surfaces
get the ordinary identity_disabled 403 above instead.
Hosting codes
Hosting is a per-workspace beta, Pro and up. These appear on /api/dashboard/sites/*, reached through the sedgemark CLI:
| Code | Meaning |
|---|---|
| hosting_disabled | 403: the workspace's Hosting beta is off. |
| hosting_not_configured | 503: the platform's Cloudflare configuration is missing. Not something a workspace can fix. |
| cloudflare_error | 502: Cloudflare answered the deploy request with a failure. |
| cloudflare_timeout | 504: Cloudflare did not answer in time. |
AI-field codes
AI-populated fields is generally available and billed to the tenant's own provider key, which shapes several of these. See the Callout below.
| Code | Meaning |
|---|---|
| ai_not_configured | 503: no usable provider key is saved for this automation. Unconfigured is a loud failure, not a silent no-op. |
| ai_fields_disabled | 403: the workspace's AI fields kill switch is off. |
| ai_budget_reached | 402: the tenant's own monthly budget for that provider is spent. Resolved by raising or removing the budget in Settings → AI, never by upgrading a plan; see the Callout below. |
| ai_rate_unknown | 400: a budget is set, but no price is known for the model in play, so the budget cannot be enforced against it. Set a rate, or remove the budget. |
| ai_provider_error | 502: the provider answered, and the answer was a failure. |
| ai_timeout | 504: the provider did not answer within the timeout. |
| ai_concurrency_limit | 429: too many generations already running for this workspace. Nothing is over budget; retry shortly. |
| ai_output_rejected | 422: the model's answer cannot be stored in the target field (an enum value outside the allowlist, non-numeric text for a number), or the provider issued a safety refusal. Reword the prompt rather than retrying unchanged. |
| ai_sources_empty | 400: manual generation only. Every field the prompt template reads from is empty, so there is nothing to summarize. |
| ai_credential_in_use | 409: dashboard only. Deleting a provider credential still used by an automation is refused; repoint or delete the automation first. |
It shares the 402 status with plan_limit_reached above, and
that is the only thing they have in common. A plan limit is resolved by paying
Sedgemark; an AI budget is the tenant's own number, raised or removed in
Settings → AI. Do not route ai_budget_reached into an upgrade prompt: it
would be selling a plan that changes nothing.
Rate limits
100 requests per minute, per IP address , shared across three surfaces:
-
/api/v1/*: the whole delivery API, including media and forms -
/api/mcp -
/api/dashboard/sdk
The budget is shared, not per-endpoint: MCP traffic and delivery traffic from the same address draw on the same 100. Over the limit you get:
HTTP/1.1 429 Too Many Requests
{ "error": "Too many requests" } The window is a fixed minute that starts at your first request, not a rolling one.
The 429 carries no Retry-After and no X-RateLimit-* headers: there is nothing to read the reset time from, so
back off on a schedule of your own choosing. A minute is always enough.
The bucket is keyed on the caller’s IP address alone, not on the workspace. Two workspaces built from the same CI runner draw on one shared budget, and adding a workspace does not add headroom.
Planning around it
-
A browser form submit costs up to three requests
: the descriptor
GETat page load, anOPTIONSpreflight (a JSON POST is never CORS-simple), then the POST itself. Preflights are cached for 24 hours per origin, and the descriptor fetch is only needed when the form has amin_submit_secondsrequirement, so a repeat visitor to a form with no timing requirement costs one. - Page with a large limit. Draining
a 1,000-entry collection at
limit=200costs 5 requests; atlimit=50it costs 20. 200 is the maximum. - Build, do not fetch per visitor. A static build reads your content once at build time from one machine. Fetching on every page view puts your visitors’ traffic against this limit.
- Shared addresses share the budget. Everyone behind one office NAT or one CI runner looks like a single caller.
A minimal backoff, which is all a build script needs:
async function sedgemarkFetch(url, init, attempt = 0) {
const res = await fetch(url, init)
// 429 is the only status worth retrying blindly. 4xx will not become 2xx.
if (res.status === 429 && attempt < 4) {
const wait = 2 ** attempt * 1000
await new Promise((r) => setTimeout(r, wait))
return sedgemarkFetch(url, init, attempt + 1)
}
return res
} What is safe to retry
Every endpoint on the public API except form submission is a GET, so
retrying is free of consequences. Retry 429 with backoff and 500 once; never retry a 400, 401, 403, 404 or 413 unchanged, since none of them will
become a success on their own.
Form submission is not idempotent: a retry after a successful 201 stores a second submission. Retry it only on 429, on a network failure with no response, or on the two retryable anti-spam
codes.