Errors
All error responses share the same shape:
json
{
"error": {
"code": "insufficient_scope",
"message": "Missing required scope: cases:write.",
"required_scope": "cases:write"
}
}The fields you can rely on:
error.code— a stable string. Match on this, not onmessage.error.message— human-readable; safe to surface to a logged-in admin but don't echo back to an end-user verbatim.- Some codes carry extra context fields (
required_scope,bucket,errors, etc.).
Status code mapping
| HTTP | When |
|---|---|
400 | The request shape is wrong (bad URL, malformed JSON, oversized body). |
401 | The token is missing, invalid, revoked, or expired. |
403 | The token is fine but the request is not allowed (missing scope, IP not allowed). |
404 | The resource doesn't exist (case id, ticket id, etc.). |
409 | Idempotency-Key reused with a different body. |
422 | Validation failed (per-field; error.errors is keyed by field name). |
429 | Rate-limited. Honour Retry-After. |
502 | Discord refused an action we relayed on your behalf (e.g. the bot lacks channel permissions). Fix the Discord-side cause before retrying. |
5xx | We broke. Retry with backoff; we're alerted automatically. |
Code catalogue
Authentication
error.code | Status | Notes |
|---|---|---|
missing_token | 401 | No Authorization: Bearer … header. |
invalid_token | 401 | Token doesn't exist, is revoked, or expired. No distinction between these on purpose — don't leak existence to a probing attacker. |
https_required | 400 | Production endpoint reached over plain HTTP. |
not_authenticated | 401 | The scope middleware ran without an upstream token bind. Should be unreachable in production. |
Authorisation
error.code | Status | Notes |
|---|---|---|
insufficient_scope | 403 | Token lacks the required scope. Carries error.required_scope. |
ip_not_allowed | 403 | Request came from outside the token's IP allowlist. |
Rate limiting
error.code | Status | Notes |
|---|---|---|
rate_limited | 429 | Hit the per-token bucket. Carries error.bucket (read or write) and Retry-After. |
Idempotency
error.code | Status | Notes |
|---|---|---|
idempotency_key_reused | 409 | Same Idempotency-Key used with a different request body. |
idempotency_key_too_long | 400 | Keys must be ≤ 128 characters. |
Validation
error.code | Status | Notes |
|---|---|---|
validation_failed | 422 | Per-field validation errors. error.errors is keyed by field name with the messages as arrays. |
invalid_user_id | 400 | A user id path/query parameter is not a Discord snowflake (17–20 digits). |
balance_would_be_negative | 422 | /economy/adjust would push the balance below zero. |
xp_would_be_negative | 422 | /levelling/adjust delta would push total XP below zero. |
Resource state
These come back when the resource exists but is in the wrong state for the requested write.
error.code | Status | Notes |
|---|---|---|
case_deleted | 422 | PATCH /cases/{id} on a case whose status is deleted — deleted cases are read-only. |
ticket_not_open | 422 | POST /tickets/{id}/message (non-internal) or /close on a ticket that isn't open. |
ticket_deleted | 422 | Internal note on a deleted ticket. |
no_ticket_channel | 422 | Non-internal ticket message, but the ticket has no Discord channel to deliver to. |
close_reason_required | 422 | The guild's ticket settings require a close reason and none was sent. |
giveaway_not_running | 422 | /giveaways/{id}/end on a giveaway that isn't active, paused, or scheduled. |
giveaway_not_ended | 422 | /giveaways/{id}/reroll on a giveaway that isn't ended. |
not_enough_entries | 422 | Reroll had no eligible entries left after exclusions. |
Upstream (Discord)
error.code | Status | Notes |
|---|---|---|
discord_send_failed | 502 | Discord rejected the message we tried to send for you — usually the bot is missing Send Messages in the ticket channel, or the channel was deleted. |
Not found
error.code | Status | Notes |
|---|---|---|
not_found | 404 | The resource id is not in this guild (or has been deleted). |
Replay header
http
HTTP/1.1 200 OK
Idempotent-Replay: trueA response carrying this header was served from the idempotency cache; the handler did not run again. Treat it as a successful "your previous call already did this" rather than a fresh execution.
