Errors
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.
codeis the one to check. - Cost
- An error reply is free and has no cost 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
{
"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 as400(notstatus_code).error: a short label, such asBad 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 oncode.
Some errors add fields, for example:
402addsrequired_credits(the price) andremaining_credits(your balance), and sometimesrequired_amountandremaining_balancein dollars. See Insufficient balance.429withrate_limit_exceededaddsretry_after(seconds to wait),limit,remainingandreset_at. See 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 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 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 Topic Trends region isn't supported | See GET /v1/trends/regions |
400 unknown_event_type | A webhook names an event that doesn't exist | See 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 |
402 insufficient_credits | Your balance is below the price (1 credit = $0.01) | Add funds in 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 [email protected] with the request time |
What to do
Retry automatically in two cases only:
rate_limit_exceeded: waitretry_afterseconds.- 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
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('; ')}`)
}

