Skip to main content
Error responses use the same top-level envelope but return an error object instead of data:
{
  "meta": {
    "requestId": "req_abc123xyz789"
  },
  "error": {
    "title": "Validation Error",
    "detail": "You must provide a valid API ID.",
    "status": 400,
    "type": "https://unkey.com/docs/errors/validation-error",
    "errors": [
      {
        "location": "body.apiId",
        "message": "API not found",
        "fix": "Provide a valid API ID or create a new API"
      }
    ]
  }
}

Error format

All API errors use application/json as the response media type. The error object is inspired by RFC7807 Problem Details, but Unkey doesn’t return a top-level application/problem+json document. The Problem Details-style fields live inside the Unkey response envelope:
  • title: short summary
  • detail: human-readable explanation
  • status: HTTP status code
  • type: URI for documentation
  • errors: optional validation details
Don’t document new v2 errors as application/problem+json. Use the same application/json media type as successful responses so clients, SDKs, and agents can parse every response with the same content-type rule.

Common error types

StatusError typeDescription
400validation-errorRequest body failed validation
401unauthorizedMissing or invalid authorization
403forbiddenValid authorization but insufficient permissions, only when the caller may know the resource exists
404not-foundResource not found, or the caller may not read it
409conflictConflicts with current state
429rate-limitedRate limit exceeded
500internal-server-errorUnexpected server error

Authorization failures must not reveal resource existence

A 403 and a 404 carry different information. If an endpoint answers 404 when a resource does not exist and 403 when it exists but the caller may not read it, a caller with no permissions can enumerate which resources exist by telling the two responses apart. The rule: an endpoint returns 403 only when the principal is allowed to know the resource exists, meaning it holds a permission that covers reading the resource, but lacks the permission the operation requires. When the principal may not read the resource, every operation on it returns the same 404 the missing resource produces, with an identical error type, title, and detail. This matches GitHub’s documented behavior of returning 404 instead of 403 for private resources. For read endpoints this collapses to: a permission rejection returns the resource’s not-found error. Two consequences for handler code:
  • The not-found branch needs no permission check at all, since unauthorized callers receive the identical response either way.
  • The masked 404 must be constructed fresh, not by wrapping the authorization error. The error middleware joins every public message in a fault chain into the response detail, and authorization rejections name the missing permissions, including concrete resource IDs, which would leak the existence the 404 is masking.
The reference implementation is svc/api/routes/v2_ratelimit_get_override, including a test that probes an existing and a missing resource with a zero-permission key and asserts the responses are indistinguishable.

Status codes and domain outcomes

HTTP status codes describe whether Unkey could process the HTTP request. They don’t describe normal product decisions. Use 200 when Unkey successfully processes a request, even if the domain result is negative. For example, key verification can return 200 with data.valid: false, and rate limiting can return 200 with data.success: false. Use 4xx when the request can’t be processed as submitted, such as invalid JSON, failed validation, missing authentication, insufficient permissions, or a missing resource. Use 5xx when Unkey fails to process a valid request. Don’t use HTTP status codes as business logic for expected decisions. Model the decision in the response data instead.

Validation errors

For validation errors, Unkey returns:
  • location: where the error occurred
  • message: what went wrong
  • fix: suggestion for resolution when available

Error recovery

Error messages are designed to be actionable. Use the requestId when reporting issues.

Error handling best practices

  1. Check status codes first.
  2. Parse the error object for details.
  3. Retry only on 5xx errors when appropriate.
  4. Log the full error response for debugging.
Example handling in JavaScript:
const response = await fetch("https://api.unkey.com/v2/keys.create", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${rootKey}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify(keyData)
});

const data = await response.json();

if (!response.ok) {
  const { meta, error } = data;
  console.error(`Error ${error.status}: ${error.title}`, {
    requestId: meta.requestId,
    detail: error.detail,
    docs: error.type
  });

  if (error.errors) {
    error.errors.forEach(err => {
      console.error(`- ${err.location}: ${err.message}`);
    });
  }

  throw new Error(`API Error: ${error.detail}`);
}