Error Codes
All API errors follow a consistent format:
{
"error": {
"type": "validation_error",
"message": "description is required",
"request_id": "req_01JQ...",
"details": [
{ "field": "description", "message": "required" }
]
}
}Error Types
| Type | HTTP Status | Description |
|---|---|---|
validation_error | 400 | Request body or parameters are invalid |
authentication_error | 401 | Missing or invalid API key |
authorization_error | 403 | Key lacks required permissions |
not_found | 404 | Resource does not exist |
conflict | 409 | Resource state conflict (e.g., cancelling a completed job) |
idempotency_error | 409 | Idempotency key reused with different parameters |
rate_limit_exceeded | 429 | Too many requests — check Retry-After header |
insufficient_credits | 402 | Not enough credits to post the job |
free_tier_job_limit | 403 | Free-tier organization hit the concurrent-job cap (default 1 active job) — upgrade or wait for current job to finish |
pricing_violation | 400 | price_per_file_cents is outside the platform-allowed band ($0.10–$500.00) or below the minimum-wage floor for the requested format |
transport_error | 502 | An upstream provider call (e.g., S3, Stripe, scoring) failed with a transport-level fault — retryable |
forbidden | 403 | Key is valid but the target resource is explicitly denied (e.g., internal-only endpoint, org mismatch) |
unauthorized | 401 | Authentication challenge required (e.g., a provisioning endpoint called without the service token) |
internal_error | 500 | Unexpected server error |
bad_gateway | 502 | Upstream service unavailable |
service_unavailable | 503 | API temporarily overloaded |
gateway_timeout | 504 | Upstream service timeout |
Handling Errors
Retryable Errors
These errors are safe to retry with exponential backoff:
429 rate_limit_exceeded— Wait forRetry-Afterheader duration500 internal_error— Retry up to 3 times502 transport_error/502 bad_gateway— Retry up to 3 times with backoff503 service_unavailable/504 gateway_timeout— Retry up to 3 times with backoff
Non-Retryable Errors
These require fixing the request:
400 validation_error— Fix request body400 pricing_violation— Adjustprice_per_file_centsinto the allowed band401 authentication_error/unauthorized— Check API key402 insufficient_credits— Purchase more credits403 authorization_error/forbidden— Key lacks the required scope, or the resource is off-limits403 free_tier_job_limit— Finish or cancel your active job, or upgrade off the free tier404 not_found— Check resource ID409 conflict/idempotency_error— Check resource state or use a fresh idempotency key
SDK Error Handling
Both SDKs automatically retry retryable errors with exponential backoff.
import { FirstHandApiError, FirstHandRateLimitError } from '@firsthandapi/sdk';
try {
await client.createJob({...});
} catch (e) {
if (e instanceof FirstHandRateLimitError) {
console.log(e.retryAfter); // seconds to wait
} else if (e instanceof FirstHandApiError) {
console.log(e.type); // "validation_error"
console.log(e.status); // 400
console.log(e.requestId); // "req_01JQ..."
console.log(e.retryable); // false
}
}from firsthandapi import FirstHandApiError, FirstHandRateLimitError
try:
client.create_job({...})
except FirstHandRateLimitError as e:
print(e.retry_after) # seconds to wait
except FirstHandApiError as e:
print(e.type) # "validation_error"
print(e.status) # 400
print(e.request_id) # "req_01JQ..."
print(e.retryable) # False