Error Handling
Understand Piloterr API error responses and learn how to handle each case gracefully.
Every request to the Piloterr API returns a standard HTTP status code. This guide covers what each code means, when credits are consumed, and how to handle failures in your integration.
Response format
Successful responses return a JSON object with the data for that endpoint. Error responses follow a consistent structure:
{
"error": "Human-readable description of what went wrong"
}Always check the HTTP status code before reading the body.
Status codes
200 Success
Billed. The request completed successfully. Credits are deducted from your balance.
const res = await fetch(url, { headers: { "x-api-key": API_KEY } })
if (res.status === 200) {
const data = await res.json()
// process data
}201 Job Created
Billed. An asynchronous job was created and accepted. Credits are deducted immediately when the job is queued.
202 Accepted (async)
Not billed yet. The request was accepted and queued for processing. Use the returned job ID to poll for the result. Credits are only billed when the job completes successfully.
400 Bad Request
Not billed. Your request contains invalid or missing parameters.
Common causes:
- A required query parameter is missing
- A parameter value has the wrong type or format
- The request body is malformed
What to do: Re-read the endpoint documentation and verify every required parameter. Log the full request URL to spot typos.
if (res.status === 400) {
const { error } = await res.json()
console.error("Bad request:", error)
// do not retry, fix the parameters first
}401 Unauthorized
Not billed. Returned for authentication issues and rate limiting. Check the error message to distinguish between them.
Your x-api-key header is missing or contains an unrecognized value.
Fix: Copy your key from your dashboard → API Keys and make sure the x-api-key header is present on every request.
The key exists but has been deactivated.
Fix: Go to your dashboard → API Keys and activate the key, or generate a new one.
The key has passed its expiry date.
Fix: Generate a new key or extend the expiry from your dashboard.
You have sent too many requests in a short window.
Fix: Back off and retry after a delay. If you hit this regularly, consider upgrading your plan.
if (res.status === 401) {
const { error } = await res.json()
if (error.toLowerCase().includes("rate limit")) {
await new Promise(r => setTimeout(r, 2000))
// retry
}
}402 Payment Required
Not billed. Your credit balance is empty. The request was not executed.
What to do:
- Top up your balance from your dashboard.
- Or enable Auto Top-Up to prevent this from happening again.
Auto Top-Up only triggers when your balance drops below the configured threshold. It never runs on a fixed schedule or at a specific time. A built-in 72-hour cooldown also prevents multiple charges in a short period: once a top-up fires, it cannot trigger again for 72 hours regardless of how your balance moves. If a charge fails, Auto Top-Up is automatically disabled to protect your account.
Do not retry a 402 response immediately. The call will fail again until you add credits. Set up an alert so your team is notified when this happens.
if (res.status === 402) {
// alert your team or trigger an auto top-up flow
throw new Error("Insufficient credits. Top up at https://app.piloterr.com")
}403 Forbidden
Not billed. Your key does not have permission to access this endpoint.
What to do: Verify that your key's permissions cover the endpoint you are calling. Contact support if you believe this is a mistake.
404 Not Found
May be billed (endpoint-specific). The resource was not found. Check the individual endpoint documentation to confirm whether this response is charged.
if (res.status === 404) {
// handle "no result" gracefully, not as a fatal error
return null
}500 Internal Server Error
Not billed. An unexpected error occurred on our side.
What to do: Retry with exponential backoff. If the issue persists, contact support.
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options)
if (res.status !== 500 || attempt === maxRetries) return res
const delay = 500 * 2 ** attempt
console.warn(`500 error, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`)
await new Promise(r => setTimeout(r, delay))
}
}Complete error handler
A single function that covers every status code:
interface ApiResult<T> {
data: T | null
error: string | null
status: number
billed: boolean
}
async function apiCall<T>(url: string, apiKey: string): Promise<ApiResult<T>> {
const res = await fetch(url, {
headers: { "x-api-key": apiKey },
})
// 200 and 201 are always billed; 404 billing is endpoint-specific
const billed = res.status === 200 || res.status === 201
if (res.status === 200) {
return { data: await res.json() as T, error: null, status: 200, billed }
}
const body = await res.json().catch(() => ({ error: "Unknown error" }))
const error = body?.error ?? "Unknown error"
switch (res.status) {
case 400:
console.error("[400] Bad request, fix your parameters:", error)
break
case 401:
if (error.toLowerCase().includes("rate limit")) {
console.warn("[401] Rate limit, back off and retry")
} else {
console.error("[401] Auth error, check your API key:", error)
}
break
case 402:
console.error("[402] No credits remaining. Top up at https://app.piloterr.com")
break
case 403:
console.error("[403] Forbidden, check your key permissions")
break
case 404:
console.warn("[404] Not found, no result for this request")
break
case 500:
console.error("[500] Server error, retry with backoff")
break
default:
console.error(`[${res.status}] Unexpected status:`, error)
}
return { data: null, error, status: res.status, billed }
}Retry strategy
| Status | Retry? | Strategy |
|---|---|---|
400 | No | Fix the request first |
401 (rate limit) | Yes | Wait 2–5 s, then retry |
401 (auth) | No | Fix the API key |
402 | No | Add credits first |
403 | No | Check permissions |
404 | No | Handle as empty result |
500 | Yes | Exponential backoff (max 3 attempts) |
Billing summary
| Status | Billed |
|---|---|
200 | Yes |
201 | Yes |
404 | Endpoint-specific (check the endpoint docs) |
| All other codes | No |
See the Credits page for a full cost breakdown per endpoint. For broader integration advice, see Best Practices.