API errors
RFC 9457 application/problem+json for every non-2xx response.
Status map
| Status | type suffix |
When |
|---|---|---|
| 400 | bad-request |
Malformed request, unknown or non-scalar query parameter, unknown body parameter, invalid/expired cursor, invalid Idempotency-Key, invalid limit |
| 401 | unauthorized |
Missing or invalid credential; the response carries WWW-Authenticate with error="invalid_token" when a credential was presented |
| 403 | forbidden |
Authenticated but not permitted (body carries required_permission / required_permission_scope, plus required_credential_scope when a bearer credential lacks the needed scope) |
| 404 | not-found |
Unknown resource, or a resource outside the caller's tenancy |
| 405 | method-not-allowed |
Wrong method for a known API path; the response carries Allow, and OPTIONS on a known path answers 204 with Allow |
| 409 | conflict |
A request with the same Idempotency-Key is in flight (Retry-After), state conflict (role still assigned, environment in use) |
| 410 | gone |
Endpoint past its sunset date; keeps the Deprecation/Sunset/Link headers |
| 412 | precondition-failed |
If-Match did not match |
| 413 | content-too-large |
Upload exceeds the size limit (RFC 9110's "Content Too Large") |
| 415 | unsupported-media-type |
Wrong request content type |
| 422 | validation |
Body or query failed validation (errors[] enumerated) |
| 422 | unprocessable |
Semantically invalid request that is not field validation (RFC 9110's "Unprocessable Content"); also a reused Idempotency-Key with a different payload |
| 429 | too-many-requests |
Rate limit exceeded (Retry-After, RateLimit-Policy, RateLimit in draft-11 syntax) |
| 500 | internal-error |
Unhandled server error |
| 503 | service-unavailable |
Upstream provider unavailable |
Body
{
"type": "https://pageweave.dev/errors/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "Insufficient permissions",
"instance": "/api/v1/websites/acme/pages",
"request_id": "01a1b2c3-...",
"required_permission": "page.content.update",
"required_permission_scope": "website"
}
typeis a stable URI identifying the error class (https://pageweave.dev/errors/<suffix>); clients branch on it, not ondetail. Dereferencing one redirects to the API error reference (/docs/api/errors).detailis human-readable and may change.instanceis the request path that produced the problem.request_idmatches theX-Request-Idresponse header and the server log line.errors[]appears only for validation problems:{ "field": "...", "detail": "..." }.fieldnames the request body key (path,source_path, ...), orbasewhen the model reports an error that belongs to no single field.required_permission/required_permission_scopename the endpoint's declared permission and the context its assignment is resolved in (workspace,website,environment). This is not a credential scope. Account-level endpoints (for examplePOST /websitesand API-key management) declare no resource permission, so both fields arenull;required_credential_scopeis the field to act on there.required_credential_scopeappears only when a bearer credential is present but lacks theread/writescope the endpoint needs — check this first when a request that used to work starts returning 403.- An unverified account is a separate 403: the body detail is
Email verification is requiredand it carries norequired_permission/required_credential_scope. It applies to a second website, publishing one, and website access invitations; API keys and the first non-indexable website remain available. The account check runs before the permission and credential checks, so an unverified caller sees this 403 even when they also lack the permission or the write scope. An invalid body still wins: request validation runs before authorization, so a malformed request is400, not the verification403. - Self-edit guards are
403: changing your own membership/access role or granting yourself access. Revoking your own website/environment grant is allowed without the manage permission (de-escalation). Workspace self-removal uses the leave endpoint. - Direct website grants and email invitations differ by design: a direct grant may override a member's implicit website role; an invitation to an address that already has access is
422(no silent demotion). 409also covers a custom role still assigned to a membership or access, a blocked website/workspace teardown, restoring a page or snippet whose path is now taken, deleting a table a live template page still uses, a release whose mine-scope items were edited by someone else, and a domain cancellation that is already scheduled, no longer undoable, or whose deletion is under way. Domain-specific conflicts: a pending purchase for the same domain with a different period, a registrar deletion already under way or already confirmed and scheduled, and a renewal charge in flight during confirmation. Structured extensions:suggested_pathon a restore (the first free path, never for template paths; nothing is renamed automatically),template_pages: [{id, path}]on a table delete, andconflicts: [{identifier, mine_version, latest_version, latest_author}]on a release conflict. A refused DNS zone deletion addsnameservers: ["ns1.pageweave.dev", ...]— the live parent answer that still delegates the name to PageWeave.422covers an unknown role key for the context, an unknown email target for a direct grant, the last owner leaving/demoting, provider sync validation that failed before any registry call, an ambiguous visitor-feedback short id (matches: [short_id, ...]), and invalid release selections (itemsnames the offending identifiers). For content it covers HTML/head validation, a duplicate page/snippet path, an unknown table slug, an invalid theme config, a redirect source that collides with a page or homepage, and a page title on a versionless page. For domains it covers an invalid, unsupported, or indeterminate availability answer, settings or lifecycle commands on an unregistered (released) row, and a missing registrant handle. Purchases add an invalid name, a taken or premium name, a TLD-disallowed period, a test-mode workspace, an unknown or mismatched workspace/website, and a different period for an already pending purchase. Deletion adds a confirmation that does not match the exact name, an unregistered (released) domain, and an invalid, expired, superseded, or already-used token (the token is never echoed in the problem body). DNS adds an invalid, duplicate, or subdomain zone name, a name registered by another workspace, the zone allowance, an unknown or mismatched workspace/website, a provider rejection, an unknown per-type record field, a missing structural record field, an exact duplicate record, an apexNSrecord, aCNAMEat the apex, and an edit/delete of the provider-managedSOAor apexNSrecords. A record id the zone does not hold is404— the id is the provider's, not a local row.- Content requests are strict about types and keys: an undeclared body key, a non-string
html_content/path/content, a non-booleanindexable, a non-objectconditions/placeholder_defaults, an unsupported redirectstatus_code, an emptyPATCHbody, an empty or oversized replacement list, or overwrite plus replacements for the same field is400before the service runs. 503covers a registrant-contact, billing-portal, registrar, or DNS provider failure (zone create/delete, record reads and writes, and a provider that answers with an authentication or throttling failure rather than rejecting the request). A domain settings or cancellation sync that fails leaves the local row unchanged; availability and auth-code fetches change nothing. A failed purchase start and a failed deletion confirmation both answer503without leaving a claimed token behind — retry the same request/link. Re-read before retrying.- Unhandled errors are reported to the error tracker and rendered as
internal-error, never as the framework's default body. - Query-string and cursor errors carry the offending parameter or a cursor message in
detail; clients recover by dropping the parameter or restarting from the first page. detailstrings are English regardless of the account language; clients branch ontypeand the structured fields.
Cross-tenant access is always 404, never 403: the API does not confirm that a resource exists outside the caller's scope.