DOCS / ERRORS
VIEW RAW

Errors

Every error response on /v1/* has the same shape:

{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded (200 requests / second on pro).",
    "hint": "Retry after 1 second, or upgrade for a higher cap.",
    "field": "task_type"
  }
}

code is the stable machine-readable identifier — switch on this in your code, not on message. field is set when the error is tied to a specific request field. hint is a human-readable suggestion, never required to display.

Catalogue

CodeHTTPCauseCustomer fix
missing_authorization401No Authorization header sentSend Authorization: Bearer hh_*
invalid_api_key401Token format invalid or unrecognizedCheck the key was copied in full
key_revoked401The key existed but was revokedGenerate a new key at /api-keys
validation_error422Request body fails schema validationRead field and fix the input
unknown_task_type422task_type not in catalogue and no inline human_baseline_minutesUse a built-in, register a custom, or send a baseline inline
occurred_at_out_of_range422occurred_at is more than 90 days old or > 24h in the futureSend a recent timestamp
duplicate_idempotency_key200Same Idempotency-Key already used; original event returnedNone — this is the success path for a retry
rate_limited429Per-key per-second cap exceededRetry after Retry-After seconds
quota_exceeded429Plan monthly cap reachedUpgrade or wait for the next period
feature_locked403Feature requires a higher planUpgrade at /billing
internal500Bug on our sideCheck /status, retry with backoff

How to handle in code

const res = await fetch("/v1/track", { ... });
if (!res.ok) {
  const { error } = await res.json();
  switch (error.code) {
    case "rate_limited":
    case "duplicate_idempotency_key":
      // backoff + retry
      break;
    case "validation_error":
    case "unknown_task_type":
      // bug in your code, fix and don't retry
      throw new Error(error.message);
    case "feature_locked":
    case "quota_exceeded":
      // surface to the user; needs an upgrade
      throw new Error(error.message);
    default:
      throw new Error(error.message);
  }
}

The official SDKs (@humanhours/sdk, humanhours for Python) implement this switch with built-in retry on rate_limited + duplicate_idempotency_key.


Found a typo or want to suggest an edit? Email support@triadagency.ai.