Error catalog
Errors are problem-style JSON with a stable type URI. Match on type and status, never on
title — titles are human-facing and may be reworded.
Shape
{
"type": "https://evonia-gate.annatarhe.com/problems/validation",
"title": "Request validation failed",
"status": 400,
"errors": [{ "path": ["body", "scopes"], "message": "Array must contain at least 1 element(s)" }]
}| Field | Present |
|---|---|
type | Always |
title | Always |
status | Always |
errors | Validation failures only |
requestId | Errors raised after the request id is assigned |
Problem types
| Type | Status | Meaning |
|---|---|---|
.../problems/validation | 400 | The request failed schema validation. errors lists the Zod issues |
.../problems/http-401 | 401 | No usable credential |
.../problems/http-403 | 403 | Authenticated, but not permitted |
.../problems/http-404 | 404 | Not found, or not visible to this caller |
.../problems/conflict | 409 | A unique constraint was violated |
.../problems/http-4xx | 4xx | Any other client error |
.../problems/internal | 500 | Unhandled server error |
All are prefixed https://evonia-gate.annatarhe.com.
Symptom to cause
401 Authentication required
- No
X-API-Keyheader and no session cookie. - The API key is malformed. It must match
gate_sk_<12>_<43>. - The key was revoked, has passed
expiresAt, or its service account is disabled. - The session expired.
403 A user session is required
You sent a service API key to an endpoint that mints credentials or manages organizations. These require a real user session; there is no scope that unlocks them. See Choose an integration mode.
403 Permission denied
- A user whose organization role lacks the permission. Check the role matrix.
- A service account whose key scopes lack the permission string.
- A service account addressing a project other than its own.
A frequent case: a developer reading a secret by key. That needs config:reveal, which developer
does not have.
403 Service account is scoped to another environment
The service account has environmentId set and you addressed a different environment. This is
checked before the permission check, so widening scopes will not help — create an unpinned service
account or use the right environment.
404 Not found
- The id is wrong or belongs to another organization.
- The record is archived and you did not pass
?state=archivedor?state=all. - A user with no membership in the owning organization. Gate returns
404rather than403so it does not leak which ids exist.
409 Conflict
A unique constraint was violated, surfaced from Postgres error 23505. Usually:
- An organization slug or project slug already in use.
- A duplicate project role slug within the project.
- A duplicate role binding for the same
(roleId, principalType, principalId).
400 on checkout
POST /projects/{projectId}/billing/checkout requires an idempotency-key header of 16–255
characters. It is a header, not a body field.
successUrl and cancelUrl must be on an allow-listed origin — Gate's own, or one of the product
origins in src/server/origins.ts.
Things that are not errors
Duplicate usage events. A replay returns 200:
{ "data": { "accepted": false, "duplicate": true, "id": "..." } }Not a 409. Stop retrying.
Masked secrets in a list. value: null, masked: true is correct behaviour, not a permission
failure. Read the key individually to decrypt it.
A missing social sign-in button. GitHub and Google appear only when the operator configured their client id and secret.
Diagnosing with request ids
Send x-request-id on every call and log it. Gate echoes it on the response and records it on audit
rows, so one identifier connects your logs to Gate's.
curl -sSi "https://evonia-gate.annatarhe.com/api/v1/projects/$PROJECT_ID" \
-H "x-api-key: $GATE_API_KEY" \
-H "x-request-id: $(uuidgen)"If you did not send one, Gate generated a UUID and returned it in the same header.
Client-side handling
type Problem = {
type: string;
title: string;
status: number;
requestId?: string;
errors?: { path: string[]; message: string }[];
};
export async function gate<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(`https://evonia-gate.annatarhe.com/api/v1${path}`, {
...init,
headers: {
"content-type": "application/json",
"x-api-key": process.env.GATE_API_KEY!,
"x-request-id": crypto.randomUUID(),
...init?.headers,
},
});
const payload = await response.json();
if (!response.ok) {
const problem = payload as Problem;
throw new GateError(
problem.errors?.[0]?.message ?? problem.title,
problem.status,
problem.type,
problem.requestId,
);
}
return payload.data as T;
}Retry 500s and network failures with backoff. Never retry a 400, 401, 403, or 409 — the
request will fail identically every time.