Skip to content

Errors

Every failure, on every route, has the same shape:

{
"error": {
"code": "forbidden",
"message": "Your role does not allow that.",
"detail": {},
"remedy": "This needs the admin role or above. An owner or admin can change yours.",
"requestId": "9a1c2e3f-..."
}
}
Field Always present What it is
code Yes A stable identifier for the kind of failure. Safe to branch on.
message Yes What went wrong, written for a person.
detail No Structured particulars, when there are any — the fields of a validation failure, or a GitHub rate limit’s retryAt.
remedy No What to do about it, when there is something to do.
requestId Yes The value of the response’s X-Request-Id header. Quote it when asking about a specific failure.

A body that fails validation before it reaches a handler is always invalid_request, with detail.issues holding the field-by-field problems Zod found.

code Status Meaning Remedy
unauthenticated 401 No credential was sent, or the one sent is not valid — including an unknown, revoked or expired API key. Send a session cookie, a session token, or a valid pb_... key.
forbidden 403 The credential is valid, but the actor’s role or the key’s scopes do not allow this action. Ask an owner or admin to change your role, or use a key with the needed scope.
not_found 404 Nothing with that id exists, or it exists in a workspace or portal this credential cannot see. Both cases answer the same way on purpose — an id from another tenant is never confirmed to exist.
invalid_request 422 The request body or query did not match the shape the route expects. Check field names and types against /openapi.json; detail.issues names the fields.
conflict 409 The request collides with something that already exists — a slug already taken, a duplicate invitation, an installation another workspace already holds. The message says what collided. Choose a different value, or the object already exists.
payment_required 402 The workspace’s subscription has lapsed. Reads still work; a write needs the workspace’s billing brought current. See Billing.
payload_too_large 413 An upload is larger than the cap. Send a smaller file. This is separate from invalid_request because the fix is not a field, it is the file.
unsupported_media_type 415 An upload’s bytes are not a kind Pealboard stores. Send one of the accepted types.
rate_limited 429 Either Pealboard’s own rate limit or GitHub’s was reached. See Rate limits — the two have different remedies and rate_limited covers both.
github_refused 502 GitHub refused the call, did not answer, or could not be reached. Nothing in Pealboard’s cache changed. The message and detail say which. On an unreachable GitHub the result of the request is genuinely unknown — check GitHub before retrying a write.
upstream_failed 502 A dependency other than GitHub refused or did not answer — mail, Stripe. Retry later. Nothing was written if the failure happened before Pealboard’s own state changed.
internal 500 Something failed inside Pealboard that is not one of the above. Quote requestId to support.

Better Auth’s endpoints under /auth and /portal/auth are browser redirects, not API calls that return JSON. A failed sign-in redirects to the application’s own /auth/error?error=<code> — the workspace application for /auth, the portal the request arrived on for /portal/auth. The error values are Better Auth’s, not this API’s; the ones a person actually meets are email_not_found (a GitHub account with no address Pealboard’s App may read), account_not_linked, signup_disabled, please_restart_the_process (an expired or reused sign-in state) and unable_to_create_user. Treat an unrecognized value as a generic failure and offer the sign-in screen again.