Rate limits and errors
Limits are enforced exactly, per key, per minute. Errors share one shape and one set of codes, so a single handler covers the whole API.
Rate limits
| Plan | Requests per minute, per key |
|---|---|
| Pro | 60 |
| Pro Scale | 300 |
| Client | 120 |
| Internal | 600 |
The window is a fixed minute and the count is shared across all our servers, so the limit holds even when you send a burst. A request over the limit gets 429 rate_limited with a Retry-After header (seconds) and is not charged. In front of that, our edge applies a coarse per-key and per-IP flood limit that no normal integration reaches.
Response headers
| Header | Meaning |
|---|---|
x-request-id | Unique id for the request. Quote it to support. |
x-credits-charged | Credits this response cost. 0 for free calls, refunds and replays. |
Retry-After | Seconds to wait. Sent with 429 and with 409 request_in_progress. |
x-idempotent-replay | true when the body is a stored replay of an earlier request. |
Error format
HTTP/1.1 402 Payment Required
x-request-id: 7f3c…
{
"error": {
"code": "insufficient_credits",
"message": "Not enough credits left this cycle. Top up or upgrade your plan.",
"requestId": "7f3c…"
}
}Branch on error.code, never on the message text, which may be reworded.
Error codes
| Status | Code | Meaning and what to do |
|---|---|---|
| 400 | invalid_query, invalid_job_description, invalid_role, invalid_id, invalid_url, invalid_events, invalid_kind, invalid_phone, invalid_city, missing_file, unsupported_type, file_too_large | A field is missing or out of range. The message names it and the limit. Fix the request; do not retry unchanged. |
| 400 | invalid_idempotency_key | Idempotency-Key must be 8 to 200 visible characters. |
| 400 | test_key | Test keys cannot register webhooks or be billed. Use a live key. |
| 401 | missing_key | No Authorization: Bearer header. |
| 401 | invalid_key | Unknown or malformed key. |
| 402 | insufficient_credits | Not enough credits this period. Top up, upgrade, or wait for renewal. |
| 402 | subscription_inactive | Period ended and grace is over. Renew via /v1/billing/checkout. |
| 403 | key_disabled | Key revoked or suspended. Contact support. |
| 403 | ip_not_allowed | Key is locked to other source addresses. |
| 403 | missing_scope | The key lacks the scope in requiredScope. |
| 403 | test_key | Test keys cannot call chat, resumes or feedback. |
| 404 | not_found | No such candidate, role or webhook. Not charged. |
| 405 | method_not_allowed | The MCP endpoint accepts POST only. |
| 409 | request_in_progress | Same Idempotency-Key is still running. Wait Retry-After seconds. |
| 409 | too_many_webhooks | A key can have at most 5 webhook endpoints. Delete one first. |
| 422 | idempotency_key_reused | Same Idempotency-Key with a different body. Use a new key. |
| 422 | unreadable_file, no_text | Resume could not be read, or has no text (scanned images are not supported). Not charged. |
| 429 | rate_limited | Over the per-minute limit. Wait Retry-After seconds. |
| 500 | internal_error | Our fault. Refunded. Safe to retry with the same Idempotency-Key. |
| 503 | scoring_unavailable | Match or fit scoring is briefly down. Not charged. Retry with backoff. |
Retry rules
- Retry: 429 (after
Retry-After), 409request_in_progress, 500, 502, 503, 504 and network errors. Use exponential backoff with jitter. - Do not retry unchanged: 400, 401, 403, 404, 422.
- Attach an Idempotency-Key to every retried POST so a retry after a timeout cannot charge twice. See Idempotency.
# curl has --retry for transient failures
curl --retry 4 --retry-all-errors https://api.helixhumancapital.in/v1/me -H "Authorization: Bearer $HELIX_KEY"import time, random, uuid, requests
def call(path, body, key):
idem = str(uuid.uuid4()) # one key for all attempts of this logical call
for attempt in range(5):
r = requests.post(f"https://api.helixhumancapital.in{path}", json=body,
headers={"Authorization": f"Bearer {key}", "Idempotency-Key": idem},
timeout=60)
if r.status_code not in (409, 429, 500, 502, 503, 504):
return r
wait = float(r.headers.get("Retry-After", 2 ** attempt)) + random.random()
time.sleep(wait)
return rasync function call(path, body, key) {
const idem = crypto.randomUUID() // one key for every attempt of this logical call
for (let attempt = 0; attempt < 5; attempt++) {
const r = await fetch(`https://api.helixhumancapital.in${path}`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", "Idempotency-Key": idem },
body: JSON.stringify(body),
})
if (![409, 429, 500, 502, 503, 504].includes(r.status)) return r
const wait = Number(r.headers.get("Retry-After") ?? 2 ** attempt) + Math.random()
await new Promise((res) => setTimeout(res, wait * 1000))
}
throw new Error("gave up")
}