Rate Limits and Errors

Per-token and per-IP rate limits, the canonical error envelope, and the full error code reference for the Arkanis public token surface.

What is this?

The complete error envelope (code,message,retryable,request_id,details), every error code REST or MCP can return, and the per-IP + per-token rate-limit tables. Worked examples for the codes you're most likely to hit:RATE_LIMITED,CAPABILITY_DENIED,RE_AUTH_REQUIRED,TOKEN_REVOKED.

Why you might want it

Your client needs one error handler that works for both REST and MCP. This page is what you wire it against. Theretryableflag is the only thing your retry loop needs to inspect; theRetry-Afterheader tells you how long to wait.

Per-IP Rate Limits

Every request to the public surface is counted against the source IP address. These limits sit alongside the per-token token-bucket and catch rapid-fire abuse from a single host before token revocation can propagate. The api uses Fastify's trust-proxy mode, so a client behind a reverse proxy is identified by its real IP, not the proxy's.

PathLimitWindowPurpose
/api/public/v1/*600 requests5 minutesCatches rapid-fire on a leaked token before revocation propagates.
/oauth/device/code20 requests10 minutesLimits device-code generation. Abusers cannot flood the user_code namespace.
/oauth/device/token120 requests10 minutesLimits polling abuse against the device-token endpoint.
/api-tokens (POST)20 requests / user1 hourLimits token-creation flooding from a compromised dashboard session.

When an IP exceeds its budget, the api returns 429 RATE_LIMITED with a Retry-After header in seconds. Standard X-RateLimit-Limit and X-RateLimit-Remaining headers are set on every /api/public/v1/* response so clients can self-pace without waiting for a 429.

Per-Token Rate Limits

Each token holds a token-bucket per risk tier. A read tool and a destructive tool do not share the same bucket. Aggregate caps per user per guild sit one tier above the per-token limit, so a single user with three tokens cannot multiply their destructive budget by three.

TierDefault capacityRefill
Read / utility120 calls60 / minute
Reversible write30 calls10 / minute
Destructive write6 calls1 / minute

A typical Pro guild driving the API from a single integration will never see a per-token 429 under normal use. There are no published per-tool quotas in v1. The tier of a given endpoint or MCP tool is derived from endpoint-policy.ts and tool-policy.ts, which are the canonical source for that mapping.

ℹ️
Note
Direct browser calls to /api/public/v1/* are blocked by CORS. Public REST tokens are designed for server-side scripts. If a customer-facing browser needs Arkanis data, route it through a server-side proxy.

Error Envelope

Every error from the public surface uses the same shape. REST returns the envelope with the matching HTTP status (401, 403, 404, 409, 422, 429, 5xx). The MCP server returns the same envelope inside the tool result body. Clients can write a single error handler and reuse it for both surfaces.

{
  "error": {
    "code": "RE_AUTH_REQUIRED",
    "message": "Re-authentication is required before running this operation.",
    "retryable": false,
    "request_id": "req_01HW9X8Y7Z6V5T",
    "details": {
      "reauth_url": "https://arkanis.gg/dashboard/123/settings/api-mcp#approve-window"
    }
  }
}

Field reference:

  • code: machine-readable error code. Drawn from the enum below.
  • message: human-readable explanation. Safe to surface to operators, never to an end user.
  • retryable: when true, the operation can be retried with backoff. When false, the client needs to fix something first.
  • request_id: trace id for the request. Include it when reporting issues to support.
  • details: optional structured payload that varies by code. The shape per code is documented below.

Retry Semantics

The retryable flag is the only thing a client needs to inspect to decide whether to retry. Codes that are transient set retryable: true. Codes that need a client-side fix set retryable: false.

retryable: trueretryable: false
RATE_LIMITED, UPSTREAM_UNAVAILABLE, INTERNAL_ERROREverything else. The client must fix the request, refresh the token, or open a re-auth window before retrying.

For retryable codes, honour Retry-After when present. Otherwise use exponential backoff starting at 1 second, doubling, capped at 30 seconds, with a small jitter.

Error Code Reference

The full enum of error codes the public surface can emit. The list is locked at v1 and lives in api/src/lib/public/error-envelope.ts as PUBLIC_ERROR_CODES.

CodeHTTPRetryableMeaning
UNAUTHORIZED401NoToken missing, malformed, or unrecognised.
FORBIDDEN403NoThe token is valid but the caller cannot act on the target resource.
CAPABILITY_DENIED403NoThe token's capability snapshot does not include the required capability.
INVALID_CONFIRMATION422NoDestructive call sent without a matching sentinel _confirmation string.
RE_AUTH_REQUIRED403NoDestructive call attempted outside an open re-auth window.
NOT_FOUND404NoThe target resource does not exist or is not visible to this token.
VALIDATION_FAILED400 / 422NoThe request body or query failed schema validation.
RATE_LIMITED429YesPer-IP or per-token rate limit exceeded. See Retry-After.
UPSTREAM_UNAVAILABLE502 / 503YesThe api could not reach Discord, the bot, or another upstream dependency.
TOKEN_EXPIRED401NoThe token reached its expiry date. Rotate it from the dashboard.
TOKEN_REVOKED401NoThe token was revoked by the issuer or by an owner. Re-issue.
TOKEN_IP_NOT_ALLOWED403NoCaller IP is outside the token's configured CIDR allowlist. Either widen the allowlist in the dashboard or call from a permitted source.
CORS_DENIED403NoDirect browser call to /api/public/v1/*. Use a server-side proxy.
INTERNAL_ERROR500YesUnhandled server error. The request_id is logged on the api side.

Example: RATE_LIMITED

Returned with HTTP 429 when the per-IP or per-token budget is exhausted. The Retry-After header carries the wait duration in seconds.

$ curl -i https://api.arkanis.gg/api/public/v1/strikes \
    -H "Authorization: Bearer arkpat_..."

HTTP/2 429
content-type: application/json
retry-after: 47
x-ratelimit-limit: 600
x-ratelimit-remaining: 0

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded. Retry in 47s.",
    "retryable": true,
    "request_id": "req_01HW9X8Y7Z6V5T"
  }
}

Wait the suggested cooldown and retry. Persistent 429s on a token point at a runaway loop on the client; an integration burning the destructive budget every minute is a sign of misconfigured automation, not a tuning problem.

Example: CAPABILITY_DENIED

The token is valid, but its capability snapshot does not include the capability required by the endpoint. Capabilities are inherited from the issuing user at token-creation time and re-checked on every call. If the issuer's capabilities changed after the token was issued, a new token is needed.

HTTP/2 403
content-type: application/json

{
  "error": {
    "code": "CAPABILITY_DENIED",
    "message": "Missing capability: enforcement_strike_add.",
    "retryable": false,
    "request_id": "req_01HW9X8Y7Z6V5T",
    "details": { "missing": ["enforcement_strike_add"] }
  }
}

The fix is to issue a fresh token from a user who currently holds the missing capability, or to adjust the role configuration in the dashboard and re-issue. Do not retry the same call with the same token — the result will not change.

Example: RE_AUTH_REQUIRED

Destructive operations require an open re-auth window. The default window is 15 minutes, opened by clicking Approve next destructive action in the dashboard. Outside the window, destructive calls return this envelope with a reauth_url pointing at the dashboard.

HTTP/2 403
content-type: application/json

{
  "error": {
    "code": "RE_AUTH_REQUIRED",
    "message": "Re-authentication is required before running this operation.",
    "retryable": false,
    "request_id": "req_01HW9X8Y7Z6V5T",
    "details": {
      "reauth_url": "https://arkanis.gg/dashboard/123/settings/api-mcp#approve-window"
    }
  }
}

For fully scripted workflows, an owner can also open the window via the authenticated dashboard API at POST /api-tokens/{tokenId}/reauth-window. The window then applies to the next destructive call from any client holding that token. See Destructive Actions for the full flow.

Example: INVALID_CONFIRMATION

Destructive operations require a sentinel string in the _confirmation body field. The string must exactly match the template for the operation, with placeholders substituted using the caller's actual arguments. The api returns the expected formats in details so a script can self-correct.

HTTP/2 422
content-type: application/json

{
  "error": {
    "code": "INVALID_CONFIRMATION",
    "message": "Confirmation string does not match the expected sentinel for this action.",
    "retryable": false,
    "request_id": "req_01HW9X8Y7Z6V5T",
    "details": {
      "expected_format": "BAN USER {user_id} IN GUILD {guildId} {PERMANENT|DURATION N}",
      "expected_concrete": "BAN USER 123456789012345678 IN GUILD 987654321098765432 PERMANENT"
    }
  }
}

Set _confirmation to expected_concrete and retry. The sentinel is case-sensitive and whitespace-sensitive. The full sentinel catalog lives in Destructive Actions.

Example: TOKEN_REVOKED

The token was revoked by its issuer, by a guild owner, or by an automated guard such as a downgrade off Pro. The dashboard records the revocation event in the audit log. Once revoked, a token is unusable.

HTTP/2 401
content-type: application/json

{
  "error": {
    "code": "TOKEN_REVOKED",
    "message": "This token has been revoked.",
    "retryable": false,
    "request_id": "req_01HW9X8Y7Z6V5T"
  }
}

Issue a new token from the dashboard and update any clients that still hold the old one. Do not attempt to revive a revoked token; revocation is final.

⚠️
Warning
Tokens are also revoked when the issuing user leaves the guild or loses Pro. Audit log rows tagged actor_source=token and code TOKEN_REVOKED are worth alerting on if a previously stable integration suddenly starts failing.