Skip to main content

Error format

All error responses share the same envelope:
{
  "error": {
    "code": "error_code",
    "message": "Human-readable description of what went wrong."
  }
}
The code field is a stable string identifier — use this in your error handling logic. The message field is for humans and may change over time.

Error codes

HTTP StatusCodeDescription
400bad_requestMalformed request body or invalid query parameters
401unauthorizedMissing, invalid, or expired authentication credentials
403forbiddenAuthenticated but key lacks required scope
404not_foundResource does not exist (or you don’t have access to it)
405method_not_allowedHTTP method not supported for this endpoint
409conflictRequest conflicts with existing state (e.g. already following trader)
422quota_exceededBusiness rule quota exceeded (e.g. max 10 API keys)
429rate_limitedToo many requests — back off and retry
500internal_errorUnexpected server-side error

Examples

400 — Bad request

{
  "error": {
    "code": "bad_request",
    "message": "copyPercentage must be between 0 and 100."
  }
}
Returned when required fields are missing, types are wrong, or values fall outside allowed ranges.

401 — Unauthorized

{
  "error": {
    "code": "unauthorized",
    "message": "Invalid or missing API key."
  }
}
Check that:
  • You’re sending the Authorization: Bearer <key> header
  • The key hasn’t been revoked or expired
  • For key management endpoints, you’re using a Privy JWT, not an API key

403 — Forbidden

{
  "error": {
    "code": "forbidden",
    "message": "This key does not have the required scope."
  }
}
Your key is valid but lacks the scope for this operation. For example, calling POST /api/v1/traders with a key that only has traders:read.

404 — Not found

{
  "error": {
    "code": "not_found",
    "message": "Trader not found."
  }
}
The requested resource doesn’t exist, or you’re not following the specified trader.

409 — Conflict

{
  "error": {
    "code": "conflict",
    "message": "You are already following this trader."
  }
}
Returned by POST /api/v1/traders when you attempt to follow a wallet you’re already copying.

422 — Quota exceeded

{
  "error": {
    "code": "quota_exceeded",
    "message": "Maximum of 10 API keys per user. Please revoke an existing key first."
  }
}
Returned when a business rule cap is hit, distinct from a validation error.

429 — Rate limited

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 30 seconds."
  }
}
See Rate Limits for handling strategies.

500 — Internal error

{
  "error": {
    "code": "internal_error",
    "message": "An unexpected error occurred. Please try again."
  }
}
This shouldn’t happen. If you’re seeing consistent 500s, contact support@carboncopy.news.

Error handling example

async function callApi(url: string, key: string) {
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${key}` },
  });

  if (!response.ok) {
    const { error } = await response.json();

    switch (error.code) {
      case "unauthorized":
        throw new Error("Check your API key");
      case "forbidden":
        throw new Error(`Missing scope for ${url}`);
      case "rate_limited":
        const retryAfter = response.headers.get("Retry-After");
        throw new Error(`Rate limited — retry in ${retryAfter}s`);
      case "not_found":
        return null; // treat as empty, not fatal
      default:
        throw new Error(`API error: ${error.message}`);
    }
  }

  return response.json();
}