Piloterr

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. Your balance is checked before the job is queued, so a job never starts without the credits to pay for it.

202 Accepted (async)

Not billed. The request was accepted and queued for processing. Use the returned job ID to poll for the result — polling the job status is free.

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. Plan limits look like Rate limit exceeded: You've exceeded the … rate limit on your subscription. Per-key quotas look like Rate limit exceeded for the API key: quota daily (or total / weekly / monthly).

Fix: Back off and retry after a delay. If you hit this regularly, upgrade your plan or raise the key's quotas. These limits return 401, not 429.

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. The request was not executed. The error string tells you why:

  • Payment required — API quota and pay-as-you-go credits cannot cover the call
  • You have open invoices: … — an unpaid invoice is blocking the account
  • Insufficient credits: X required, Y available — a per-row endpoint would return more rows than you can afford

What to do:

  1. Purchase a top-up or upgrade your plan from your dashboard, or settle open invoices under Settings → Invoices.
  2. Or enable Auto Top-Up to prevent balance exhaustion 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. The request was refused before it ran. The usual causes are an endpoint that does not exist on your plan, a deactivated account, or an IP temporarily blocked after repeated failed authentications.

What to do: Check the endpoint path against the reference, then confirm your account is active in the dashboard. 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

StatusRetry?Strategy
400NoFix the request first
401 (rate limit)YesWait 2–5 s, then retry
401 (auth)NoFix the API key
402NoAdd credits or settle invoices first
403NoCheck permissions
404NoHandle as empty result
500YesExponential backoff (max 3 attempts)

Billing summary

StatusBilled
200Yes
201Yes
404Endpoint-specific (check the endpoint docs)
All other codesNo

See the Credits page for a full cost breakdown per endpoint. For broader integration advice, see Best Practices.

On this page