v1 API

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:

json
{ "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:

json
{
  "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

StatusMeansWhat to do
200Success.Nothing to do.
201A form submission was accepted.Check id: a null id means the honeypot was tripped and nothing was stored.
302A media id resolved to a signed file URL.Follow it. Every HTTP client does this by default.
400Malformed request: an invalid media id, unparseable JSON, or form validation failure.Fix the request. Retrying it unchanged will not help.
401A private collection or the MCP endpoint was reached with no valid API key.Send Authorization: Bearer with a valid key. See Authentication.
402The workspace’s plan does not cover this action.Only on authenticated dashboard surfaces. See below.
403A 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.
404No such collection, entry, media asset, or form; or no such workspace.Read the body to tell those apart. See below.
413A form submission body exceeded 64 KB.Send less. The cap is enforced while reading, before parsing.
429Over the shared per-IP rate limit.Back off and retry; it is the only status worth retrying automatically.
500Something 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.

BodyMeans
{ "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.
A missing subdomain header is a 500, not a 404

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.

CodeMeans
plan_limit_reachedYou are at the cap for this resource and cannot add another one.
plan_resource_lockedThe resource exists and is intact, but the current plan no longer covers it. Nothing has been deleted; upgrading restores access immediately.
On the public API a locked resource is simply a 404

/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:

CodeMeaning
email_taken409: signup, or a staff-created account: that address already has one.
user_disabled403: login only, and only after the password verified. Naming it before that would let a caller probe for which addresses exist.
identity_disabled403: 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_requiredSee Content API → Visibility. A gated read with no valid account session.
access_rule_not_satisfiedSee Content API → Visibility. A gated read from a signed-in account the attached rule rejects.
access_rule_in_use409: dashboard only. Deleting a rule still attached to a collection is refused rather than silently un-gating it; detach it first.
The flag being off is a uniform 404, not one of these codes

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:

CodeMeaning
hosting_disabled403: the workspace's Hosting beta is off.
hosting_not_configured503: the platform's Cloudflare configuration is missing. Not something a workspace can fix.
cloudflare_error502: Cloudflare answered the deploy request with a failure.
cloudflare_timeout504: 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.

CodeMeaning
ai_not_configured503: no usable provider key is saved for this automation. Unconfigured is a loud failure, not a silent no-op.
ai_fields_disabled403: the workspace's AI fields kill switch is off.
ai_budget_reached402: 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_unknown400: 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_error502: the provider answered, and the answer was a failure.
ai_timeout504: the provider did not answer within the timeout.
ai_concurrency_limit429: too many generations already running for this workspace. Nothing is over budget; retry shortly.
ai_output_rejected422: 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_empty400: manual generation only. Every field the prompt template reads from is empty, so there is nothing to summarize.
ai_credential_in_use409: dashboard only. Deleting a provider credential still used by an automation is refused; repoint or delete the automation first.
ai_budget_reached is not a plan limit

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
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.

No Retry-After, and the budget is per IP, not per workspace

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 GET at page load, an OPTIONS preflight (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 a min_submit_seconds requirement, 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=200 costs 5 requests; at limit=50 it 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:

javascript
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.