docs / reference
Error codes
The error envelope, and what every code in it means and what to do about it. The docsUrl on any non-2xx response points at its own anchor here.
complete · updated 1 september 2026
Every non-2xx response carries the same envelope, and the docsUrl in it points at the anchor on this page for its code. The code is the field to branch on: it is stable, it is narrower than the HTTP status, and it is the only part of the body written for a machine.
{
"error": {
"type": "validation_failed",
"code": "impact_preview_required",
"message": "An org-wide or pattern-matched freeze needs its blast radius resolved first.",
"details": [{ "path": "previewedRepoCount", "message": "required for an org or glob scope" }],
"docsUrl": "https://fwozen.com/docs/api/errors#impact_preview_required",
"requestId": "req_9tKQ2mF7bX4vZnRs"
}
}typeis the coarse family and follows the status.codeis the specific one. New codes ship without notice, so fall back totypefor one you do not recognise rather than failing.detailsis present on validation failures, one row per field, each with a path.requestIdidentifies that exact request in our logs. Quote it to [email protected] and we can find it.- The compatibility namespace is the one exception: it answers with the older
{ "error": "message" }shape plus an additivecodeandrequest_id, so a migrated script’s error handling keeps working.
400 invalid_request
malformed_json
The body did not parse as JSON. Check the quoting. A shell eats single quotes inside a single-quoted argument, so put the body in a file and send `-d @body.json`.
missing_parameter
A required query or body parameter is not there. The `message` names it.
invalid_parameter
A parameter arrived with the wrong type or a value outside its range. Read `error.details`: each row carries the field path and what was wrong with it. Inside a validated request body the same code answers 422.
invalid_cursor
The `cursor` is not one this endpoint issued. Cursors are opaque and endpoint-specific. Start the listing again with no cursor rather than editing one.
unsupported_content_type
The request declared a body type this endpoint does not accept. Send `Content-Type: application/json`. `curl -d` declares a form body unless you add that header, which is the single most common first failure against this API.
unknown_api_version
Reserved for a second API version. Nothing emits this today. `/v1` is the only version and it is permanent.
compat_feature_removed
The compatibility namespace carries the route, but the behaviour behind it is deliberately not reproduced. Only `POST /api/compat/v1/generate_access_token` answers this. Create keys at /app/settings/api-keys; a key can never mint another key.
401 unauthorized
missing_credentials
No `Authorization` header, or one this endpoint cannot read. Send `Authorization: Bearer fwz_…` or a session JWT. Create a key at /app/settings/api-keys. The compatibility namespace takes `fwz_` keys only.
invalid_token
The credential was read and rejected: an unknown key, a revoked one, or a JWT that does not verify. Check the key is the one you minted and has not been revoked or rotated past its grace window.
token_expired
The credential is past its expiry: an API key that has reached the date it was given, or a session JWT that has aged out. Mint a fresh JWT at `GET /api/auth/token` on the web origin with your session token, and cache it until it expires rather than per request. An expired key cannot be extended; create another at /app/settings/api-keys.
invalid_signature
A signed payload did not verify. GitHub webhooks and Slack requests authenticate this way. Check the shared secret, and that no proxy rewrote the body before it reached Fwozen.
403 forbidden
insufficient_permission
Your role in the organisation does not carry this permission. Ask an owner or an admin. Roles are set in organisation settings.
insufficient_scope
The API key does not carry a scope this route requires. Scopes are fixed at creation and cannot be widened. Mint a new key with the scope at /app/settings/api-keys.
credential_type_not_allowed
The route takes a session credential and got an `fwz_` key. It is never a 401, so a script can tell the two apart. Minting keys and setting enforcement up are session-only. Sign in, or use the device flow, and call it with the JWT.
feature_not_available
The organisation’s plan does not include this feature. `GET /v1/entitlements` lists what the plan carries.
github_permission_denied
GitHub refused a call Fwozen made for you. Usually the installation does not cover that repository any more, or the permission it needs was never granted. Re-check the installation on GitHub.
organization_not_provisioned
The credential is valid but resolves to no Fwozen organisation. `POST /v1/onboarding/bootstrap` creates one for the signed-in user, and is idempotent.
no_organization_selected
The session belongs to more than one organisation and none is active. `POST /api/auth/org/set-active` with the organisation id, then fetch a fresh token.
impersonation_read_only
A support session is reading your organisation. Support sessions cannot write. Nothing to do. It is the guarantee, not the bug.
404 not_found
not_found
No such object in this organisation. Another organisation’s id answers 404 rather than 403, on purpose: a 403 would confirm the id exists.
repository_not_found
No repository with that id or `owner/name` in this organisation. `GET /v1/repos` lists what Fwozen can see. A repository outside the installation is not visible at all.
branch_not_found
That branch is not watched. `GET /v1/branches` lists the watched branches and their `wbr_` ids. `POST /v1/repos/{repoId}/branches` starts watching one.
freeze_not_found
No freeze with that id. A freeze that ended keeps its id and still reads; check the id itself.
schedule_not_found
No schedule with that id. `GET /v1/schedules` lists them.
occurrence_not_found
No occurrence with that id on that schedule. Occurrences are generated ahead of time. List the schedule’s occurrences and use an id from the response.
exception_not_found
No exception with that id. An exception that expired keeps its id. Check the id itself.
api_key_not_found
No API key with that id in this organisation. Keys are organisation-scoped, so another organisation’s key id reads as absent.
member_not_found
That user is not a member of this organisation. `GET /v1/members` lists them.
invitation_not_found
No pending invitation with that id. An accepted or revoked invitation is gone from this route.
installation_not_found
No GitHub App installation with that id belongs to this organisation. `GET /v1/installations/status` resolves an install that has just landed.
pull_request_not_found
Fwozen has no state for that pull request. It sees pull requests on watched branches of enabled repositories. Check the repository is enabled and the base branch watched.
409 conflict
invalid_state
The object exists but is not in a state where this call means anything, such as revoking a key that is already revoked. Read the object first and branch on what it says.
already_frozen
The branch is frozen, and what you asked for would strand the check. Un-watching a frozen branch answers this. Lift the freeze first. Leaving a watched branch behind with a failing check run and nothing left to clear it is the one state the API refuses to create.
not_frozen
There is nothing to lift. Treat it as success in a retry loop. A thaw that finds nothing frozen has already got what it wanted.
billing_source_conflict
The organisation is already billed through another source, such as the GitHub Marketplace. Cancel at the source that owns the subscription before subscribing directly.
slack_already_connected
A Slack workspace is already connected to this organisation. Disconnect the existing workspace first.
schedule_overlap
The schedule collides with one that already exists. The response names the schedule it collided with. Widen one window or delete the other.
idempotency_key_reuse
That `Idempotency-Key` was already used with a different body, or an identical request is still in flight. One key per logical request. Generate a new one for a new freeze, and reuse the same one only when retrying the same call.
exceptions_not_permitted
This freeze was created with exceptions turned off. Nobody can grant one while it is in force. Lift the freeze, or wait for it to end.
admin_authorization_required
Writing the ruleset needs a one-time GitHub authorization from an admin on that repository. An admin opens the repository in the dashboard and runs the enforcement step once.
installation_claimed_by_other_org
That GitHub installation already belongs to a different Fwozen organisation. An installation belongs to one organisation. Uninstall and reinstall, or use the organisation that holds it.
already_exists
An object with that name or natural key is already there. Read it and patch it instead of creating a second one.
trial_already_used
You have had your no-card trial. It is once per person, not once per organisation. Making another organisation will not earn a second one. Subscribe, or stay on Free.
410 gone
organization_deleted
The organisation was deleted. Data is purged 30 days after deletion. Anything still holding a credential for it should stop.
installation_removed
The GitHub App installation was removed. Install the App again. Freezes ended when it went; the rulesets it wrote stayed in GitHub.
413 payload_too_large
export_too_large
The audit range holds more rows than one export may carry. The message names the row count and the cap. Narrow `from`/`to` and export again.
too_many_targets
The scope resolves to more targets than one request may carry. Split the scope, or freeze the organisation rather than enumerating it.
422 validation_failed
validation_failed
The body is well-formed JSON that does not match the schema. `error.details` carries one row per field, each with a path and a message.
reason_required
Every freeze carries a reason, and it has to be at least eight characters. Write what a colleague reading the audit log in March would need. `"4.2 release cut"` is enough; `"freeze"` is not.
no_targets_in_scope
The scope resolved to nothing, so there would be nothing to freeze. Enable a repository and watch a branch first. `POST /v1/scope/preview` shows what a scope resolves to before you commit to it.
invalid_timezone
The timezone is not an IANA name. Use `Europe/London`, not `GMT` or `BST`. The whole point of the name is that it carries the daylight-saving rules.
invalid_recurrence
The recurrence fields do not describe a window that can repeat. The message names the field. `POST /v1/schedules/preview` resolves a draft and answers this before you save one.
invalid_glob
A repository pattern did not parse. Patterns match `owner/name` with `*` and `**`, and `!` excludes. `POST /v1/scope/preview` shows what one matches.
impact_preview_required
An org-wide or pattern-matched freeze cannot be created blind, because the request does not say how big it is. Send `previewedRepoCount` from `POST /v1/scope/preview`, or `impactAcknowledged: true` to accept an uncalculated blast radius. Both answers are recorded; neither changes what gets frozen.
429 rate_limited
rate_limited
You are over the organisation ceiling or the per-credential write budget. Honour `Retry-After`. Read `RateLimit-Remaining` off every credentialled response and size retries from it rather than from a number in prose.
github_rate_limited
GitHub is rate-limiting Fwozen, not the other way round. Freezes already written stay in force. Wait out `Retry-After`; the reconciler catches up on its own.
github_secondary_rate_limit
GitHub’s secondary limit, which is about how fast a burst arrives rather than how many calls it holds. Back off further than the primary limit asks. Retrying faster makes it worse.
500 internal_error
internal_error
Something broke on our side, and it is not something you did. Nothing about your freezes changed: enforcement lives in GitHub’s required checks. Send `error.requestId` to [email protected] and we can find the exact request.
502 upstream_error
github_unavailable
GitHub did not answer. A check already written as failing keeps blocking. Retry; the reconciler re-asserts state after a recovery.
slack_unavailable
Slack did not answer. The freeze itself is unaffected. Slack is a notification surface, never the enforcement one.
stripe_unavailable
Stripe did not answer. Retry. Billing failures never lift a freeze or block a thaw.
workos_unavailable
Reserved. Authentication is first-party, so there is no identity provider to be unavailable. Nothing emits this. It stays in the enum so an older build inside a rollback window still validates the value.
503 unavailable
database_unavailable
Fwozen cannot reach its own database. Retry. Checks already written to GitHub are unaffected, which is the property the whole design rests on.
maintenance
Fwozen is in a maintenance window. Retry after the window. status.fwozen.com carries the current state.