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. code is 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 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.
  • 429 with rate_limit_exceeded adds retry_after (seconds to wait), limit, remaining and reset_at. See Rate limits.

Error codes

ErrorWhen it happensWhat to do
400 validation_errorA 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_platformA raw data route was asked for a platform it doesn't cover, such as an Instagram transcriptUse tiktok or youtube, or another route
400 invalid_cursorA raw data cursor was changed, has expired, or came from a different requestStart again from page 1 without cursor
400 invalid_date_rangeA date is missing or invalid, or the range is too long (hashtags: 90 days max)Send both dates within the limit
400 unknown_regionThe Topic Trends region isn't supportedSee GET /v1/trends/regions
400 unknown_event_typeA webhook names an event that doesn't existSee supported events
401 missing_api_keyNo Authorization header. A key in x-api-key or the URL doesn't count.Send Authorization: Bearer YOUR_API_KEY
401 invalid_api_keyThe key is wrong, revoked, or badly formattedUse 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_creditsYour balance is below the price (1 credit = $0.01)Add funds in Billing, then retry
403 forbiddenYou 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_foundThe ID doesn't exist or belongs to another team, or the URL is wrongCheck the ID, and that the path starts with /v1/
409 conflictSomething 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_exceededYour key sent too many requestsWait retry_after seconds, then retry
429 too_many_requestsYour 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_errorA 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_unavailableVirlo is briefly unavailable or overloaded (for example, a database timeout). Not charged.Wait a few seconds and retry, with backoff
504 request_timeoutThe request took longer than 27 seconds and was stopped. You are not charged for it.Wait a few seconds and retry
500 internal_errorSomething went wrong on our sideRetry once or twice, then email [email protected] 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

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('; ')}`)
}

Was this page helpful?