> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sixtyfour.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Async Jobs

> Statuses, result shapes, polling limits, webhooks, retries and retention for jobs read with GET /job-status.

Each `-async` endpoint, and each [Bulk Intelligence](https://docs.sixtyfour.ai/api-reference/endpoint/bulk-intelligence.md) submission, starts a job and returns its `task_id` at once. `GET /job-status/{task_id}` reports the job's status and, once it completes, its result.

```json Start response theme={null}
{"task_id": "3f8b2c1e-...", "status": "RUNNING"}
```

A start response carries `task_id` and an uppercase `status`, `RUNNING` for a new job. Some endpoints add fields, such as `unrecognized_fields`, `investigation`, `total_leads`, `mode` or `estimated_cost_credits`. Job status reports statuses in lowercase. The format of `task_id` varies by endpoint, so store it exactly as returned.

Age Verification jobs report their status at [`GET /verify-age/{task_id}`](https://docs.sixtyfour.ai/api-reference/endpoint/age-verification#status) instead.

## Job status

```http theme={null}
GET https://api.sixtyfour.ai/job-status/{task_id}
```

| Field | Present | Description |
| - | - | - |
| `status` | Always | See [Statuses](#statuses). |
| `id` | Always | The job's `task_id`. |
| `run_id` | Always | An internal execution ID, not the `run_id` of a bulk callback. Refer to the job by `task_id`. |
| `start_time` | Always | When the job started (ISO 8601). |
| `close_time` | `completed` | When the job finished. |
| `result` | `completed` | The job's output. See [Results by job type](#results-by-job-type). Bulk jobs return files in `results` instead. |
| `task_type` | `completed` | The job type. See [Results by job type](#results-by-job-type). Not returned for bulk jobs. |
| `charge_credits` | `completed` | Credits the job charged, including any paid sources its research used. Omitted when the charge is unconfirmed, and on bulk jobs once it [expires](#retention). `charge_amount` is the [legacy field](https://docs.sixtyfour.ai/guides/credits-and-pricing.md#costs-in-api-responses). |
| `results`, `results_url` | `completed` bulk jobs | Signed links to the output files, and the run in the Sixtyfour app. See [Bulk Intelligence](https://docs.sixtyfour.ai/api-reference/endpoint/bulk-intelligence.md#polling-for-results). |
| `progress` | `running` bulk jobs | Rows enriched so far, such as `{"completed": 120, "total": 500}`. |
| `investigation` | Atlas runs | `{"id", "workspace_url"}` of the run's [Atlas investigation](https://docs.sixtyfour.ai/atlas/integrations.md). |
| `error` | `failed` | Set only when the job was refused for insufficient credits. |

```json Completed job theme={null}
{
  "status": "completed",
  "id": "3f8b2c1e-...",
  "run_id": "7d41a0c2-...",
  "start_time": "2026-11-02T19:30:12Z",
  "close_time": "2026-11-02T19:34:48Z",
  "result": {
    "structured_data": {
      "title": "CEO & Co-Founder",
      "linkedin": "https://www.linkedin.com/in/saarthshah"
    },
    "notes": "Confirmed current role from the LinkedIn profile and the company site.",
    "references": {
      "https://www.linkedin.com/in/saarthshah": "LinkedIn profile with current title"
    },
    "confidence_score": 9
  },
  "charge_amount": 10,
  "charge_credits": 1,
  "task_type": "lead_enrichment"
}
```

### Statuses

| Status | Meaning |
| - | - |
| `running` | Waiting for capacity or in progress. |
| `completed` | Finished. The response carries the result and the charge. |
| `failed` | Ended without a result. `error` is set to `"Insufficient API credits"` when the job was refused for insufficient credits; any other failure carries no reason. A People Intelligence, Company Intelligence or QA Agent job that finds no capacity within 24 hours also ends `failed`. |
| `cancelled` | Cancelled before it finished. |
| `terminated` | Stopped before it finished. |
| `timed_out` | Ran past its time limit. |

`running` is the only status in progress: every other status is final. A single-record job that ends `failed` returns its held credits, but [paid source](https://docs.sixtyfour.ai/guides/credits-and-pricing.md#paid-sources) tasks an Atlas run already started are still charged.

An Atlas run (`xhigh`, or `scout` with `graph`) that the agents decline still reports `completed`: its `notes` start with `Declined:`, `structured_data` repeats the input, and nothing is charged. Check `notes` before you store `structured_data`. See [Declined runs](https://docs.sixtyfour.ai/atlas/integrations.md#declined-runs).

## Results by job type

| Started by | `task_type` | `result` |
| - | - | - |
| `POST /people-intelligence-async` | `lead_enrichment` | `structured_data`, `notes`, `references`, and `confidence_score` or `field_confidence`. Atlas runs add research notes, the graph and workspace files. See [People Intelligence](https://docs.sixtyfour.ai/api-reference/endpoint/people-intelligence.md#response) and [Atlas](https://docs.sixtyfour.ai/atlas/integrations.md#response). |
| `POST /company-intelligence-async` | `company_enrichment` | `structured_data`, with the people found in `structured_data.leads`, plus `notes`, `references`, `findings`, `org_chart`, and `confidence_score` or `field_confidence`. See [Company Intelligence](https://docs.sixtyfour.ai/api-reference/endpoint/company-intelligence.md#response). |
| `POST /qa-agent-async` | `qa_agent` | `structured_data`, `notes` and `references`. See [QA Agent](https://docs.sixtyfour.ai/api-reference/endpoint/qa-agent.md#response). |
| `POST /find-email-async`, `/find-phone-async`, `/reverse-email-async`, `/reverse-phone-async` | `find_email`, `find_phone`, `reverse_enrichment` | The lead itself, with `cost_cents` and `cost_credits`: the object the sync endpoint returns. |
| `POST /find-email-bulk-async`, `/find-phone-bulk-async`, `/reverse-email-bulk-async`, `/reverse-phone-bulk-async` | `find_email`, `find_phone`, `reverse_enrichment` | `{ leads, total_leads, <count>, cost_cents, cost_credits }`: the object the sync bulk endpoint returns. The count is `emails_found`, `phones_found` or `successful_lookups`, and Find Email adds `mode`. |
| `POST /bulk-intelligence/people`, `/bulk-intelligence/company` | None | No `result`. A completed job carries file links in `results`. See [Bulk Intelligence](https://docs.sixtyfour.ai/api-reference/endpoint/bulk-intelligence.md#polling-for-results). |

Find Email reports `find_email`, Find Phone reports `find_phone`, and Reverse Email and Reverse Phone both report `reverse_enrichment`.

### Contact lookups in job status and webhooks

A single-lead contact job returns the lead itself in job status. Its [webhook](#webhooks) callback wraps the lead in `result.leads`, with the totals:

```json Job status theme={null}
{
  "status": "completed",
  "id": "3f8b2c1e-...",
  "run_id": "7d41a0c2-...",
  "start_time": "2026-11-02T19:30:12Z",
  "close_time": "2026-11-02T19:30:19Z",
  "result": {
    "name": "Saarth Shah",
    "company": "Sixtyfour",
    "email": [["saarth@sixtyfour.ai", "OK", "COMPANY"]],
    "other_emails": [],
    "cost_cents": 5,
    "cost_credits": 0.5
  },
  "charge_amount": 5,
  "charge_credits": 0.5,
  "task_type": "find_email"
}
```

```json Webhook theme={null}
{
  "task_id": "3f8b2c1e-...",
  "status": "completed",
  "task_type": "find_email",
  "result": {
    "leads": [
      {
        "name": "Saarth Shah",
        "company": "Sixtyfour",
        "email": [["saarth@sixtyfour.ai", "OK", "COMPANY"]],
        "other_emails": []
      }
    ],
    "total_leads": 1,
    "emails_found": 1,
    "cost_cents": 5,
    "cost_credits": 0.5,
    "mode": "PROFESSIONAL"
  },
  "error": null,
  "event_id": "f7c2a4e0-...",
  "attempt": 1
}
```

## Polling

* Poll each job every 10 seconds or slower. Job status is cached for up to 10 seconds: a new `status` shows on the next request, but other fields, such as bulk `progress`, refresh at most every 10 seconds.
* API keys and OAuth clients share one polling budget per organization: 300 requests a minute with a burst of 15, counted across `GET /job-status/{task_id}` and [workflow live status](https://docs.sixtyfour.ai/api-reference/workflows/workflows-execution.md#get-live-status). At one request per job every 10 seconds, the budget covers about 50 jobs at once. Signed-in sessions in the Sixtyfour app do not count against it. See [Rate Limits](https://docs.sixtyfour.ai/api-reference/rate-limits.md#status-polling).
* Past the budget, job status returns `429` with a `Retry-After` header and `X-RateLimit-Requests`, `X-RateLimit-Period` and `X-RateLimit-Burst` headers. A `503` with `Retry-After` means status is briefly unavailable. Either way the job keeps running: wait `Retry-After` seconds and poll again, without restarting the job.
* To follow many jobs, poll them from one loop that stays within the budget, or pass `webhook_url` and poll only the jobs whose callback is late.

This example waits for one job and honors `Retry-After`:

```bash cURL theme={null}
TASK_ID="3f8b2c1e-..."

# Requires jq.
while true; do
  code=$(curl -s -o job.json -D headers.txt -w "%{http_code}" \
    -H "x-api-key: YOUR_API_KEY" \
    "https://api.sixtyfour.ai/job-status/$TASK_ID")
  if [ "$code" = "429" ] || [ "$code" = "503" ]; then
    retry_after=$(awk 'tolower($1) == "retry-after:" { print $2 + 0 }' headers.txt)
    sleep "${retry_after:-10}"
  elif [ "$code" != "200" ] || [ "$(jq -r .status job.json)" != "running" ]; then
    break
  else
    sleep 10
  fi
done

cat job.json
```

```python Python theme={null}
import time
import requests

API = "https://api.sixtyfour.ai"
HEADERS = {"x-api-key": "YOUR_API_KEY"}


def wait_for_job(task_id, interval=10, timeout=2 * 60 * 60):
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        response = requests.get(f"{API}/job-status/{task_id}", headers=HEADERS, timeout=30)
        if response.status_code in (429, 503):
            # Over the polling budget, or status briefly unavailable: the job keeps running.
            time.sleep(float(response.headers.get("Retry-After", interval)))
            continue
        response.raise_for_status()
        job = response.json()
        if job["status"] != "running":
            return job
        time.sleep(interval)
    raise TimeoutError(f"Job {task_id} still running after {timeout} seconds")


job = wait_for_job("3f8b2c1e-...")
if job["status"] != "completed":
    raise RuntimeError(f"Job {job['status']}: {job.get('error', 'no reason given')}")
print(job["result"])  # bulk jobs carry file links in job["results"] instead
```

```javascript JavaScript theme={null}
const API = "https://api.sixtyfour.ai";
const headers = { "x-api-key": "YOUR_API_KEY" };
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function waitForJob(taskId, { interval = 10, timeout = 2 * 60 * 60 } = {}) {
  const deadline = Date.now() + timeout * 1000;
  while (Date.now() < deadline) {
    const response = await fetch(`${API}/job-status/${taskId}`, {
      headers,
      signal: AbortSignal.timeout(30_000),
    });
    if (response.status === 429 || response.status === 503) {
      // Over the polling budget, or status briefly unavailable: the job keeps running.
      await sleep(Number(response.headers.get("Retry-After") ?? interval));
      continue;
    }
    if (!response.ok) throw new Error(`Job status ${response.status}: ${await response.text()}`);
    const job = await response.json();
    if (job.status !== "running") return job;
    await sleep(interval);
  }
  throw new Error(`Job ${taskId} still running after ${timeout} seconds`);
}

const job = await waitForJob("3f8b2c1e-...");
if (job.status !== "completed") {
  throw new Error(`Job ${job.status}: ${job.error ?? "no reason given"}`);
}
console.log(job.result); // bulk jobs carry file links in job.results instead
```

## Webhooks

Pass `webhook_url` to receive the result by POST instead of polling. [Outgoing Webhooks](https://docs.sixtyfour.ai/api-reference/webhooks/outgoing.md) documents the payload, signing and retries. Callbacks relate to job status as follows:

* A callback is sent before the job closes, so job status can still read `running` briefly after the callback arrives.
* People Intelligence, Company Intelligence and QA Agent send the callback as soon as research finishes, before the job's charge is settled. The callback carries no charge: read `charge_credits` from job status once it reads `completed`. These three also send the callback from their sync endpoints when the request has a `webhook_url`.
* Contact lookups send the callback after the charge, so `result.cost_credits` is final. Their `result` differs from job status; see [Contact lookups in job status and webhooks](#contact-lookups-in-job-status-and-webhooks).
* Bulk Intelligence sends a workflow run payload whose `run_id` equals the `task_id`. See [Bulk Intelligence](https://docs.sixtyfour.ai/api-reference/endpoint/bulk-intelligence.md#webhook-callback).
* Only completed jobs send a callback. If none arrives by your deadline, poll job status.
* A job retried after its callback went out sends another, with a new `event_id`. Deduplicate callbacks on `task_id`.

## Sync calls and retries

> [!WARNING]
>
> A sync endpoint runs the same job and waits for it. If your client times out or disconnects, the job keeps running and is charged when it finishes, and a retry starts and charges a second job.

* Use the async endpoint for long research. A sync People Intelligence, Company Intelligence or QA Agent call that waits more than 90 seconds for capacity returns `503` and is not charged; the async job waits up to 24 hours.
* To make an async start safe to retry, send `dispatch_id` (8 to 128 characters) to `POST /people-intelligence-async` or `POST /qa-agent-async`. A repeat with the same `dispatch_id` in your organization returns the original job's `task_id` with `status: "RUNNING"`, and starts and charges nothing. This holds even after the original finished, for as long as it is [retained](#retention), and even when the body differs. Poll the `task_id` for the job's actual status. Keys share one namespace across People Intelligence, QA Agent and the [Atlas API](https://docs.sixtyfour.ai/atlas/api.md)'s start and continue routes, so a reused key returns the first job even when the request names another investigation.
* Company Intelligence and the contact lookups take no `dispatch_id`, so a repeated start runs and charges again. A `dispatch_id` sent to `POST /company-intelligence-async` comes back in `unrecognized_fields`.
* Bulk Intelligence takes an `Idempotency-Key` header. See [Bulk Intelligence](https://docs.sixtyfour.ai/api-reference/endpoint/bulk-intelligence.md#idempotency).

## Retention

Job status is not long-term storage: save each result when its job completes.

| Data | Available |
| - | - |
| A job in `GET /job-status/{task_id}` | For a limited time after it finishes, with no guaranteed window. After that the request returns `404`, as it does for an unknown ID. |
| `charge_credits` and `charge_amount` on bulk jobs | About 2 days after the job finishes, then omitted. |
| `download_url` links in bulk `results` | 15 minutes. Poll again for fresh links. |
| Bulk result files | With the run in the Sixtyfour app, at `results_url`. |

## Errors

For error responses (404, 429, 503, etc.), see [Handling Errors](https://docs.sixtyfour.ai/api-reference/errors.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.