Every execution creates one account-scoped job. The same job resource is returned by the asynchronous execution path and by a `Prefer: wait` request whose wait window expires.

## Poll a job

```http
GET https://data.upscrape.com/jobs/JOB_ID
Authorization: Bearer UPSCRAPE_API_KEY
```

`GET /jobs/:id/result` remains available for compatibility and currently delegates to the same representation.

Jobs are account-scoped. A key from another account cannot retrieve them.

Every job also appears in the console under **Runs**, from any source (API keys, the playground, AI apps over MCP and monitors). Open one to see its input, status, cost and error, read its result, or download the whole result as one JSON file.

## Pending states

Queued, running, and retrying jobs return HTTP `202`:

```json
{
  "job_id": "job_id",
  "request_id": "job_id",
  "platform": "platform",
  "capability": "platform.capability.action",
  "state": "running",
  "poll_url": "https://data.upscrape.com/jobs/job_id",
  "poll_after_ms": 2000
}
```

`poll_url` is the address to read the job from (with the same API key), and `poll_after_ms` is the least time to wait before the first poll. After that, poll with bounded exponential backoff and jitter. A practical starting sequence is 1, 2, 4, 8, then 10 seconds. Stop after an application-defined deadline; timing out your local wait does not cancel the server-side job.

## Completed state

A completed job returns HTTP `200`, `state: "completed"`, `success: true`, a one-item `results` list, billing details, and execution statistics. Capability-specific JSON is under `results[0].data`.

Some supported q-commerce capabilities also include an additive `normalized` object. Raw output remains available and does not depend on normalization succeeding.

## Hosted artifacts

Capabilities that produce large binary files can return a hosted artifact
instead of base64 data:

```json
{
  "artifact_id": "artifact_id",
  "reference": "artifact://artifact_id",
  "delivery": "hosted",
  "kind": "zip",
  "filename": "example.zip",
  "content_type": "application/zip",
  "bytes": 12345678,
  "sha256": "hex_sha256",
  "download_url": "https://data.upscrape.com/archive-artifacts/artifact_id/download?token=...",
  "download_url_expires_at": "2026-09-01T12:00:00Z"
}
```

Download the file from `download_url` as a normal binary HTTP response; do
not decode the job JSON. The link is private, signed, and valid for one hour.
Do not log or share it. If the link expires while the job is retained, poll
`GET /jobs/:id` again to receive a fresh link. Hosted archive files are
retained for 30 days by default.

For Universal Web, `delivery: "auto"` keeps small archives inline and switches
to hosted delivery when the requested asset budgets exceed the inline safety
caps. Set `delivery: "hosted"` to force a downloadable file. Hosted mode
supports up to 50,000,000 bytes per asset and 500,000,000 bytes across archived
assets.

## Failed state

A failed job returns HTTP `200` with `state: "failed"`, `success: false`, and an error object:

```json
{
  "job_id": "job_id",
  "state": "failed",
  "success": false,
  "error": {
    "code": "upstream_error",
    "message": "sanitized failure description"
  },
  "results": null,
  "stats": null
}
```

Inspect the JSON state rather than treating every HTTP `200` poll response as success.

## Unknown jobs

An unknown or inaccessible job ID returns HTTP `404` with the stable `not_found` error code.
