# Errors

> What a Virlo API error looks like, what each error code means, and what to do about it.

Source: https://dev.virlo.ai/docs/errors
Markdown: https://dev.virlo.ai/docs/errors.md
Section: How the API works

## About the Virlo API (applies to every page)

- Base URL: `https://api.virlo.ai/v1`. Every request needs the header `Authorization: Bearer YOUR_API_KEY` (keys start with `virlo_tkn_`).
- Responses are JSON inside a `data` field, except the `/v1/webhooks` endpoints, which return the object or array directly. Field names are snake_case.
- Prices are in US dollars from a prepaid balance. 1 credit = $0.01. The `X-Cost` response header on each successful response is the exact charge. Errors are free.
- Slow jobs return an ID. Check its status every 15 seconds (or whatever `retry_after_seconds` says) until `finalized` is `true`.
- All docs pages: https://dev.virlo.ai/llms.txt. Every page in one file: https://dev.virlo.ai/llms-full.txt. MCP server for AI assistants: https://dev.virlo.ai/api/mcp/mcp.

---

Every error names its problem in `code`, such as `insufficient_credits`. 400-range errors are yours to fix; 500-range are ours.

**At a glance**

- **What it does:** Lists every error code and its fix.
- **You get back:** Four fields. `code` is the one to check.
- **Cost:** An error reply is free and has no [cost headers](https://dev.virlo.ai/docs/credits#response-headers). A creator, sound, or hashtag lookup that starts and then fails is refunded later.

## Error format

Every error has four fields:

**400 from POST /v1/agents:**

```json
{
  "message": [
    "keywords must contain at least 1 elements",
    "each value in platforms must be one of the following values: youtube, tiktok, instagram, meta_ads"
  ],
  "error": "Bad Request",
  "statusCode": 400,
  "code": "validation_error"
}
```

- `code`: the error's fixed name. New codes may appear, but existing ones are never renamed or reused.
- `statusCode`: the status number, such as `400` (not `status_code`).
- `error`: a short label, such as `Bad Request`.
- `message`: what went wrong. Usually a list when parameters fail checks, even one. A badly formed ID and other errors send one sentence. Handle both. Wording can change, so rely on `code`.

Some errors add fields, for example:

- `402` adds `required_credits` (the price) and `remaining_credits` (your balance), and sometimes `required_amount` and `remaining_balance` in dollars. See [Insufficient balance](https://dev.virlo.ai/docs/credits#insufficient-balance).
- `429` with `rate_limit_exceeded` adds `retry_after` (seconds to wait), `limit`, `remaining` and `reset_at`. See [Rate limits](https://dev.virlo.ai/docs/rate-limits).

## Error codes

| Error | When it happens | What to do |
| - | - | - |
| **400** `validation_error` | A parameter is missing, wrong, out of range or not accepted (`property colour should not exist`), or an ID is badly formed. A few endpoints, such as `GET /v1/agents`, ignore extra parameters. | Fix what `message` names |
| **400** `unsupported_platform` | A [raw data](https://dev.virlo.ai/docs/raw) route was asked for a platform it doesn't cover, such as an Instagram transcript | Use `tiktok` or `youtube`, or another route |
| **400** `invalid_cursor` | A [raw data](https://dev.virlo.ai/docs/raw#pagination) `cursor` was changed, has expired, or came from a different request | Start again from page 1 without `cursor` |
| **400** `invalid_date_range` | A date is missing or invalid, or the range is too long (hashtags: 90 days max) | Send both dates within the limit |
| **400** `unknown_region` | The Pop culture & News `region` isn't supported | See [`GET /v1/trends/regions`](https://dev.virlo.ai/docs/trends#list-regions) |
| **400** `unknown_event_type` | A webhook names an event that doesn't exist | See [supported events](https://dev.virlo.ai/docs/webhooks#supported-events) |
| **401** `missing_api_key` | No `Authorization` header. A key in `x-api-key` or the URL doesn't count. | Send `Authorization: Bearer YOUR_API_KEY` |
| **401** `invalid_api_key` | The key is wrong, revoked, or badly formatted | Use `Bearer` (capital B) and paste the key once; it already starts with `virlo_tkn_`. If revoked, create a new one in [API keys](https://dev.virlo.ai/dashboard/api-keys) |
| **402** `insufficient_credits` | Your balance is below the price (1 credit = $0.01) | Add funds in [Billing](https://dev.virlo.ai/dashboard/billing), then retry |
| **403** `forbidden` | You won't see this with an API key. Only dashboard team settings return it (wrong role, or invite for another email). A failure inside Virlo is never reported as 403. | Ask a team owner or admin |
| **404** `not_found` | The ID doesn't exist or belongs to another team, or the URL is wrong | Check the ID, and that the path starts with `/v1/` |
| **409** `conflict` | Something is in the way, for example you're already collecting that creator's past posts, or your team has 5 active webhooks (the max). | Wait for the job to finish, or delete a webhook or set one inactive, then retry |
| **429** `rate_limit_exceeded` | Your key sent too many requests | Wait `retry_after` seconds, then retry |
| **429** `too_many_requests` | Your key is missing or malformed, and your IP address sent too many requests. No `retry_after`. | Fix the `Authorization` header, then slow down |
| **502** `upstream_error` | A Virlo service behind the API failed or rejected Virlo's own request. Not caused by your request, and not charged. | Wait a few seconds and retry |
| **503** `service_unavailable` | Virlo is briefly unavailable or overloaded (for example, a database timeout). Not charged. | Wait a few seconds and retry, with backoff |
| **504** `request_timeout` | The request took longer than 27 seconds and was stopped. You are not charged for it. | Wait a few seconds and retry |
| **500** `internal_error` | Something went wrong on our side | Retry once or twice, then email <info@virlo.ai> with the request time |

## What to do

Retry automatically in two cases only:

- `rate_limit_exceeded`: wait `retry_after` seconds.
- Any 500-range error: wait 2 seconds, then 4, then stop.

For other errors, fix the cause the table names, then retry.

**Handle errors by code:**

```js
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

async function callVirlo(url, attempt = 1) {
  const res = await fetch(url, {
    headers: { Authorization: 'Bearer YOUR_API_KEY' },
  })
  if (res.ok) return res.json()

  // A few 500-range errors arrive as a web page, not JSON.
  // Treat those as a server-side error so they are retried too.
  const err = await res.json().catch(() => ({
    code: res.status >= 500 ? 'service_unavailable' : 'unknown_error',
    message: res.statusText,
  }))
  const canRetry = attempt < 3

  if (err.code === 'rate_limit_exceeded' && canRetry) {
    // Wait as long as the API asks, then try again.
    await sleep((err.retry_after ?? 60) * 1000)
    return callVirlo(url, attempt + 1)
  }
  const serverSide = ['upstream_error', 'service_unavailable', 'request_timeout', 'internal_error']
  if (serverSide.includes(err.code) && canRetry) {
    // Wait 2 seconds, then 4, then give up.
    await sleep(2 ** attempt * 1000)
    return callVirlo(url, attempt + 1)
  }
  if (err.code === 'insufficient_credits') {
    throw new Error(
      `Add funds: this request needs ${err.required_credits} credits, you have ${err.remaining_credits}`,
    )
  }
  // Anything else: fix the cause first (see the table).
  // message can be one sentence or a list of them.
  throw new Error(`${err.code}: ${[].concat(err.message).join('; ')}`)
}
```

---

More in How the API works:

- [How results load](https://dev.virlo.ai/docs/async-data.md)
- [Pagination](https://dev.virlo.ai/docs/pagination.md)
- [Rate limits](https://dev.virlo.ai/docs/rate-limits.md)
- [Webhooks](https://dev.virlo.ai/docs/webhooks.md)
- [Workflow recipes](https://dev.virlo.ai/docs/recipes.md)
- [API playground](https://dev.virlo.ai/docs/playground.md)
