Errors

Unsuccessful requests return a consistent JSON shape. Branch on the machine-readable code — never on message, which is human-facing and may be reworded at any time.

Error shape

400 — validation

{
  "statusCode": 400,
  "code": "validation_error",
  "error": "Bad Request",
  "message": [
    "platforms must contain only valid values: youtube, tiktok, instagram",
    "keywords must contain at least 1 elements"
  ]
}
FieldTypeDescription
statusCodeintegerHTTP status, mirroring the response status
codestringStable machine-readable identifier. Branch on this. Values are only ever added — never renamed or repurposed
errorstringShort human label for the status
messagestring | string[]Human-readable detail (an array for validation errors). Not stable — do not parse

Some errors add fields specific to the failure — a 402 carries required_credits and remaining_credits, a 429 carries limit, remaining, reset_at, and retry_after.

Error codes

codeStatusWhen it happensWhat to do
validation_error400A parameter is missing, malformed, or out of rangeRead message[] and fix the request
invalid_date_range400start_date/end_date missing, unparseable, or spanning more than the endpoint's maximum window (90 days on hashtags)Narrow the window
unknown_region400A region value isn't a supported trend regionCall GET /v1/trends/regions for the current list
unknown_event_type400A webhook subscription names an event that doesn't existSee supported events
missing_api_key401No Authorization header was sentAdd Authorization: Bearer virlo_tkn_…
invalid_api_key401The key is malformed, revoked, or inactiveRegenerate from the dashboard
insufficient_credits402Balance is below the cost of the requested operationTop up, then retry. Compare required_credits to remaining_credits
forbidden403The key is valid but not permitted on this resourceCheck the resource belongs to your team
not_found404The resource doesn't exist, or isn't owned by your teamVerify the ID; a wrong-team ID reports as not-found by design
conflict409The operation conflicts with current stateRe-read the resource and retry
rate_limit_exceeded429You exceeded a rate limit windowWait retry_after seconds — see Rate Limits
upstream_error502 / 504A dependency was unreachable or too slowRetry with backoff; the request was not billed
service_unavailable503Virlo is temporarily unavailableRetry with backoff
internal_error500Unexpected server-side failureRetry once; if it persists, contact support with the timestamp

Status codes

RangeMeaningBilled?
2xxSuccessYes — only successful responses are charged
4xxClient error (auth, validation, balance, rate limit)No
5xxServer errorNo

Every response carries X-Cost and X-Credits-Used headers so you can confirm what a call charged — see Billing.

Handling errors

A reasonable client:

  1. Treats 4xx as terminal except 429 (wait retry_after) and 408.
  2. Retries 5xx with exponential backoff — these are never billed, so retries are free.
  3. Branches on code, not message.
  4. Surfaces required_credits / remaining_credits on 402 rather than retrying, since retrying cannot succeed.

Branching on code

const res = await fetch(url, { headers })
if (!res.ok) {
  const err = await res.json()
  switch (err.code) {
    case 'rate_limit_exceeded':
      await sleep(err.retry_after * 1000)
      return retry()
    case 'insufficient_credits':
      throw new Error(
        `Need ${err.required_credits}, have ${err.remaining_credits}`,
      )
    case 'upstream_error':
    case 'service_unavailable':
    case 'internal_error':
      return retryWithBackoff()
    default:
      throw new Error(err.message)
  }
}

Was this page helpful?