---
title: "API Errors — PageWeave"
---

# 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

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

- `type` is a stable URI identifying the error class (`https://pageweave.dev/errors/<suffix>`); clients branch on it, not on `detail`. Dereferencing one redirects to the API error reference (`/docs/api/errors`).
- `detail` is human-readable and may change.
- `instance` is the request path that produced the problem.
- `request_id` matches the `X-Request-Id` response header and the server log line.
- `errors[]` appears only for validation problems: `{ "field": "...", "detail": "..." }`. `field` names the request body key (`path`, `source_path`, ...), or `base` when the model reports an error that belongs to no single field.
- `required_permission` / `required_permission_scope` name 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 example `POST /websites` and API-key management) declare no resource permission, so both fields are `null`; `required_credential_scope` is the field to act on there.
- `required_credential_scope` appears only when a bearer credential is present but lacks the `read`/`write` scope 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 required` and it carries no `required_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 is `400`, not the verification `403`.
- 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).
- `409` also 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_path` on a restore (the first free path, never for template paths; nothing is renamed automatically), `template_pages: [{id, path}]` on a table delete, and `conflicts: [{identifier, mine_version, latest_version, latest_author}]` on a release conflict. A refused DNS zone deletion adds `nameservers: ["ns1.pageweave.dev", ...]` — the live parent answer that still delegates the name to PageWeave.
- `422` covers 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 (`items` names 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 apex `NS` record, a `CNAME` at the apex, and an edit/delete of the provider-managed `SOA` or apex `NS` records. A record id the zone does not hold is `404` — 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-boolean `indexable`, a non-object `conditions`/`placeholder_defaults`, an unsupported redirect `status_code`, an empty `PATCH` body, an empty or oversized replacement list, or overwrite plus replacements for the same field is `400` before the service runs.
- `503` covers 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 answer `503` without 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.
- `detail` strings are English regardless of the account language; clients branch on `type` and 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.
