# How results load

> Some Virlo jobs keep working in the background after you start them. How to tell when the results are fully ready, and how to wait for them.

Source: https://dev.virlo.ai/docs/async-data
Markdown: https://dev.virlo.ai/docs/async-data.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.

---

Agents, lookups, tracking, and audience snapshots keep working after you start them. Here is how to tell when results are ready.

**At a glance**

- **What it does:** Tells you when a background job's results are ready.
- **You send:** A free status check (polling) every 15 seconds, or whatever `retry_after_seconds` says. Or a webhook message telling you when to check.
- **You get back:** `finalized: true` once everything is ready, even if `status` says `completed`. A few jobs only have `status` (see the table).
- **Cost:** Checking is free. You pay when a job starts, and for each recurring agent run or tracking check.
- **How long:** About 30 seconds for a creator lookup. Usually under 20 minutes for a Content Research Agent to collect, plus a few for its AI report.

## Where to check on each job

Replace each `:placeholder` with its ID.

| Job | Check (free `GET`) | Done when |
| - | - | - |
| [Content Research Agent](https://dev.virlo.ai/docs/agents) | `/v1/agents/:id` | `finalized: true` |
| [Creator lookup](https://dev.virlo.ai/docs/satellite) | `/v1/satellite/creator/status/:job_id` | `finalized: true` |
| Creator lookup's Data Intelligence (about 3 min after `finalized`) | `/v1/satellite/runs/:run_id` (your `job_id`) | `intelligence_status` is `ready`, `failed`, or `skipped` |
| [Batch creator lookup](https://dev.virlo.ai/docs/satellite#poll-batch-status) | `/v1/satellite/creators/batch/:batch_id` | `status` is `completed` or `failed` |
| [Video outlier](https://dev.virlo.ai/docs/satellite/video-outlier), [sound](https://dev.virlo.ai/docs/satellite/sounds), or [hashtag](https://dev.virlo.ai/docs/satellite/hashtags) lookup | `/v1/satellite/<type>/status/:job_id` (`video-outlier`, `sounds`, `hashtags`) | `status` is `completed` or `failed` |
| [Tracked creator](https://dev.virlo.ai/docs/tracking) | `/v1/tracking/creators/:id` | `finalized: true` |
| [Post collection](https://dev.virlo.ai/docs/tracking#get-collection-status) | `/v1/tracking/creators/:id/posts/collect/:collection_id` | `status` is `completed` or `failed` |
| [Tracked video](https://dev.virlo.ai/docs/tracking) | `/v1/tracking/videos/:id` | `enrichment_status` is `ready` or `failed` |
| [Audience snapshot](https://dev.virlo.ai/docs/tracking#audience-basics) | `/v1/audience/snapshot/:job_id` | `finalized: true` |

After 24 hours, a lookup's status check returns `404`. Use `/v1/satellite/runs/:run_id` with the same ID.

## The done signal (`finalized`)

`finalized` turns `true` once the main job and every follow-up job, such as the AI report, have stopped. Until then, empty (`null`) fields mean "not ready", not "nothing found". This agent has finished collecting but is still writing its report:

**Report still being written:**

```json
{
  "data": {
    "id": "df04c83b-7104-4579-bb4d-43f64c392c87",
    "analysis": null,
    "pending_jobs": [
      {
        "type": "viral_analysis",
        "status": "processing",
        "poll_url": "/v1/agents/df04c83b-7104-4579-bb4d-43f64c392c87",
        "result_path": "data",
        "webhook_event": "content_research_agent.run.completed",
        "retry_after_seconds": 15
      }
    ],
    "finalized": false,
    "stage": "analyzing",
    "progress_pct": 75,
    "eta_seconds": 893,
    "latest_run": { "status": "completed" }
  }
}
```

**Finished is not the same as worked.** First check that the job's status is not `failed`: `latest_run.status` on agents, `enrichment_status` on tracked creators, `status` elsewhere. On agents, `partial_failure` means one keyword or platform failed. The rest is usable.

**It can turn back to `false`** when a recurring agent's next run or a tracked creator's next check starts.

**It doesn't cover** per-video [Data Intelligence](https://dev.virlo.ai/docs/async-data#intelligence-status). An agent's outlier-creator list can also shift for about a minute afterward.

## What is still running (`pending_jobs`)

`pending_jobs` lists follow-up work still queued or running. Jobs leave the list when they finish or fail, as does an AI report not started 15 minutes after collection ends. A failed job's result stays `null`. So an agent with `finalized: true` and `analysis: null` has no report coming for this run, but its videos are still good.

**An empty list does not mean done.** An agent that is still collecting shows `finalized: false` with `pending_jobs: []`. So does an audience snapshot job while its `status` is `processing`.

**Audience snapshots have two traps.** A failed snapshot never lets a creator lookup's `finalized` turn `true`. During a refresh, a tracked creator's audience results show the old snapshot with `finalized: true`. So check the audience job, `GET /v1/audience/snapshot/:job_id` (on a lookup, if an audience field still reads `processing` after 15 minutes). It ends `completed` or `failed`, and failures are refunded. For tracked creators, [the tracking route](https://dev.virlo.ai/docs/tracking#check-audience-refresh-status) works too, with friendlier field names.

**Full field reference**

- `type` (string, required): `viral_analysis` (an agent's AI report), `tracking_report` (a tracked creator's AI report), or `audience_demographics` and `audience_geography` (one audience snapshot job).
- `status` (string, required): `pending` or `processing`.
- `job_id` (string, optional): Audience snapshot jobs only.
- `poll_url` (string, optional): Free path to check.
- `result_path` (string, optional): Where the result appears (`data` is the whole object).
- `webhook_event` (string, optional): Event sent when it ends.
- `started_at` (string, optional): Not sent today.
- `retry_after_seconds` (integer, optional): `15` today.

## Per-video breakdowns (`intelligence_status`)

With [Data Intelligence](https://dev.virlo.ai/docs/intelligence) (a paid breakdown of the videos worth studying), every video and slideshow an agent returns has an `intelligence_status`:

- `ready`: the `intelligence` object is filled in. Agents with Data Intelligence off also get this, free, on videos Virlo already analyzed.
- `pending`: not analyzed yet, and it may never be. After `finalized`, re-fetch a few times, then use the `ready` videos.
- `skipped`: Virlo chose not to analyze it, so stop checking. `intelligence_skip_reason` says why: `intent_mismatch`, `too_old`, or `under_followers` ([details](https://dev.virlo.ai/docs/intelligence#when-fields-are-missing)).
- `disabled`: off for this agent. Turning it on only affects future runs: [update](https://dev.virlo.ai/docs/agents#update-agent) a recurring agent, or create a new one.

**Creator lookups** ([`GET /v1/satellite/runs/:run_id`](https://dev.virlo.ai/docs/satellite#durable-runs)) add `failed` (the run's analysis failed), and their `skipped` (this video got no analysis) carries no reason. The lookup's overall `intelligence_status` can be `null` for a few seconds after `finalized`. Check `request.data_intelligence` to see if you asked for it, and keep checking while the status is `null` or `pending`.

Slideshows leave out 16 video-only fields ([details](https://dev.virlo.ai/docs/intelligence#slideshow-intelligence)).

## Waiting by polling

These functions wait for `finalized: true`. For other jobs, use the done condition in the [table](https://dev.virlo.ai/docs/async-data#per-resource).

**Python:**

```python
import time
import requests

def wait_until_finalized(url, headers, timeout_s=3600, default_wait_s=15):
    started = time.time()
    while time.time() - started < timeout_s:
        res = requests.get(url, headers=headers)
        res.raise_for_status()
        data = res.json()["data"]
        if data.get("finalized") is True:
            return data
        # Wait as long as the pending jobs suggest (15 seconds today).
        pending = data.get("pending_jobs") or []
        wait_s = max(
            (job.get("retry_after_seconds") or default_wait_s for job in pending),
            default=default_wait_s,
        )
        time.sleep(wait_s)
    raise TimeoutError(f"{url} not finalized after {timeout_s} seconds")

agent = wait_until_finalized(
    "https://api.virlo.ai/v1/agents/YOUR_AGENT_ID",
    {"Authorization": "Bearer YOUR_API_KEY"},
)
# Finished is not the same as worked: check for a failed run first.
if (agent.get("latest_run") or {}).get("status") == "failed":
    raise RuntimeError("The agent run failed, so there are no results")
print(agent["analysis"])
```

**TypeScript:**

```ts
// Uses top-level await: run it as an ES module (for example, a .mts file).
async function waitUntilFinalized(
  url: string,
  headers: Record<string, string>,
  { timeoutSeconds = 3600, defaultWaitSeconds = 15 } = {},
) {
  const started = Date.now()
  while (Date.now() - started < timeoutSeconds * 1000) {
    const res = await fetch(url, { headers })
    if (!res.ok) throw new Error(`${url} returned HTTP ${res.status}`)
    const { data } = await res.json()
    if (data.finalized === true) return data

    // Wait as long as the pending jobs suggest (15 seconds today).
    const pending: Array<{ retry_after_seconds?: number }> = data.pending_jobs ?? []
    const waitSeconds = pending.length
      ? Math.max(...pending.map((job) => job.retry_after_seconds ?? defaultWaitSeconds))
      : defaultWaitSeconds
    await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000))
  }
  throw new Error(`${url} not finalized after ${timeoutSeconds} seconds`)
}

const agent = await waitUntilFinalized(
  'https://api.virlo.ai/v1/agents/YOUR_AGENT_ID',
  { Authorization: 'Bearer YOUR_API_KEY' },
)
// Finished is not the same as worked: check for a failed run first.
if (agent.latest_run?.status === 'failed') {
  throw new Error('The agent run failed, so there are no results')
}
console.log(agent.analysis)
```

They stop after an hour. Raise that for large agents, but keep a limit.

### Show progress while you wait

Agents also return `stage`, `progress_pct`, and `eta_seconds` on `GET /v1/agents/:id` and `/summary`, for a progress bar:

| `stage` | `progress_pct` | `eta_seconds` |
| - | - | - |
| `collecting` | `40` | `null` |
| `analyzing` (AI report) | `75` | Counts down from about 900, a worst case |
| `finalizing` | `90` | `null` |
| `completed` | `100` | `0` |

Recurring agents often jump from `collecting` straight to `completed`.

## Waiting with a webhook

A webhook is a message Virlo sends to your server when a job ends. Register one for all four events:

**One webhook for all jobs:**

```bash
curl -X POST https://api.virlo.ai/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example.com/webhooks/virlo",
    "description": "All finished jobs",
    "enabled_events": [
      "content_research_agent.run.completed",
      "satellite.lookup.completed",
      "tracking.cycle.completed",
      "audience.snapshot.completed"
    ]
  }'
```

Use your own public `https://` address; the example gets a `400`.

**Messages are not copies of the `GET` response** and have no `finalized` or `pending_jobs`. Some carry the full result, others only IDs. When one arrives, call the job's `GET` once. See [event contents](https://dev.virlo.ai/docs/webhooks#data-shape) and [retries](https://dev.virlo.ai/docs/webhooks#retry-ladder).

**Poll too.** Messages can be late or lost, and some jobs send none: post collections, a lookup's Data Intelligence, failed audience snapshots, and free repeat lookups within 6 hours. A tracked item that can't be checked sends `tracking.paused` instead. Also check each job's `GET` every 10 minutes or so.

---

More in How the API works:

- [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)
- [Errors](https://dev.virlo.ai/docs/errors.md)
- [Workflow recipes](https://dev.virlo.ai/docs/recipes.md)
- [API playground](https://dev.virlo.ai/docs/playground.md)
