Keylayer error codes
The Keylayer relay can refuse a request with 42 codes, with statuses from 400 to 504, and every refusal carries a keylayer-error header with its code, while errors from the provider itself pass through without one. A refusal with a known wait also carries a retry-after header in seconds.
Tell a Keylayer refusal from a provider error
- The keylayer-error header holds the code, such as budget_exceeded. Check it first.
- Most refusals take the error shape of the key's provider, so your SDK parses them like any other error. Their message starts with Keylayer and the code, as in Keylayer budget_exceeded: this key's spend cap is reached; retry after 5m.
- missing_token, token_in_query, ambiguous_token, invalid_token, provider_unavailable and the flood limit's rate_limited have a flat body instead:
{"error": "invalid_token"}. - Cloudflare refuses TRACE and CONNECT before they reach the relay, with its own 405 or 400 and no keylayer-error header.
Both examples below print the code and the wait, and let every other error through:
import os
import openai
from openai import OpenAI
client = OpenAI(
base_url="https://relay.keylayer.dev/v1",
api_key=os.environ["KEYLAYER_KEY"],
)
try:
response = client.chat.completions.create(
model="gpt-5.4-mini",
messages=[{"role": "user", "content": "Say hello in five words."}],
max_completion_tokens=1000,
)
except openai.APIStatusError as err:
code = err.response.headers.get("keylayer-error")
if code is None:
raise
print(f"Keylayer refused the request: {err.status_code} {code}")
retry_after = err.response.headers.get("retry-after")
if retry_after is not None:
print(f"Retry in {retry_after} seconds")import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://relay.keylayer.dev/v1",
apiKey: process.env.KEYLAYER_KEY,
});
try {
await client.chat.completions.create({
model: "gpt-5.4-mini",
messages: [{ role: "user", content: "Say hello in five words." }],
max_completion_tokens: 1000,
});
} catch (error) {
if (!(error instanceof OpenAI.APIError)) {
throw error;
}
const code = error.headers?.get("keylayer-error");
if (!code) {
throw error;
}
console.log(`Keylayer refused the request: ${error.status} ${code}`);
const retryAfter = error.headers?.get("retry-after");
if (retryAfter) {
console.log(`Retry in ${retryAfter} seconds`);
}
}The provider pages show the same check in huggingface_hub, the ElevenLabs SDKs, Octokit, replicate and resend.
Retry headers
- retry-after gives the seconds to wait whenever Keylayer knows them: until the period resets for quota_exceeded and budget_exceeded, until 00:00 UTC for daily_cap and free_tier_limited, until the oldest request leaves the 60-second window for rate_limited, until Keylayer next tries to write the credential's activity log for trail_backlog, from 5 seconds to 5 minutes, and 1 second for concurrency_limit, gateway_busy and authority_unavailable.
- x-should-retry: false comes with a 429 whose wait is unknown or longer than 60 seconds. The openai, anthropic and groq SDKs for Python and Node.js stop retrying when they see it.
- A relay instance repeats a refusal from memory for up to 5 seconds. A repeat of a key-wide refusal, such as quota_exceeded or revoked, answers like the first: one without retry-after gains none, and one with it carries the seconds left of its own wait, so a repeated quota_exceeded keeps x-should-retry: false while that wait is over 60 seconds. Repeats of path, method and IP refusals carry no retry-after.
- The relay's flood limit also answers 429 rate_limited, with retry-after 60. It allows 600 requests a minute per IP address (per /64 for IPv6), 6,000 a minute per zone for requests from Cloudflare Workers, 3,000 a minute per key and 600 a minute per credential, on every plan.
Every code
| Code | Status | Meaning | What to do |
|---|---|---|---|
missing_token |
401 | The request has no Keylayer key, for example because it still sends the provider key. A browser's CORS preflight gets this too, because the relay doesn't answer preflights. | Send the key in Authorization (Bearer or token), x-api-key, x-goog-api-key, xi-api-key or api-key. Call the relay from a server. |
token_in_query |
400 | A Keylayer key is in the query string. | Move it to a header. |
ambiguous_token |
400 | The request has two different Keylayer keys. | Send one. |
invalid_token |
401 | The value isn't a working Keylayer key, or it's a minting key (kl_m_). | Copy the key again. Send a Keylayer key (kl_k_) to the relay. |
key_expired |
401 | The key's lifetime has run out. | Mint a new key. |
expired |
401 | The key or a key above it passed its Stops working (UTC) time. | Mint a new key. |
revoked |
401 | The key or a key above it was revoked. | Mint a new key. |
unknown_key |
401 | Keylayer has no record of this key. | Mint a new key. |
invalid_proof, kind_mismatch, route_mismatch |
401 | The key didn't pass the authority's verification. | Mint a new key. |
suspended |
403 | The key or a key above it is paused. | Set Status back to Active; see pausing a key. |
quarantined |
403 or 503 | Keylayer has taken the credential or its workspace out of service. | Retry later. |
ip_not_allowed |
403 | The caller's IP address isn't in the key's Only from list. | Call from an allowed address; see IP allowlist. |
method_not_allowed |
403 | The method isn't in the key's Only methods. | Use an allowed method. |
method_override |
400 | The request has x-http-method-override, x-http-method or x-method-override, or a $httpMethod query parameter. |
Drop the header or parameter and send the real method. |
path_not_allowed |
403 | The path isn't in the key's Only paths, or it's a blocked admin route. | Use an allowed path; see admin routes and escape hatches. |
ambiguous_path |
400 | The path has an encoded slash, backslash, null byte or other control character, a backslash, a semicolon, a double slash, a dot segment, or an escaped character that needs no escaping. | Send the plain path. |
host_not_sealed |
400 | The host behind a path prefix such as /-/router isn't sealed into the key's credential. | Add the provider key again as a new credential and mint from it. |
not_permitted |
403 | Keylayer couldn't check the request, for example a path over 8,192 characters. | Shorten the path. |
rate_limited |
429 | The key's Requests a minute or the free plan's 60 a minute per credential is used up, or the flood limit above was hit. | Wait retry-after seconds. |
quota_exceeded |
429 | A Requests cap is used up for its period. | Wait for the reset in retry-after, or raise the cap. |
daily_cap |
429 | The credential's daily request limit is reached. Every key on the credential counts toward it. | Wait for the reset at 00:00 UTC in retry-after. |
free_tier_limited |
429 | The free plan is busy today, and this workspace has used more than its share of it. Pro workspaces never get it. | Wait for the reset at 00:00 UTC in retry-after. |
concurrency_limit |
429 | The key already has Requests at once requests running. | Retry after 1 second, or raise the limit. |
budget_exceeded |
403 | What Keylayer reserves for the request doesn't fit in what's left of a spend cap for its period. | Wait for the reset in retry-after, ask for less, such as a lower max_tokens, or raise the cap; see spend caps. |
unpriced_operation |
403 | The key's spend cap can't count this request: a hard cap refuses anything Keylayer can't price up front, and any cap refuses a POST, PUT or PATCH to a route Keylayer can't price, a model missing from its price table (OpenRouter chat completions, responses and embeddings excepted), a body it can't read, a Gemini query parameter other than key or alt, work the provider bills after the response, such as a batch, and Anthropic fallbacks. The message ends with the method, the path and the reason, such as (POST /v1/images/generations: unpriced_route). |
Use a priced model or endpoint, or a key with an estimated cap for built-in tools; send valid JSON; send Gemini only key and alt in the query; send other routes, unknown models, billed-later work and Anthropic fallbacks with a key that has no spend cap; see hard and estimated caps. |
body_too_large_for_budget |
413 | The body is over 16 MiB, and the key or a key above it has a spend cap. | Send a smaller body. |
content_encoding_unsupported |
415 | The body is compressed (Content-Encoding), and the key or a key above it has a spend cap. | Send it uncompressed. |
body_timeout |
408 | The body took over 30 seconds to arrive, or stalled for 5, and the key or a key above it has a spend cap. | Retry. |
body_unreadable |
400 | The relay couldn't read the body. | Retry. |
gateway_busy |
503 | The relay is holding too many request bodies in memory right now, for your workspace or overall. | Retry after 1 second. |
estimate_failed |
500 | Keylayer couldn't price the request. | Retry. |
authority_unavailable |
503 | Keylayer couldn't reach the service that authorizes keys. | Retry after 1 second. |
trail_backlog |
503 | Keylayer is behind on writing the credential's activity log and holds new requests until it catches up. Keys below a minting key other than the root get it at half the backlog; see fixed limits. | Wait retry-after seconds. |
unwrap_failed |
500 | Keylayer couldn't decrypt the stored provider key. | Retry. |
internal_error |
500 | Something failed inside the relay. | Retry. |
provider_unavailable |
501 | The relay doesn't serve the key's provider. | Mint a key for a listed provider. |
upstream_unreachable |
502 | The provider couldn't be reached. | Retry. |
upstream_protocol |
502 | The provider answered with an invalid HTTP status. | Retry. |
reservation_refused |
403 | The key or a key above it was revoked, paused or expired while the request waited for the provider. | Check the key's status. |
deadline_exceeded |
504 | The request ran past its limit: 900 seconds on OpenAI, Anthropic, Google Gemini, Groq, Mistral, OpenRouter, Hugging Face and ElevenLabs, and 120 on GitHub, Replicate and Resend. | Split the work into shorter requests. |
If Keylayer stops a request after the provider has started answering, because the key was revoked, paused or expired or the deadline passed, the response ends early with a network error instead of a JSON body.
Errors when listing keys
GET /v1/keys refuses with a flat body such as {"error": "invalid_limit"} and no keylayer-error header. See listing keys.
| Code | Status | Meaning | What to do |
|---|---|---|---|
invalid_limit |
400 | The limit parameter isn't a whole number from 1 to 500. | Send 1 to 500, or leave it out for 100. |
invalid_cursor |
400 | The cursor parameter isn't shaped like a next_cursor. | Send the last answer's next_cursor as it is. |
rate_limited |
429 | The minting keys below the root already listed 60 times this minute. | Wait for retry-after, then list again. |
Error bodies by provider
Fields that depend on the status follow the provider's conventions, such as rate_limit_error on a 429 for OpenAI. The examples show 403 path_not_allowed.
- OpenAI, Groq:
{"error": {"message": "Keylayer path_not_allowed: ...", "type": "invalid_request_error", "param": null, "code": "path_not_allowed"}} - Mistral:
{"object": "error", "message": "Keylayer path_not_allowed: ...", "type": "invalid_request_error", "param": null, "code": "path_not_allowed"} - Anthropic:
{"type": "error", "error": {"type": "path_not_allowed", "message": "Keylayer path_not_allowed: ..."}} - Google Gemini:
{"error": {"code": 403, "message": "Keylayer path_not_allowed: ...", "status": "PERMISSION_DENIED", "details": [{"@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "PATH_NOT_ALLOWED", "domain": "keylayer"}]}} - OpenRouter:
{"error": {"code": 403, "message": "Keylayer path_not_allowed: ...", "metadata": {"keylayer_error": "path_not_allowed"}}} - Hugging Face:
{"error": "Keylayer path_not_allowed: ..."} - ElevenLabs:
{"detail": {"type": "authorization_error", "code": "path_not_allowed", "message": "Keylayer path_not_allowed: ...", "status": "path_not_allowed"}} - GitHub:
{"message": "Keylayer path_not_allowed: ...", "status": "403"} - Replicate:
{"title": "path_not_allowed", "detail": "Keylayer path_not_allowed: ...", "status": 403} - Resend:
{"statusCode": 403, "message": "Keylayer path_not_allowed: ...", "name": "path_not_allowed"}
Errors when minting
The Keylayer API refuses a mint with a flat body such as {"error": "label_required"} and no keylayer-error header. For the other mint codes, see how deep keys go and how many.
| Code | Status | Meaning | What to do |
|---|---|---|---|
label_required |
400 | The minting key or a key above it requires a label, and the mint has none. | Send a label of 1 to 64 characters. |
See limits for what each limit allows and the quickstart to mint your first key.
Updated