# Quick start Source: https://docs.sixtyfour.ai/api-quick-start Authenticate, make your first request, and handle long-running jobs with the Sixtyfour API. Every request goes to `https://api.sixtyfour.ai` with your API key in the `x-api-key` header; [Authentication](/api-reference/authentication) covers OAuth tokens too. ## Get an API key **1. Create a key** Sign in to [app.sixtyfour.ai](https://app.sixtyfour.ai), open **Settings → API Keys**, and create a key. See [Get an API key](/get-api-key) for expiration and rotation. **2. Store it as an environment variable** ```bash theme={null} export SIXTYFOUR_API_KEY="YOUR_API_KEY" ``` ## Make your first request Find Email is a fast first call: give it a name and a company, and it returns a verified address. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/find-email" \ -H "x-api-key: $SIXTYFOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lead": { "name": "Saarth Shah", "company": "Sixtyfour AI" } }' ``` ```python Python theme={null} import os import requests response = requests.post( "https://api.sixtyfour.ai/find-email", headers={"x-api-key": os.environ["SIXTYFOUR_API_KEY"]}, json={"lead": {"name": "Saarth Shah", "company": "Sixtyfour AI"}}, ) response.raise_for_status() print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/find-email", { method: "POST", headers: { "x-api-key": process.env.SIXTYFOUR_API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ lead: { name: "Saarth Shah", company: "Sixtyfour AI" } }), }); console.log(await response.json()); ``` ```json Response theme={null} { "name": "Saarth Shah", "company": "Sixtyfour AI", "email": [["saarth@sixtyfour.ai", "OK", "COMPANY"]], "other_emails": [], "cost_cents": 5, "cost_credits": 0.5 } ``` Each entry in `email` is `[address, status, type]`. `OK` means the address was verified as deliverable; see [Find Email](/api-reference/endpoint/find-email#response) for the other statuses. ## Run long research asynchronously People Intelligence and Company Intelligence research the web and can take several minutes. In production, submit the job to the `-async` endpoint and poll `GET /job-status/{task_id}` instead of holding a connection open. `wait_for_job` is the polling helper from [Async jobs](/api-reference/async-jobs#polling). It polls every 10 seconds, waits for `Retry-After` on `429` and `503`, and returns once the status is anything other than `running`. See [Rate limits](/api-reference/rate-limits#status-polling). ```python Python theme={null} import os import time import requests API = "https://api.sixtyfour.ai" HEADERS = {"x-api-key": os.environ["SIXTYFOUR_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): # Rate-limited or unavailable: the job keeps running. Wait, then poll again. 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") task = requests.post( f"{API}/people-intelligence-async", headers=HEADERS, json={ "lead_info": {"name": "Saarth Shah", "company": "Sixtyfour AI"}, "struct": {"title": "Current job title", "linkedin": "LinkedIn profile URL"}, }, ) task.raise_for_status() job = wait_for_job(task.json()["task_id"]) if job["status"] != "completed": raise RuntimeError(f"Job {job['status']}: {job.get('error', 'no reason given')}") print(job["result"]["structured_data"]) ``` ```javascript JavaScript theme={null} const API = "https://api.sixtyfour.ai"; const headers = { "x-api-key": process.env.SIXTYFOUR_API_KEY, "Content-Type": "application/json", }; 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) { // Rate-limited or unavailable: the job keeps running. Wait, then poll again. 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 task = await fetch(`${API}/people-intelligence-async`, { method: "POST", headers, body: JSON.stringify({ lead_info: { name: "Saarth Shah", company: "Sixtyfour AI" }, struct: { title: "Current job title", linkedin: "LinkedIn profile URL" }, }), }); if (!task.ok) throw new Error(await task.text()); const job = await waitForJob((await task.json()).task_id); if (job.status !== "completed") { throw new Error(`Job ${job.status}: ${job.error ?? "no reason given"}`); } console.log(job.result.structured_data); ``` To get a callback instead of polling, pass `webhook_url` on the async request. See [Outgoing webhooks](/api-reference/webhooks/outgoing). ## Endpoints | Area | Endpoints | Use it for | | - | - | - | | [People Intelligence](/api-reference/endpoint/people-intelligence) | `POST /people-intelligence`, `-async`, `/bulk-intelligence/people` | Research a person and return the fields you define | | [Company Intelligence](/api-reference/endpoint/company-intelligence) | `POST /company-intelligence`, `-async`, `/bulk-intelligence/company` | Research a company and the people who work there | | [Contact](/api-reference/endpoint/find-email) | `POST /find-email`, `/find-phone`, `/verify-email`, `/reverse-email`, `/reverse-phone` | Find, verify, or reverse-look-up emails and phone numbers | | [Search](/api-reference/search/search-overview) | `POST /search/start-deep-search`, `/search/query`, `/search/lookalike` | Build lists of people or companies | | [Workflows](/api-reference/workflows/workflows-overview) | `POST /workflows/run`, `/schedules` | Run and schedule multi-step pipelines over a table | | [Monitors](/api-reference/monitors/monitors-overview) | `POST /monitors` | Track changes to people and companies over time | | [QA Agent](/api-reference/endpoint/qa-agent) | `POST /qa-agent` | Score or qualify records against your criteria | | [Age Verification](/api-reference/endpoint/age-verification) | `POST /verify-age` | Establish a person's date of birth or age from public sources, with evidence | | Account | `GET /check-balance` | Read your remaining credits | Request and response schemas for every endpoint are in the **API Reference** tab, generated from the live [OpenAPI spec](https://api.sixtyfour.ai/openapi.json). ## Errors and rate limits Errors return a standard HTTP status and a JSON body with `detail`, `code` and `correlation_id`. See [Errors](/api-reference/errors) for each status and when to retry, and [Rate limits](/api-reference/rate-limits) for every limit and the headers a `429` carries. # Check Balance Source: https://docs.sixtyfour.ai/api-reference/account/check-balance https://api.sixtyfour.ai/openapi.json get /check-balance Returns the current credit balance for the organization associated with the API key. # Async Jobs Source: https://docs.sixtyfour.ai/api-reference/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](/api-reference/endpoint/bulk-intelligence) 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}`](/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 | The ID of this execution of the job, 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 could not be confirmed, and on bulk jobs once it [expires](#retention). `charge_amount` is the [legacy field](/guides/credits-and-pricing#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](/api-reference/endpoint/bulk-intelligence#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](/atlas/integrations). | | `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; see `error`. A People Intelligence, Company Intelligence or QA Agent job that does not start within 24 hours also ends `failed`. | | `cancelled`, `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 `failed` single-record job is not charged, except for [paid source](/guides/credits-and-pricing#paid-sources) tasks an Atlas run already started. 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](/atlas/integrations#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](/api-reference/endpoint/people-intelligence#response) and [Atlas](/atlas/integrations#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](/api-reference/endpoint/company-intelligence#response). | | `POST /qa-agent-async` | `qa_agent` | `structured_data`, `notes` and `references`. See [QA Agent](/api-reference/endpoint/qa-agent#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, , 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](/api-reference/endpoint/bulk-intelligence#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; bulk `progress` updates at most every 10 seconds. * Job status shares one organization budget with [workflow live status](/api-reference/workflows/workflows-execution#get-live-status); see [Status polling](/api-reference/rate-limits#status-polling). * A `429` or `503` on a status read does not affect the job: wait `Retry-After` seconds and poll again. * 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](/api-reference/webhooks/outgoing) documents the payload, signing and retries. Callbacks relate to job status as follows: * Job status can read `running` briefly after the callback arrives. * People Intelligence, Company Intelligence and QA Agent callbacks carry no charge; read `charge_credits` from job status once it reads `completed`. Their sync endpoints also send the callback when the request has a `webhook_url`. * Contact lookup callbacks carry the final `result.cost_credits`. 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](/api-reference/endpoint/bulk-intelligence#webhook-callback). * Only completed jobs send a callback. If none arrives by your deadline, poll job status. * A job can send more than one callback, each with a new `event_id`; deduplicate 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: when Sixtyfour is at capacity, a sync call returns [`503`](/api-reference/errors#503-service-unavailable) and an async job waits. * Send `dispatch_id` (8–128 characters) to `POST /people-intelligence-async` or `POST /qa-agent-async` to make a start safe to retry. A repeat with the same key, whatever the body, returns the original `task_id` with `status: "RUNNING"` and charges nothing while the job is [retained](#retention); poll it for the real status. Keys are unique per organization across People Intelligence, QA Agent and the [Atlas API](/atlas/api). * 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](/api-reference/endpoint/bulk-intelligence#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 | 2 days after the job starts, 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](/api-reference/errors). # Agent event stream Source: https://docs.sixtyfour.ai/api-reference/atlas/agent-event-stream https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/events The agents' sessions and turn summaries, oldest first. With activity access, also includes tool events; when an event's `payload.storage_ref` is present, read its full payload from `GET …/events/{event_id}/payload`. For a live feed, use `GET …/events/stream`. # Attribution graph Source: https://docs.sixtyfour.ai/api-reference/atlas/attribution-graph https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/graph The attribution graph: `nodes` (findings with source URLs and quotes) and `edges` (confidence-weighted links). # Check Atlas access Source: https://docs.sixtyfour.ai/api-reference/atlas/check-atlas-access https://api.sixtyfour.ai/openapi.json get /v1/atlas/access Whether your organization can use Atlas, and the engines it can run with each engine's price in credits per unit. # Continue an investigation Source: https://docs.sixtyfour.ai/api-reference/atlas/continue-an-investigation https://api.sixtyfour.ai/openapi.json post /v1/atlas/investigations/{investigation_id}/runs Run again in an investigation you have, building on its graph and workspace. # Export the workspace as a ZIP Source: https://docs.sixtyfour.ai/api-reference/atlas/export-the-workspace-as-a-zip https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/export.zip Everything visible in the app, in one archive: files, attachments with `.json` metadata sidecars, the attribution graph, `investigation.json`, and — with agent-stream access — `agents/`. # Full agent event I/O Source: https://docs.sixtyfour.ai/api-reference/atlas/full-agent-event-io https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/events/{event_id}/payload One event's full arguments, result and reasoning. Use the event's `id` from `GET …/events` when `payload.storage_ref` is present. Requires activity access to the investigation. # Generate the intelligence report Source: https://docs.sixtyfour.ai/api-reference/atlas/generate-the-intelligence-report https://api.sixtyfour.ai/openapi.json post /v1/atlas/investigations/{investigation_id}/report Start writing a PDF report from the current findings, or join the build already running. An up-to-date report returns `completed` with `cached: true`. Poll `GET …/report` until it is `completed` and `follow_up` is `false`, then download `…/report.pdf`. # Get an investigation Source: https://docs.sixtyfour.ai/api-reference/atlas/get-an-investigation https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id} One investigation, in the shape `GET /v1/atlas/investigations` lists: its name, status, subject (`lead_info`), cost and, when another team shared it with yours, who shared it. # Intelligence report PDF Source: https://docs.sixtyfour.ai/api-reference/atlas/intelligence-report-pdf https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/report.pdf The latest report PDF. `X-Report-As-Of` is when its findings were read; `X-Report-Current` is `false` once the investigation has changed. Returns 202 with `Retry-After` while the first report, or a replacement, is being written, and 404 until a report is requested or after its build failed. `POST …/report` starts one. # List finding links Source: https://docs.sixtyfour.ai/api-reference/atlas/list-finding-links https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/links A bounded page of confidence-weighted links between findings, oldest first. # List findings Source: https://docs.sixtyfour.ai/api-reference/atlas/list-findings https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/findings A bounded page of attribution-graph findings, oldest first. # List investigations Source: https://docs.sixtyfour.ai/api-reference/atlas/list-investigations https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations Your team's Atlas investigations and the ones shared with it, newest first. `shared` is `null` for your team's own. Page with `next_cursor` (send it as `after`) until it is `null`; a page can be short before the end. # List workspace file metadata Source: https://docs.sixtyfour.ai/api-reference/atlas/list-workspace-file-metadata https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/file-entries A page of workspace file metadata, without file contents. # List workspace files Source: https://docs.sixtyfour.ai/api-reference/atlas/list-workspace-files https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/files Every file in the workspace. Attachments carry their metadata inline; fetch bytes via the file endpoint. # Live agent event stream Source: https://docs.sixtyfour.ai/api-reference/atlas/live-agent-event-stream https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/events/stream Server-Sent Events: the agents' turn summaries as they are written. Frames: - `sessions`: every agent session; sent first and on each change. - `event`: one turn summary; its `id:` is the resume cursor. - `status`: the investigation's status, on open and on each change. - `end`: the run finished and every event was sent. The `sessions` frame before it carries the run's cost, `null` until the charge is recorded. - `access_lost`: you can no longer see the investigation. The stream closes after about five minutes; reconnect with `Last-Event-ID` or `after` to resume. Comment lines keep the connection open. # Read a workspace file Source: https://docs.sixtyfour.ai/api-reference/atlas/read-a-workspace-file https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/files/{path} A workspace file: text as stored, or an attachment with its own content type. # Report generation status Source: https://docs.sixtyfour.ai/api-reference/atlas/report-generation-status https://api.sixtyfour.ai/openapi.json get /v1/atlas/investigations/{investigation_id}/report The most recent report job. 404 until generation has been requested. # Start an investigation Source: https://docs.sixtyfour.ai/api-reference/atlas/start-an-investigation https://api.sixtyfour.ai/openapi.json post /v1/atlas/investigations Start researching a subject in a new Atlas investigation. The investigation is returned at once, so its workspace and graph can be read while the run works. It belongs to your credential's team, which decides who else can see it. # Authentication Source: https://docs.sixtyfour.ai/api-reference/authentication Authenticate Sixtyfour API requests with an API key or an OAuth access token. Every request authenticates with an API key in the `x-api-key` header or an OAuth access token in the `Authorization` header. Both act for one team in your organization. | Credential | Format | Header | Get one | | - | - | - | - | | API key | Starts with `api_` | `x-api-key: YOUR_API_KEY` | [Get an API Key](/get-api-key) | | OAuth access token | Opaque string that starts with `s4at_` | `Authorization: Bearer YOUR_ACCESS_TOKEN` | [Using OAuth Clients](/guides/oauth-clients) | ## API keys ```bash cURL theme={null} curl "https://api.sixtyfour.ai/check-balance" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import os import requests response = requests.get( "https://api.sixtyfour.ai/check-balance", headers={"x-api-key": os.environ["SIXTYFOUR_API_KEY"]}, ) response.raise_for_status() print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/check-balance", { headers: { "x-api-key": process.env.SIXTYFOUR_API_KEY }, }); console.log(await response.json()); ``` ```json Response theme={null} { "credits": 1250.5 } ``` A key belongs to the team it was created in. Requests made with it see that team's workflows, runs and monitors, and count toward that team's monthly spend limit. ## OAuth access tokens An OAuth client exchanges a signed assertion for an access token at `POST /oauth/token`, then sends the token as a Bearer token: ```bash theme={null} curl "https://api.sixtyfour.ai/check-balance" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" ``` The token expires after `expires_in` seconds and works on every endpoint that accepts an API key. An OAuth client acts for your organization's default team, with the same team rules as a key. ## How the API picks a credential * A request with `Authorization: Bearer` is authenticated with that token. The API ignores `x-api-key` on that request, even when the token is invalid. * An API key sent as a Bearer token is refused with [`401`](/api-reference/errors#401-unauthorized). Send API keys only in `x-api-key`. * Send one credential per request. For every `401` message, see [401 Unauthorized](/api-reference/errors#401-unauthorized). ## Endpoints without credentials | Endpoint | Behavior | | - | - | | `GET /templates`, `GET /templates/{template_key}` | Public catalog. A credential you do send must be valid | | `GET /v1/media/{token}` | Media URLs from enrichment results. Organizations with customer-managed encryption must send their API key | | `GET /v1/media/profiles/{key}` | Profile photos from people search | | `POST /oauth/token` | Authenticates with the client assertion instead | | `GET /.well-known/oauth-authorization-server` | OAuth discovery document | Every other endpoint returns `401` without a valid credential. ## Revocation and expiry * A revoked key stops working within five minutes. * An expired key stops working at its expiry time. Sixtyfour emails your organization before longer-lived keys expire; see [Expiration](/get-api-key#expiration). * Archiving a team revokes its keys. * Disabling or deleting an OAuth client invalidates its tokens on their next request. When Sixtyfour cannot check a key, the request fails without a charge; retry it. See [Handling Errors](/api-reference/errors#503-service-unavailable). ## Request IDs Every response carries an `X-Correlation-ID` header: ```http theme={null} X-Correlation-ID: enrich_3f8b2c1e-7a4d-4e9b-8c21-5d6f0a9b1e47 ``` Error bodies repeat it as `correlation_id`. Log it with each request, and include it when you [contact support](/guides/getting-support). It is the **Request ID** column in [exported usage logs](/guides/usage-logs). # Age Verification Source: https://docs.sixtyfour.ai/api-reference/endpoint/age-verification Establish a person's date of birth or age from public sources, with confidence scores, evidence and source URLs. Age Verification researches public sources to establish a person's date of birth, or an age estimate grounded in evidence when the exact date isn't public, and returns the evidence and sources behind it. For when and how to use it in a trust-and-safety or KYC flow, see the [Age Verification guide](/guides/age-verification). ## Endpoint ```http theme={null} POST https://api.sixtyfour.ai/verify-age GET https://api.sixtyfour.ai/verify-age/{task_id} ``` > [card] [API Reference](/api-reference/intelligence/age-verification) > > See the full request/response schema and parameters in the API Reference. ## Request `POST /verify-age` takes one field. | Parameter | Type | Required | Description | | - | - | - | - | | `subject` | object | Yes | The person to verify: any identifiers you have, such as `name`, `company`, `linkedin`, `email`, `location`. Must not be empty. | `lead_info` and `input` are aliases for `subject`. Other keys, including `struct`, `tier` and `webhook_url`, are ignored; poll for the result. The response is the job handle: ```json theme={null} {"task_id": "7f3c9a2e-1b4d-4e8a-9c6f-2d5e8b1a0c37", "status": "RUNNING"} ``` ## Status `GET /verify-age/{task_id}` returns `{"task_id", "status"}` until the job finishes, and the full result once `status` is `completed`. | Status | Description | | - | - | | `running` | Research is in progress. | | `completed` | Research finished. The response includes the result fields below. | | `failed`, `cancelled`, `terminated`, `timed_out` | Ended without a result. | ## Response | Field | Type | Description | | - | - | - | | `task_id` | string | The job ID. | | `status` | string | `completed`. | | `date_of_birth.day` | integer or null | Day of birth, 1–31. | | `date_of_birth.month` | integer or null | Month of birth, 1–12. | | `date_of_birth.year` | integer or null | Year of birth. | | `date_of_birth.confidence` | integer | Confidence in the date, 0–100: the lowest score among the components that were established. `0` when none were. | | `age.value` | integer or null | Age in whole years. Exact when the date of birth is known, otherwise an estimate from the evidence. | | `age.as_of` | string | The date of this response (ISO 8601), not the date of the research: each poll returns the current date. `age.value` stays the age the research established. | | `age.confidence` | integer | Confidence in `age.value`, 0–100. | | `evidence` | string | How the date of birth or age was established. | | `sources` | array of strings | URLs of the pages the determination rests on. | Each date component is `null` unless a source establishes it, and `day` and `month` are always `null` when `year` is. `age.value` can be set while the date components are `null`. ```json theme={null} { "task_id": "7f3c9a2e-1b4d-4e8a-9c6f-2d5e8b1a0c37", "status": "completed", "date_of_birth": {"day": null, "month": 3, "year": 1991, "confidence": 82}, "age": {"value": 35, "as_of": "2026-10-09", "confidence": 80}, "evidence": "A 2016 conference bio lists her as 25 in March 2016; a public alumni record gives a 1991 birth month of March.", "sources": [ "https://example.com/conference-2016/speakers", "https://example.edu/alumni/jane-doe" ] } ``` ## Usage ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/verify-age" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "subject": { "name": "Jane Doe", "company": "Acme Corp", "linkedin": "https://www.linkedin.com/in/janedoe" } }' curl "https://api.sixtyfour.ai/verify-age/TASK_ID" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import time import requests BASE_URL = "https://api.sixtyfour.ai" headers = {"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"} start = requests.post( f"{BASE_URL}/verify-age", headers=headers, json={ "subject": { "name": "Jane Doe", "company": "Acme Corp", "linkedin": "https://www.linkedin.com/in/janedoe", } }, ) start.raise_for_status() task_id = start.json()["task_id"] while True: response = requests.get(f"{BASE_URL}/verify-age/{task_id}", headers=headers) if response.status_code in (429, 503): time.sleep(float(response.headers.get("Retry-After", 10))) continue response.raise_for_status() result = response.json() if result["status"] == "completed": print(result["date_of_birth"], result["age"]) break if result["status"] != "running": raise RuntimeError(f"Age verification {result['status']}") time.sleep(10) ``` ```javascript JavaScript theme={null} const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }; const start = await fetch(`${BASE_URL}/verify-age`, { method: "POST", headers, body: JSON.stringify({ subject: { name: "Jane Doe", company: "Acme Corp", linkedin: "https://www.linkedin.com/in/janedoe" } }) }); const { task_id } = await start.json(); while (true) { const response = await fetch(`${BASE_URL}/verify-age/${task_id}`, { headers }); if (response.status === 429 || response.status === 503) { const retryAfter = Number(response.headers.get("Retry-After") ?? 10); await new Promise((r) => setTimeout(r, retryAfter * 1000)); continue; } if (!response.ok) throw new Error(`Age verification status ${response.status}: ${await response.text()}`); const result = await response.json(); if (result.status === "completed") { console.log(result.date_of_birth, result.age); break; } if (result.status !== "running") { throw new Error(`Age verification ${result.status}`); } await new Promise((r) => setTimeout(r, 10000)); } ``` ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for more information. Age Verification needs no tier access: any organization with credits can call it. ## Errors For error responses (400, 403, 404, etc.), see [Handling Errors](/api-reference/errors). # Bulk Intelligence Source: https://docs.sixtyfour.ai/api-reference/endpoint/bulk-intelligence Upload a file of leads or companies and enrich every row in a single job. Upload a file of people or companies and get every row back enriched, without managing per-row API calls. Use it for list building, CRM backfills and large data refreshes. ## Endpoint Use the people endpoint for lead/person rows: ```http theme={null} POST https://api.sixtyfour.ai/bulk-intelligence/people ``` Use the company endpoint for company rows: ```http theme={null} POST https://api.sixtyfour.ai/bulk-intelligence/company ``` > [card] [People API Reference](/api-reference/enrichment/people-intelligence-bulk) > > Full request/response schema for `POST /bulk-intelligence/people`. > [card] [Company API Reference](/api-reference/enrichment/company-intelligence-bulk) > > Full request/response schema for `POST /bulk-intelligence/company`. ## Tiers Bulk jobs use the same tiers and access rules as the single-record endpoints: | Endpoint | Tiers | | - | - | | `/bulk-intelligence/people` | `micro`, `low` (default), `medium`, `high`, `scout`, `xhigh` — see [People Intelligence tiers](/api-reference/endpoint/people-intelligence#tiers) | | `/bulk-intelligence/company` | `micro`, `low` (default), `medium`, `high` — see [Company Intelligence tiers](/api-reference/endpoint/company-intelligence#tiers) | [Atlas](/atlas/integrations#bulk-intelligence) run options (`graph`, `max_credits`, `max_minutes`, `investigation_id`) are not accepted in `config`; each `xhigh` row runs one investigation on the default budget. ## Request format Both endpoints take `multipart/form-data` with two parts: | Part | Description | | - | - | | `file` | The records to enrich. Accepts `.csv`, `.json`, `.jsonl`, or `.ndjson`. | | `config` | A JSON-encoded string describing the enrichment — what to return for each row. | ### Limits | Limit | Value | | - | - | | File size | 100 MB | | Rows per file | 100,000 | | Config size | 1,000,000 characters | For the submission rate limit, see [Rate Limits](/api-reference/rate-limits#intelligence-and-contact). ## The `config` object `config` takes the same options as the single-record request, minus the input record itself (each row is the input), plus three bulk-only fields. Unknown keys are rejected. | Field | Type | Default | Description | | - | - | - | - | | `struct` | object | required | The fields to return for each row. | | `tier` | string | `low` | Research depth. See [Tiers](#tiers). | | `research_plan` | string (≤ 100,000 chars) | — | Plain-language guidance on where and how to look. | | `field_confidence` | boolean | off | Add per-field [confidence scoring](/api-reference/endpoint/field-confidence). | | `webhook_url` | string (HTTPS) | — | Receive the result files by POST when the job finishes. | | `find_people`, `people_focus_prompt` (≤ 10,000 chars), `lead_struct`, `full_org_chart` | — | — | Company endpoint only. Same meaning as on [Company Intelligence](/api-reference/endpoint/company-intelligence#request-parameters). | | `columns` | array of strings | all columns | Subset of uploaded columns to use as input. | | `result_formats` | array of strings | `["csv"]` | Output files to save. See below. | | `save_json` | boolean | `false` | Also save the results as a JSON file. | ```json theme={null} { "struct": { "email": "The individual's email address", "linkedin": "LinkedIn URL for the person" }, "tier": "low", "field_confidence": true, "result_formats": ["csv", "ndjson"], "webhook_url": "https://your-server.com/webhooks/sixtyfour" } ``` For struct value formats, supported types, and casting, see [Struct & Type Casting](/guides/struct-and-type-casting). `result_formats` lists the output files. CSV is always produced; add `"ndjson"` for typed values (numbers, booleans and nested objects). Each format is its own `results` entry; pick by `filename` extension. ## Submitting a job ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/bulk-intelligence/people" \ -H "x-api-key: YOUR_API_KEY" \ -F "file=@leads.csv" \ -F 'config={ "struct": { "email": "The individual'\''s email address", "linkedin": "LinkedIn URL for the person", "title": "The individual'\''s job title" } }' ``` ```python Python theme={null} import json import requests config = { "struct": { "email": "The individual's email address", "linkedin": "LinkedIn URL for the person", "title": "The individual's job title" } } with open("leads.csv", "rb") as f: response = requests.post( "https://api.sixtyfour.ai/bulk-intelligence/people", headers={"x-api-key": "YOUR_API_KEY"}, files={"file": ("leads.csv", f, "text/csv")}, data={"config": json.dumps(config)} ) response.raise_for_status() task_id = response.json()["task_id"] ``` ```javascript JavaScript theme={null} import { readFile } from "node:fs/promises"; const form = new FormData(); form.append("file", new Blob([await readFile("leads.csv")], { type: "text/csv" }), "leads.csv"); form.append("config", JSON.stringify({ struct: { email: "The individual's email address", linkedin: "LinkedIn URL for the person", title: "The individual's job title" } })); const response = await fetch("https://api.sixtyfour.ai/bulk-intelligence/people", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY" }, body: form }); if (!response.ok) throw new Error(`Submission failed: ${response.status}`); const { task_id } = await response.json(); ``` The response includes the job handle and the cost estimate: ```json theme={null} { "task_id": "9b1f9d2e-...", "status": "RUNNING", "row_count": 2500, "estimated_cost_cents": 25000, "estimated_cost_credits": 2500 } ``` ## Polling for results Poll `GET /job-status/{task_id}` until `status` is no longer `running`. A completed job carries `results` (signed links to the output files), `results_url` (the run in the Sixtyfour app) and `charge_credits`, and no inline `result`. See [Async Jobs](/api-reference/async-jobs) for statuses, polling and retention. The final enriched file is the `results` entry with the highest `block_number`. An entry for a file of at most 200 rows and 512 KB also holds the rows in its own `results` field. CSV downloads are escaped for spreadsheets by default, so a phone number `+14155550123` downloads as `'+14155550123`. Append `?spreadsheet_safe=false` to the `download_url` to get the file as stored, or read the NDJSON file. See [Spreadsheet-safe CSV](/api-reference/workflows/workflows-execution#spreadsheet-safe-csv). ### Tracking progress while running While `status` is `running`, a bulk response includes a `progress` object with the number of rows enriched so far out of the total submitted: ```json theme={null} { "status": "running", "id": "9b1f9d2e-...", "start_time": "2026-06-10T00:00:00Z", "progress": { "completed": 120, "total": 500 } } ``` > [!NOTE] > > Download links are signed URLs that expire after 15 minutes — poll again for fresh links — and require your `x-api-key` header when fetching. ```bash cURL theme={null} # Check status. For a polling loop that honors Retry-After, see /api-reference/async-jobs. curl "https://api.sixtyfour.ai/job-status/TASK_ID" \ -H "x-api-key: YOUR_API_KEY" # When status is "completed", download the result (no redirects — pass your key directly). # spreadsheet_safe=false returns the values as stored, without spreadsheet escaping. curl -o enriched.csv \ -H "x-api-key: YOUR_API_KEY" \ "SIGNED_DOWNLOAD_URL?spreadsheet_safe=false" ``` ```python Python theme={null} import requests import time deadline = time.monotonic() + 2 * 60 * 60 # give up after 2 hours while True: poll_resp = requests.get( f"https://api.sixtyfour.ai/job-status/{task_id}", headers={"x-api-key": "YOUR_API_KEY"} ) if poll_resp.status_code in (429, 503): # Over the polling budget, or status briefly unavailable: the job keeps running. time.sleep(float(poll_resp.headers.get("Retry-After", 30))) continue poll_resp.raise_for_status() status = poll_resp.json() if status["status"] == "completed": # The final stage has the highest block_number. If you requested NDJSON, # CSV and NDJSON share that block_number — pick by filename extension. final_block = max(f["block_number"] or 0 for f in status["results"]) suffix = ".csv" # use ".ndjson" if you requested NDJSON output final = next( (f for f in status["results"] if (f["block_number"] or 0) == final_block and f["filename"].endswith(suffix)), None, ) if final is None: raise RuntimeError(f"No {suffix} file in results") download = requests.get( final["download_url"], headers={"x-api-key": "YOUR_API_KEY"}, # Values as stored, without spreadsheet escaping. params={"spreadsheet_safe": "false"}, # Downloads are served directly by api.sixtyfour.ai; never # forward your API key across a redirect. allow_redirects=False ) if download.status_code != 200: raise RuntimeError(f"Download failed: {download.status_code}") with open(final["filename"], "wb") as out: out.write(download.content) break if status["status"] != "running": raise RuntimeError(f"Job {status['status']}") if time.monotonic() > deadline: raise TimeoutError("Job didn't complete within 2 hours") time.sleep(30) ``` ```javascript JavaScript theme={null} const deadline = Date.now() + 2 * 60 * 60 * 1000; // give up after 2 hours while (true) { const pollResponse = await fetch( `https://api.sixtyfour.ai/job-status/${task_id}`, { headers: { "x-api-key": "YOUR_API_KEY" } } ); if (pollResponse.status === 429 || pollResponse.status === 503) { // Over the polling budget, or status briefly unavailable: the job keeps running. const retryAfter = Number(pollResponse.headers.get("Retry-After") ?? 30); await new Promise(r => setTimeout(r, retryAfter * 1000)); continue; } if (!pollResponse.ok) throw new Error(`Status check failed: ${pollResponse.status}`); const status = await pollResponse.json(); if (status.status === "completed") { // The final stage has the highest block_number. If you requested NDJSON, // CSV and NDJSON share that block_number — pick by filename extension. const finalBlock = Math.max(...status.results.map(f => f.block_number ?? 0)); const suffix = ".csv"; // use ".ndjson" if you requested NDJSON output const final = status.results.find( f => (f.block_number ?? 0) === finalBlock && f.filename.endsWith(suffix) ); if (!final) throw new Error(`No ${suffix} file in results`); // Values as stored, without spreadsheet escaping. const downloadUrl = new URL(final.download_url); downloadUrl.searchParams.set("spreadsheet_safe", "false"); // Downloads are served directly by api.sixtyfour.ai; redirect: "error" // ensures your API key is never forwarded across a redirect. const data = await fetch(downloadUrl, { headers: { "x-api-key": "YOUR_API_KEY" }, redirect: "error" }); if (!data.ok) throw new Error(`Download failed: ${data.status}`); // write data to disk or stream it onward break; } if (status.status !== "running") { throw new Error(`Job ${status.status}`); } if (Date.now() > deadline) { throw new Error("Job didn't complete within 2 hours"); } await new Promise(r => setTimeout(r, 30000)); } ``` ## Webhook callback Pass `webhook_url` in the config to receive the result when the job completes. The callback's `run_id` is the job's `task_id`, and `results_download_url` links the enriched CSV for 15 minutes. See [Outgoing Webhooks](/api-reference/webhooks/outgoing#workflow-outgoing_webhook-block) for the payload. With `webhook_url`, the final `results` file adds three columns: `webhook_status_code`, `webhook_response` and `webhook_success`. > [!NOTE] > > Webhook URLs must use HTTPS and resolve to a public address. The URL is validated when you submit and again at delivery time. ## Idempotency Send an `Idempotency-Key` header (up to 255 characters) to make submissions retry-safe. The key is scoped to your organization and the endpoint, and kept for 24 hours. Resubmitting with the same key returns the original job instead of starting a new one, with `status` set to that job's current status in uppercase, such as `RUNNING` or `COMPLETED`: ```json theme={null} { "task_id": "9b1f9d2e-...", "status": "RUNNING", "already_started": true } ``` A replay omits `row_count` and the cost estimate. ## Failure behavior Rows that fail enrichment pass through un-enriched; one bad row does not fail the job. A file that cannot be parsed is rejected before anything runs. A job that fails as a whole reports `failed` with no `error`. ## Pricing The submit response carries `estimated_cost_credits`; a start your balance does not cover is refused with [`402`](/api-reference/errors#402-payment-required). See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs by tier. ## Errors For error responses (400, 402, 403, 422, etc.), see [Handling Errors](/api-reference/errors). # Company Intelligence Source: https://docs.sixtyfour.ai/api-reference/endpoint/company-intelligence Research a company, return the fields you ask for, and optionally find the people who work there. Turn a company name or domain into a complete, sourced company profile, with the right contacts attached, for account research, CRM enrichment, lead qualification or diligence. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/company-intelligence" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ --max-time 900 \ -d '{ "target_company": { "company_name": "Pacific View Studios", "website": "https://pacificview.studio" }, "struct": { "instagram_url": "The company'\''s primary Instagram profile URL", "headquarters": "City and country of the headquarters" }, "tier": "medium", "find_people": true, "people_focus_prompt": "The founder and the head of operations" }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/company-intelligence", headers={"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"}, json={ "target_company": { "company_name": "Pacific View Studios", "website": "https://pacificview.studio" }, "struct": { "instagram_url": "The company's primary Instagram profile URL", "headquarters": "City and country of the headquarters" }, "tier": "medium", "find_people": True, "people_focus_prompt": "The founder and the head of operations" }, timeout=900 ) response.raise_for_status() result = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/company-intelligence", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ target_company: { company_name: "Pacific View Studios", website: "https://pacificview.studio" }, struct: { instagram_url: "The company's primary Instagram profile URL", headquarters: "City and country of the headquarters" }, tier: "medium", find_people: true, people_focus_prompt: "The founder and the head of operations" }), signal: AbortSignal.timeout(900_000) }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const result = await response.json(); ``` ## Request parameters | Parameter | Type | Default | Description | | - | - | - | - | | `target_company` | object | required | What you already know about the company: `company_name`, `domain`, `website`, `linkedin`, `location`, or other keys. | | `struct` | object | required | The fields to return. See [Using the `struct` field](#using-the-struct-field). | | `tier` | string | `low` | Research depth. See [Tiers](#tiers). | | `research_plan` | string | — | Plain-language guidance on where and how to look. Up to 100,000 characters. | | `find_people` | boolean | `false` | Also find people at the company and return them in `structured_data.leads`. | | `people_focus_prompt` | string | — | Who to find when `find_people` is on, for example `"The VP of Marketing and the CTO"`. | | `lead_struct` | object | — | Fields to return for each person in `structured_data.leads`, in the same format as `struct`. | | `full_org_chart` | boolean | `false` | Return a department-level view of the company's people in `org_chart`. | | `field_confidence` | boolean | off | Return a confidence score, justification and sources for every field. See [Confidence Scoring](/api-reference/endpoint/field-confidence). | | `webhook_url` | string (HTTPS) | — | Receive the result by POST when the job finishes. Sync calls send it too. See [Outgoing Webhooks](/api-reference/webhooks/outgoing). | The async start response lists unrecognized body keys in `unrecognized_fields`. ## Tiers The `tier` parameter controls research depth and cost. | Tier | What it does | Access | | - | - | - | | `micro` | Lightweight lookup for very high volume. Up to 10 `struct` fields. | All orgs | | `low` (default) | Fast, low-cost firmographic enrichment. | All orgs | | `medium` | Deeper research: more sources, more fields filled. | All orgs | | `high` | Open-source intelligence (OSINT) investigation: deep, recursive research across the open web, public records, directories and specialist sources. Built for high-stakes accounts and diligence. | [On request](mailto:support@sixtyfour.ai) | Company Intelligence has no `scout` or `xhigh` tier. See [People Intelligence](/api-reference/endpoint/people-intelligence#tiers). To investigate a company in depth, use [Atlas](/atlas/overview). ## Using the `struct` field The `struct` field defines the data you want back. Each key becomes a field in `structured_data`, and its value tells the agent what to find. Pass a plain-English description or an object with `description` and `type`: ```json theme={null} { "struct": { "instagram_url": "The company's primary Instagram profile URL", "headquarters": {"description": "City and country of the headquarters", "type": "str"} } } ``` Specific descriptions return better results: `"The company's primary Instagram profile URL"` beats `"social"`. For supported types and casting, see [Struct & Type Casting](/guides/struct-and-type-casting). ## Choosing related parameters ### `find_people` vs `full_org_chart` * `find_people` finds specific people at the company. Pair it with `people_focus_prompt` to target roles or seniority, and with `lead_struct` to choose what is returned for each person. * `full_org_chart` returns a compact view of employees grouped by department, capped per department. It shows broad coverage of who works where; it is not a reporting tree or an exhaustive employee list. ### `research_plan` vs `people_focus_prompt` * `research_plan` is methodology: where to look, for example `"Check the About page and the LinkedIn company page"`. * `people_focus_prompt` is criteria: who to find, for example `"The VP of Marketing and the CTO"`. ## Response | Field | Type | Description | | - | - | - | | `structured_data` | object | One key per `struct` field, with the value found. It also echoes every key you sent in `target_company`, and holds `leads` when `find_people` is on. | | `structured_data.leads` | array | People found when `find_people` is on. With `lead_struct`, each lead has your fields. Without it, each lead has `name`, `email`, `company`, `title`, `phone`, `linkedin`, `location` and a `score` (0–10): how likely the person is to help or respond. | | `notes` | string | A short research summary. | | `references` | object | Source URL → what was found there. | | `confidence_score` | number (0–10) | Overall quality, consistency and correctness of the result. Omitted when the response carries `field_confidence`. | | `org_chart` | object or null | The department-level view when `full_org_chart` is on, otherwise `null`. | | `field_confidence` | object | Per-field confidence when `field_confidence` is on. If no field could be scored, the response carries `confidence_score` instead. See [Confidence Scoring](/api-reference/endpoint/field-confidence). | | `findings` | array of strings | Deprecated: a free-text log of what the research found. Read `structured_data` instead. | ## Async and timeouts Company Intelligence runs take up to 10 minutes. * Set sync client timeouts to at least 15 minutes. * Use `POST /company-intelligence-async` and poll `GET /job-status/{task_id}` in production. `POST /company-intelligence-async` takes the same body and returns a `task_id`. See [Async Jobs](/api-reference/async-jobs). > [!WARNING] > > A sync call keeps running after your client times out or disconnects, and it is charged when it finishes. A retry starts and charges a second run. Company Intelligence takes no `dispatch_id`, so a repeated async start also runs and charges again: keep the `task_id` from the first response. ## Bulk To enrich a file of companies in one job, use [Bulk Intelligence](/api-reference/endpoint/bulk-intelligence). ## Errors For error responses (400, 403, 422, etc.), see [Handling Errors](/api-reference/errors). ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs by tier. # Confidence Scoring Source: https://docs.sixtyfour.ai/api-reference/endpoint/field-confidence Get a 0–100 confidence score, a justification and source URLs for every enriched field. Field confidence tells you, field by field, how far to trust an enrichment result: each value gets a 0–100 score, a short justification and the URLs it was read on, in place of the single record-level `confidence_score`. Each score reflects how much assumption is needed to tie the value back to the target person or company. A high score means the evidence chain is strong; a low score means attribution is uncertain. ## How to enable Pass `field_confidence: true` in the request body. It works on: * [People Intelligence](/api-reference/endpoint/people-intelligence) (`/people-intelligence`, `/people-intelligence-async`) * [Company Intelligence](/api-reference/endpoint/company-intelligence) (`/company-intelligence`, `/company-intelligence-async`) * [Bulk Intelligence](/api-reference/endpoint/bulk-intelligence), in the `config` object No access request is needed. ```json theme={null} { "lead_info": {"full_name": "Jane Smith", "company": "Acme Corp"}, "struct": { "email": "Email address", "title": "Current job title", "linkedin": "LinkedIn profile URL" }, "tier": "medium", "field_confidence": true } ``` ## Response format The response carries a `field_confidence` object shaped like your `struct`: ```json theme={null} { "notes": "Research complete. Found Jane Smith on LinkedIn and Acme Corp website.", "structured_data": { "email": "jsmith@acmecorp.com", "title": "VP of Engineering", "linkedin": "https://www.linkedin.com/in/janesmith-acme/" }, "references": { "https://linkedin.com/in/janesmith-acme/": "LinkedIn profile" }, "field_confidence": { "email": { "confidence": 88, "justification": "Listed on the Acme Corp team page beside her title.", "sources": ["https://acmecorp.com/team"] }, "title": { "confidence": 96, "justification": "Her LinkedIn profile and the company site both give this title.", "sources": ["https://www.linkedin.com/in/janesmith-acme/", "https://acmecorp.com/team"] }, "linkedin": { "confidence": 98, "justification": "Profile name, employer and photo match the input.", "sources": ["https://www.linkedin.com/in/janesmith-acme/"] } } } ``` ### Response fields | Field | Type | Description | | - | - | - | | `field_confidence` | object | Confidence mirroring the struct shape. Present when `field_confidence: true` is passed and at least one field was scored; otherwise the response carries `confidence_score` instead. | | `field_confidence.{field}` | object or null | The field's confidence entry, described below. `null` when the field was not scored, such as a field with no value. | | `field_confidence.{field}.confidence` | integer (0–100) or null | How sure we are the value is right and belongs to this person or company. `null` when the value rests only on the input you sent. | | `field_confidence.{field}.justification` | string | Why the value holds, in a sentence or two. | | `field_confidence.{field}.sources` | array of URLs | The pages the value was read on. Empty when it came from a source with no public page. | | `field_confidence.{field}.nodes` | array of strings | [Atlas](/atlas/overview) runs only (`xhigh`, or `scout` with `graph`): the ids of the findings in the investigation's graph that the value rests on. | | `field_confidence.{object_field}` | object | Same keys as the struct object; each subfield gets its own entry. | | `field_confidence.{object_list_field}` | array | One object per item in `structured_data`; each subfield gets its own entry. | | `field_confidence.{list_field}` | object | A list of plain values (`list[str]`) gets one entry for the whole list. | > [!WARNING] > > `field_confidence` (0–100 per field) and `confidence_score` (0–10 per record) never appear together; read whichever the response has. ## Nested fields (objects and object\_lists) For nested structs, `field_confidence` mirrors the exact shape of `structured_data`: ```json theme={null} { "struct": { "email": "Email address", "company": { "name": "Company name", "size": "Company size" }, "jobs": [ {"title": "Job title", "company": "Company name", "start_date": "Start date"} ] } } ``` Response: ```json theme={null} { "structured_data": { "email": "jsmith@acmecorp.com", "company": {"name": "Acme Corp", "size": "500"}, "jobs": [ {"title": "VP of Engineering", "company": "Acme Corp", "start_date": "2020"}, {"title": "Senior Engineer", "company": "Initech", "start_date": "2015"} ] }, "field_confidence": { "email": {"confidence": 88, "justification": "Listed on the team page.", "sources": ["https://acmecorp.com/team"]}, "company": { "name": {"confidence": 96, "justification": "Named on her LinkedIn profile.", "sources": ["https://www.linkedin.com/in/janesmith-acme/"]}, "size": {"confidence": 45, "justification": "A data aggregator's range, not confirmed elsewhere.", "sources": []} }, "jobs": [ { "title": {"confidence": 95, "justification": "LinkedIn experience section.", "sources": ["https://www.linkedin.com/in/janesmith-acme/"]}, "company": {"confidence": 95, "justification": "LinkedIn experience section.", "sources": ["https://www.linkedin.com/in/janesmith-acme/"]}, "start_date": {"confidence": 60, "justification": "Year inferred from a 2020 announcement.", "sources": ["https://acmecorp.com/news/2020"]} }, { "title": {"confidence": 72, "justification": "An older conference bio.", "sources": ["https://example-conf.com/speakers"]}, "company": {"confidence": 72, "justification": "An older conference bio.", "sources": ["https://example-conf.com/speakers"]}, "start_date": null } ] } } ``` ## Workflow mode In workflows and bulk jobs, field confidence is returned as a single `enrichment_field_confidence` column containing a JSON string. The column parses as standard JSON while keeping the CSV flat: | name | email | enrichment\_field\_confidence | | - | - | - | | Jane Smith | [jsmith@acme.com](mailto:jsmith@acme.com) | `{"email": {"confidence": 88, "justification": "...", "sources": ["..."]}, ...}` | For nested structs, the JSON column contains the same nested structure as the API response above. ## Confidence tiers The 0–100 score maps to named confidence levels: | Range | Level | What it means | | - | - | - | | 85–100% | Confirmed | Near-zero assumption. Unique identifiers, direct assertions from authoritative sources. | | 65–84% | Established | Minimal assumption. A careful person would rely on this. One small residual uncertainty. | | 40–64% | Supported | Moderate assumption. The identity or value link is reasonable but not tight. | | 15–39% | Speculative | Significant assumption. Weak identifiers, multiple possible matches. Treat as hypothesis. | | 0–14% | Unreliable | Major assumption or conflicting evidence. Do not rely on. | ## Usage tips * Threshold by use case: 65 or higher for cold outreach, 85 or higher for legal and compliance work. * The weakest link dominates: if the target has a very common name, even well-sourced fields score lower because the identity link is uncertain. # Find Email Source: https://docs.sixtyfour.ai/api-reference/endpoint/find-email Find a verified work or personal email for a person. Send what you know about a person and get back a verified email address. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/find-email" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lead": { "name": "Saarth Shah", "company": "Sixtyfour" } }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/find-email", headers={"x-api-key": "YOUR_API_KEY"}, json={"lead": {"name": "Saarth Shah", "company": "Sixtyfour"}}, ) response.raise_for_status() lead = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/find-email", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ lead: { name: "Saarth Shah", company: "Sixtyfour" } }), }); if (!response.ok) throw new Error(await response.text()); const lead = await response.json(); ``` ```json Response theme={null} { "name": "Saarth Shah", "company": "Sixtyfour", "email": [["saarth@sixtyfour.ai", "OK", "COMPANY"]], "other_emails": [{ "email": "saarth.shah@sixtyfour.ai", "status": "RISKY" }], "cost_cents": 5, "cost_credits": 0.5 } ``` ## Request | Field | Type | Default | Description | | - | - | - | - | | `lead` | object | Required | The person. Send a LinkedIn URL for the best match, or a name with a company or domain. Other fields come back unchanged. | | `mode` | string | `PROFESSIONAL` | `PROFESSIONAL` finds a work address. `PERSONAL` finds a personal mailbox and returns it in `personal_email` and `personal_other_emails`. | | `verify_emails` | boolean | `true` | Check addresses with the mail server. With `false`, you get the first address found with status `UNKNOWN`. | | `webhook_url` | string | — | Async only. HTTPS URL that receives the result. See [Outgoing Webhooks](/api-reference/webhooks/outgoing). | Lead field names are matched loosely: `linkedin` or `linkedin_url`, `name` or `first_name` and `last_name`, `company` or `company_name`, `domain` or `website`. ## Response `email` holds one `[address, status, type]` tuple: the first deliverable address, or the best remaining one if none is deliverable. Other candidates are in `other_emails`; addresses that fail verification are left out. When nothing is found, `email` is absent. | Status | Meaning | | - | - | | `OK` | The mail server accepted the address. | | `RISKY` | It can receive mail, but delivery is uncertain, for example on a domain that accepts every address. | | `UNKNOWN` | Verification was inconclusive, or the address was not checked. | Type is `COMPANY` or `PERSONAL`. `cost_credits` is what the request charged; `cost_cents` is the [deprecated field](/guides/credits-and-pricing#costs-in-api-responses). > [!NOTE] > > Find Email always searches, even if the lead already has an `email`. It replaces your address when it finds one; when it finds nothing, your address comes back unchecked with status `UNKNOWN`. To check a specific address, use [Verify Email](/api-reference/endpoint/verify-email). ## Async and bulk | Endpoint | Body | Returns | | - | - | - | | `POST /find-email` | `lead` | The lead | | `POST /find-email-async` | `lead` | A `task_id` | | `POST /find-email-bulk` | `leads`, 1 to 100 | `{ leads, total_leads, emails_found, cost_cents, cost_credits, mode }` | | `POST /find-email-bulk-async` | `leads`, 1 to 100 | A `task_id` | Poll async jobs with `GET /job-status/{task_id}`. For the result shape in job status and webhooks, see [Async Jobs](/api-reference/async-jobs#contact-lookups-in-job-status-and-webhooks). See [Credits & Pricing](/guides/credits-and-pricing) for costs. ## Errors For error responses (400, 403, 422, etc.), see [Handling Errors](/api-reference/errors). # Find Phone Source: https://docs.sixtyfour.ai/api-reference/endpoint/find-phone Find a phone number for a person. Send what you know about a person and get back a phone number. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/find-phone" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lead": { "name": "Jane Doe", "linkedin": "https://www.linkedin.com/in/janedoe" } }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/find-phone", headers={"x-api-key": "YOUR_API_KEY"}, json={"lead": {"name": "Jane Doe", "linkedin": "https://www.linkedin.com/in/janedoe"}}, ) response.raise_for_status() lead = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/find-phone", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ lead: { name: "Jane Doe", linkedin: "https://www.linkedin.com/in/janedoe" } }), }); if (!response.ok) throw new Error(await response.text()); const lead = await response.json(); ``` ```json Response theme={null} { "name": "Jane Doe", "linkedin": "https://www.linkedin.com/in/janedoe", "phone": "+14155550123", "cost_cents": 30, "cost_credits": 3 } ``` ## Request | Field | Type | Description | | - | - | - | | `lead` | object | Required. The person. Send a LinkedIn URL for the best match, or a name with a company. Other fields come back unchanged. | | `webhook_url` | string | Async only. HTTPS URL that receives the result. See [Outgoing Webhooks](/api-reference/webhooks/outgoing). | Lead field names are matched loosely: `linkedin` or `linkedin_url`, `name` or `first_name` and `last_name`, `company` or `company_name`, `domain` or `website`. ## Response `phone` is the number found, or an empty string when none was found. Numbers are not verified. `cost_credits` is what the request charged; `cost_cents` is the [deprecated field](/guides/credits-and-pricing#costs-in-api-responses). ## Async and bulk | Endpoint | Body | Returns | | - | - | - | | `POST /find-phone` | `lead` | The lead | | `POST /find-phone-async` | `lead` | A `task_id` | | `POST /find-phone-bulk` | `leads`, 1 to 100 | `{ leads, total_leads, phones_found, cost_cents, cost_credits }` | | `POST /find-phone-bulk-async` | `leads`, 1 to 100 | A `task_id` | Poll async jobs with `GET /job-status/{task_id}`. For the result shape in job status and webhooks, see [Async Jobs](/api-reference/async-jobs#contact-lookups-in-job-status-and-webhooks). See [Credits & Pricing](/guides/credits-and-pricing) for costs. ## Errors For error responses (400, 403, 422, etc.), see [Handling Errors](/api-reference/errors). # People Intelligence Source: https://docs.sixtyfour.ai/api-reference/endpoint/people-intelligence Research a person and return the fields you ask for, with sources and a confidence score. Turn a partial lead record (a name, a company, a LinkedIn URL) into a complete, sourced profile for sales outreach, CRM enrichment, lead qualification or diligence. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/people-intelligence" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ --max-time 900 \ -d '{ "lead_info": { "name": "Saarth Shah", "company": "Sixtyfour", "linkedin": "https://www.linkedin.com/in/saarthshah" }, "struct": { "name": "The individual'\''s full name", "title": "The individual'\''s current job title", "company": "The company the individual works at", "linkedin": "LinkedIn URL for the person", "website": "Company website URL" }, "tier": "medium" }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/people-intelligence", headers={"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"}, json={ "lead_info": { "name": "Saarth Shah", "company": "Sixtyfour", "linkedin": "https://www.linkedin.com/in/saarthshah" }, "struct": { "name": "The individual's full name", "title": "The individual's current job title", "company": "The company the individual works at", "linkedin": "LinkedIn URL for the person", "website": "Company website URL" }, "tier": "medium" }, timeout=900 ) response.raise_for_status() result = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/people-intelligence", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ lead_info: { name: "Saarth Shah", company: "Sixtyfour", linkedin: "https://www.linkedin.com/in/saarthshah" }, struct: { name: "The individual's full name", title: "The individual's current job title", company: "The company the individual works at", linkedin: "LinkedIn URL for the person", website: "Company website URL" }, tier: "medium" }), signal: AbortSignal.timeout(900_000) }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const result = await response.json(); ``` ## Request parameters | Parameter | Type | Default | Description | | - | - | - | - | | `lead_info` | object | required | What you already know about the person: any combination of `name`, `title`, `company`, `location`, `linkedin`, `email`, and other keys. | | `struct` | object | required (optional on Atlas runs) | The fields to return. See [Using the `struct` field](#using-the-struct-field). | | `tier` | string | `low` | Research depth. See [Tiers](#tiers). | | `research_plan` | string | — | Plain-language guidance on where and how to look. Up to 100,000 characters. | | `field_confidence` | boolean | off | Return a confidence score, justification and sources for every field. See [Confidence Scoring](/api-reference/endpoint/field-confidence). | | `webhook_url` | string (HTTPS) | — | Receive the result by POST when the job finishes. Sync calls send it too. See [Outgoing Webhooks](/api-reference/webhooks/outgoing). | | `dispatch_id` | string (8–128 chars) | — | Async only. Idempotency key: a repeat returns the first job and charges nothing. See [Async Jobs](/api-reference/async-jobs#sync-calls-and-retries). | | `private` | boolean | `false` | Show the run's result, status and usage log only to you, or to the key's creator for an API key. Your team must allow private runs. Atlas runs need the async endpoint and a new investigation. | Atlas runs (`xhigh`, or `scout` with `graph: true`) accept [more parameters](/atlas/integrations#request-parameters). The async start response lists unrecognized body keys in `unrecognized_fields`. ## Tiers The `tier` parameter controls research depth and cost. | Tier | What it does | Access | | - | - | - | | `micro` | Lightweight lookup for very high volume. Up to 10 `struct` fields. | All orgs | | `low` (default) | Fast, low-cost enrichment. Good for name → email or LinkedIn mapping at scale. | All orgs | | `medium` | More sources and research iterations. Better for hard-to-find contacts. | All orgs | | `high` | Open-source intelligence (OSINT) investigation: deep, recursive research across the open web, public records, directories and specialist sources. Built for the hardest prospects and for diligence. | [On request](mailto:support@sixtyfour.ai) | | `scout` | The full `high` research toolset on a lighter, lower-cost model. Add `graph: true` to run it on [Atlas](/atlas/overview). | [On request](mailto:support@sixtyfour.ai) | | `xhigh` | Structured [Atlas](#atlas-xhigh) run: a team of research agents builds a persistent investigation with an attribution graph you can continue later, and returns your `struct` fields. | [On request](mailto:support@sixtyfour.ai) | ## Using the `struct` field The `struct` field defines the data you want back. Each key becomes a field in `structured_data`, and its value tells the agent what to find. Pass a plain-English description or an object with `description` and `type`: ```json theme={null} { "struct": { "email": "The individual's primary work email", "github_url": {"description": "URL for their GitHub profile", "type": "str"} } } ``` Specific descriptions return better results: `"The individual's primary work email"` beats `"email"`. For supported types and casting, see [Struct & Type Casting](/guides/struct-and-type-casting). ### Image fields On the `high` and `xhigh` tiers, a field that asks for an image (a headshot, a logo, a screenshot of a page) comes back as a permanent image URL hosted by Sixtyfour: ```json theme={null} {"struct": {"headshot": "A recent photo of the person"}} ``` ```json theme={null} {"structured_data": {"headshot": "https://api.sixtyfour.ai/v1/media/v1.eyJt..."}} ``` The URL needs no credentials and stays valid while your data is retained, so it embeds in an ``, CRM record or spreadsheet cell. The source page is listed in `references`. Organizations with customer-managed encryption fetch media from a server with their `x-api-key` header, so their URLs cannot be embedded. Page screenshots show the source URL and capture time (UTC) in a header above the page. For a printable version, ask for a PDF in the field description, for example `"Printable PDF capture of the source page"`. It comes back as the same kind of media URL, served as a US Letter PDF with the URL, capture time and page number on every page. ## Response | Field | Type | Description | | - | - | - | | `structured_data` | object | One key per `struct` field, with the value found. | | `notes` | string | A short research summary. | | `references` | object | Source URL → what was found there. | | `confidence_score` | number (0–10) | Overall quality, consistency and correctness of the result. Omitted when the response carries `field_confidence`. | | `field_confidence` | object | Per-field confidence when `field_confidence` is on. If no field could be scored, the response carries `confidence_score` instead. See [Confidence Scoring](/api-reference/endpoint/field-confidence). | ```json theme={null} { "structured_data": { "name": "Saarth Shah", "title": "CEO & Co-Founder", "company": "Sixtyfour", "linkedin": "https://www.linkedin.com/in/saarthshah", "website": "https://sixtyfour.ai" }, "notes": "Confirmed current role from the LinkedIn profile and the company site.", "references": { "https://www.linkedin.com/in/saarthshah": "LinkedIn profile with current title", "https://sixtyfour.ai": "Company website" }, "confidence_score": 9 } ``` ## Atlas (xhigh) > [!NOTE] > > `xhigh` is a structured version of [Atlas](/atlas/overview), Sixtyfour's deep investigation product. It runs the same agents and creates the same persistent investigation, then returns your `struct` fields in `structured_data`. See [People Intelligence on Atlas](/atlas/integrations#people-intelligence) for its parameters and response. ## Async and timeouts People Intelligence runs take up to 10 minutes; Atlas runs take longer. * Set sync client timeouts to at least 15 minutes. * Use `POST /people-intelligence-async` and poll `GET /job-status/{task_id}` in production. `POST /people-intelligence-async` takes the same body plus `dispatch_id` and returns a `task_id`. See [Async Jobs](/api-reference/async-jobs). > [!WARNING] > > A sync call keeps running after your client times out or disconnects, and it is charged when it finishes. A retry starts and charges a second run. To retry safely, use the async endpoint with `dispatch_id`. ## Bulk To enrich a file of leads in one job, use [Bulk Intelligence](/api-reference/endpoint/bulk-intelligence). ## LinkedIn profile enrichment `POST /enrich-linkedin` reads one person's LinkedIn profile and extracts the fields you ask for in a single pass, without researching other sources. It is sync only. | Parameter | Type | Default | Description | | - | - | - | - | | `linkedin_url` | string | required | The person's LinkedIn profile URL. | | `struct` | object | see description | The fields to extract, in the same format as [`struct`](#using-the-struct-field). Without it: `full_name`, `headline`, `summary`, `current_company`, `current_title`, `location`, `email`, `phone`, `skills`, `experience_summary`, `education_summary` and `connections`. | ```json Request theme={null} { "linkedin_url": "https://www.linkedin.com/in/saarthshah", "struct": { "full_name": "Full name of the person", "current_title": "Current job title", "current_company": "Current employer" } } ``` ```json Response theme={null} { "linkedin_url": "https://www.linkedin.com/in/saarthshah", "structured_data": { "full_name": "Saarth Shah", "current_title": "CEO & Co-Founder", "current_company": "Sixtyfour", "linkedin_url": "https://www.linkedin.com/in/saarthshah" }, "confidence_score": 9, "notes": "Current role taken from the profile's experience section.", "elapsed_seconds": 4.2, "profile_found": true } ``` | Field | Type | Description | | - | - | - | | `linkedin_url` | string | The URL you sent. | | `structured_data` | object | One key per `struct` field, plus `linkedin_url`. Values are empty strings when nothing could be extracted. | | `confidence_score` | integer (0–10) | Overall confidence in the extracted values. | | `notes` | string | A short summary, or why nothing was extracted. | | `elapsed_seconds` | number | How long the call took. | | `profile_found` | boolean | `false` when the profile could not be retrieved. | See [Credits & Pricing Guide](/guides/credits-and-pricing). ## Errors For error responses (400, 403, 409, 422, etc.), see [Handling Errors](/api-reference/errors). ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs by tier. # QA Agent Source: https://docs.sixtyfour.ai/api-reference/endpoint/qa-agent Research a record against your qualification criteria and get back a written assessment plus the fields you ask for. > [!NOTE] > > QA Agent is in beta. The request and response format can change. QA Agent researches a record (a lead, a seller, a company) against criteria you write in plain English and returns an assessment in `notes` plus the fields you define in `struct`, such as a score or a YES/NO verdict. Use it for lead qualification, marketplace vetting, and CRM hygiene. ## Endpoint ```http theme={null} POST https://api.sixtyfour.ai/qa-agent POST https://api.sixtyfour.ai/qa-agent-async ``` > [card] [API Reference](/api-reference/enrichment/qa-agent-sync) > > See the full request/response schema and parameters in the API Reference. ## Request | Parameter | Type | Required | Description | | - | - | - | - | | `data` | object | Yes | The record to evaluate. Any keys and values. Must not be empty. | | `qualification_criteria` | array | Yes | The criteria to evaluate against. Must contain at least one item. | | `struct` | object | No | Fields to return in `structured_data`, as `field_name: "description"` pairs. | | `references` | array | No | Pages the agent reads before it starts researching. | | `max_tool_calls` | integer | No | Maximum research actions per evaluation, 1–50. Default `10`. | | `webhook_url` | string | No | HTTPS URL that receives the result when the job completes. Sync calls send it too. See [Outgoing Webhooks](/api-reference/webhooks/outgoing). | | `dispatch_id` | string (8–128 chars) | No | Async only. Idempotency key: a repeat returns the first job and charges nothing. See [Async Jobs](/api-reference/async-jobs#sync-calls-and-retries). | ### `qualification_criteria` | Field | Type | Required | Description | | - | - | - | - | | `criteria_name` | string | Yes | Short name for the criterion. | | `description` | string | Yes | What qualifies a record, in plain English. | | `weight` | number | No | Relative importance of this criterion. Default `1.0`. | | `threshold` | number | No | The minimum the record must meet on this criterion. It has no fixed scale; give the scale in `description`. | The agent reads every criterion, including its weight and threshold, as guidance for its research and assessment. QA Agent does not compute a score itself: to get one, ask for it in `struct` and state the scale you want (for example `"Weighted qualification score from 0 to 10"`). ### `references` Each reference is an object with a required `url` and an optional `description` of what the page shows. ```json theme={null} { "references": [ {"url": "https://www.linkedin.com/company/sixtyfourai", "description": "Company profile"} ] } ``` ### `struct` Each key in `struct` becomes a field in `structured_data`, and its value is a plain-English description of what to put there. Only the fields you list are returned. Specific descriptions work best: `"Overall qualification score from 0 to 10, weighted by criterion"` beats `"score"`. `struct` values must be strings; typed objects such as `{"type": "int"}` are rejected. Every returned value is a string, and an unfilled field is an empty string. Without a `struct`, `structured_data` contains a single `general_findings` field. ## Response | Field | Type | Description | | - | - | - | | `structured_data` | object | One string value per `struct` field. | | `notes` | string | The agent's assessment: findings, how the record measures up against each criterion, and gaps in the evidence. | | `references` | object | Always empty; sources are cited in `notes`. | ```json theme={null} { "notes": "Alex Johnson runs an online store selling handmade ceramics and ships from their own studio. Physical Inventory: met; product photos and shipping policy show stock held on site...", "structured_data": { "suitability_score": "9", "final_verdict": "YES" }, "references": {} } ``` ## Sync usage ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/qa-agent" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": { "name": "Alex Johnson", "country": "United Kingdom", "business_type": "e-commerce seller", "website": "https://example.com" }, "qualification_criteria": [ { "criteria_name": "Physical Inventory", "description": "Must own and store physical products", "weight": 10.0 } ], "struct": { "suitability_score": "Overall qualification score from 0 to 10", "final_verdict": "YES or NO" } }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/qa-agent", headers={"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"}, json={ "data": { "name": "Alex Johnson", "country": "United Kingdom", "business_type": "e-commerce seller", "website": "https://example.com", }, "qualification_criteria": [ { "criteria_name": "Physical Inventory", "description": "Must own and store physical products", "weight": 10.0, } ], "struct": { "suitability_score": "Overall qualification score from 0 to 10", "final_verdict": "YES or NO", }, }, ) response.raise_for_status() result = response.json() print(result["structured_data"]["final_verdict"]) ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/qa-agent", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ data: { name: "Alex Johnson", country: "United Kingdom", business_type: "e-commerce seller", website: "https://example.com" }, qualification_criteria: [ { criteria_name: "Physical Inventory", description: "Must own and store physical products", weight: 10.0 } ], struct: { suitability_score: "Overall qualification score from 0 to 10", final_verdict: "YES or NO" } }) }); const result = await response.json(); console.log(result.structured_data.final_verdict); ``` ## Async pattern Use `/qa-agent-async` for batches and anything that runs inside a request timeout. 1. Submit: `POST /qa-agent-async` with the same body as the sync endpoint. The response is `{"task_id": "...", "status": "RUNNING"}`. 2. Poll: `GET /job-status/{task_id}` every 10 seconds until `status` is no longer `running`. 3. Read: when `completed`, the response's `result` holds the same object the sync endpoint returns. Pass `webhook_url` to receive the result instead of polling. [Async Jobs](/api-reference/async-jobs) covers statuses, the polling budget and retries. > [!WARNING] > > A sync call keeps running after your client times out or disconnects, and it is charged when it finishes. A retry starts and charges a second run. To retry safely, use `/qa-agent-async` with `dispatch_id`. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/qa-agent-async" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "data": {"name": "Alex Johnson", "business_type": "e-commerce seller"}, "qualification_criteria": [ {"criteria_name": "Physical Inventory", "description": "Must own and store physical products"} ], "struct": {"final_verdict": "YES or NO"} }' curl "https://api.sixtyfour.ai/job-status/TASK_ID" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import time import requests BASE_URL = "https://api.sixtyfour.ai" headers = {"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"} start = requests.post( f"{BASE_URL}/qa-agent-async", headers=headers, json={ "data": {"name": "Alex Johnson", "business_type": "e-commerce seller"}, "qualification_criteria": [ {"criteria_name": "Physical Inventory", "description": "Must own and store physical products"} ], "struct": {"final_verdict": "YES or NO"}, }, ) start.raise_for_status() task_id = start.json()["task_id"] while True: response = requests.get(f"{BASE_URL}/job-status/{task_id}", headers=headers) if response.status_code in (429, 503): time.sleep(float(response.headers.get("Retry-After", 10))) continue response.raise_for_status() job = response.json() if job["status"] == "completed": print(job["result"]["structured_data"]) break if job["status"] != "running": raise RuntimeError(f"QA Agent job {job['status']}") time.sleep(10) ``` ```javascript JavaScript theme={null} const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }; const start = await fetch(`${BASE_URL}/qa-agent-async`, { method: "POST", headers, body: JSON.stringify({ data: { name: "Alex Johnson", business_type: "e-commerce seller" }, qualification_criteria: [ { criteria_name: "Physical Inventory", description: "Must own and store physical products" } ], struct: { final_verdict: "YES or NO" } }) }); const { task_id } = await start.json(); while (true) { const response = await fetch(`${BASE_URL}/job-status/${task_id}`, { headers }); if (response.status === 429 || response.status === 503) { const retryAfter = Number(response.headers.get("Retry-After") ?? 10); await new Promise((r) => setTimeout(r, retryAfter * 1000)); continue; } if (!response.ok) throw new Error(`Job status ${response.status}: ${await response.text()}`); const job = await response.json(); if (job.status === "completed") { console.log(job.result.structured_data); break; } if (job.status !== "running") { throw new Error(`QA Agent job ${job.status}`); } await new Promise((r) => setTimeout(r, 10000)); } ``` ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for more information. ## Errors For error responses (400, 403, etc.), see [Handling Errors](/api-reference/errors). # Reverse Email Source: https://docs.sixtyfour.ai/api-reference/endpoint/reverse-email Find the person behind an email address. Send an email address and get back the person behind it: name, title, employer and LinkedIn profile. Use it to enrich inbound signups or deduplicate CRM records. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/reverse-email" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lead": { "email": "saarth@sixtyfour.ai" } }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/reverse-email", headers={"x-api-key": "YOUR_API_KEY"}, json={"lead": {"email": "saarth@sixtyfour.ai"}}, ) response.raise_for_status() lead = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/reverse-email", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ lead: { email: "saarth@sixtyfour.ai" } }), }); if (!response.ok) throw new Error(await response.text()); const lead = await response.json(); ``` ```json Response theme={null} { "email": "saarth@sixtyfour.ai", "reverse_email_error": null, "reverse_firstname": "Saarth", "reverse_lastname": "Shah", "reverse_title": "CEO & Co-Founder", "reverse_company_name": "Sixtyfour", "reverse_linkedin_url": "https://www.linkedin.com/in/saarthshah", "cost_cents": 5, "cost_credits": 0.5 } ``` ## Request | Field | Type | Description | | - | - | - | | `lead` | object | Required. The lead to look up, with an `email`. Other fields come back unchanged. | | `webhook_url` | string | Async only. HTTPS URL that receives the result. See [Outgoing Webhooks](/api-reference/webhooks/outgoing). | ## Response The lead comes back with the fields you sent plus every `reverse_*` field below. Fields with no match are `null`. `cost_credits` is what the request charged; `cost_cents` is the [deprecated field](/guides/credits-and-pricing#costs-in-api-responses). | Field | Description | | - | - | | `reverse_email_error` | `No data found` when no person matched, otherwise `null`. | | `reverse_firstname`, `reverse_lastname` | Name. | | `reverse_title`, `reverse_headline`, `reverse_summary`, `reverse_position_description` | Current role and profile text. | | `reverse_linkedin_url`, `reverse_linkedin_id` | LinkedIn profile. | | `reverse_location` | Where the person is based. | | `reverse_email_searched`, `reverse_domain` | The address looked up and its domain. | | `reverse_company_*` | Employer: `name`, `domain`, `website`, `linkedin_url`, `description`, `industry`, `headquarters`, `headcount`, `headcount_range`, `year_founded`. | ## Async and bulk | Endpoint | Body | Returns | | - | - | - | | `POST /reverse-email` | `lead` | The lead | | `POST /reverse-email-async` | `lead` | A `task_id` | | `POST /reverse-email-bulk` | `leads`, 1 to 100 | `{ leads, total_leads, successful_lookups, cost_cents, cost_credits }` | | `POST /reverse-email-bulk-async` | `leads`, 1 to 100 | A `task_id` | Poll async jobs with `GET /job-status/{task_id}`. For the result shape in job status and webhooks, see [Async Jobs](/api-reference/async-jobs#contact-lookups-in-job-status-and-webhooks). See [Credits & Pricing](/guides/credits-and-pricing) for costs. ## Errors For error responses (400, 403, 422, etc.), see [Handling Errors](/api-reference/errors). # Reverse Phone Source: https://docs.sixtyfour.ai/api-reference/endpoint/reverse-phone Find the person behind a phone number. Send a phone number and get back the person behind it: name, location, email, employer and LinkedIn profile. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/reverse-phone" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lead": { "phone": "+14155550123" } }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/reverse-phone", headers={"x-api-key": "YOUR_API_KEY"}, json={"lead": {"phone": "+14155550123"}}, ) response.raise_for_status() lead = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/reverse-phone", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ lead: { phone: "+14155550123" } }), }); if (!response.ok) throw new Error(await response.text()); const lead = await response.json(); ``` ```json Response theme={null} { "phone": "+14155550123", "reverse_phone_error": null, "reverse_fullname": "Jane Doe", "reverse_location": "San Francisco, CA", "reverse_email": "jane.doe@example.com", "reverse_title": "Head of Sales", "reverse_company_name": "Example Corp", "reverse_linkedin_url": "https://www.linkedin.com/in/janedoe", "cost_cents": 30, "cost_credits": 3 } ``` ## Request | Field | Type | Description | | - | - | - | | `lead` | object | Required. The lead to look up, with a `phone`. Other fields come back unchanged. | | `webhook_url` | string | Async only. HTTPS URL that receives the result. See [Outgoing Webhooks](/api-reference/webhooks/outgoing). | ## Response The lead comes back with the fields you sent plus every `reverse_*` field below. Fields with no match are `null`. `cost_credits` is what the request charged; `cost_cents` is the [deprecated field](/guides/credits-and-pricing#costs-in-api-responses). | Field | Description | | - | - | | `reverse_phone_error` | `No data found` when no person matched, otherwise `null`. | | `reverse_fullname`, `reverse_firstname`, `reverse_lastname` | Name. | | `reverse_age`, `reverse_location`, `reverse_address` | Details from public records. | | `reverse_relatives`, `reverse_associates` | People linked to the number in public records. | | `reverse_email` | An email address for the person. | | `reverse_linkedin_url`, `reverse_title` | LinkedIn profile and role. | | `reverse_company_name`, `reverse_domain` | Employer and its domain. | | `reverse_phone_searched` | The number looked up. | ## Async and bulk | Endpoint | Body | Returns | | - | - | - | | `POST /reverse-phone` | `lead` | The lead | | `POST /reverse-phone-async` | `lead` | A `task_id` | | `POST /reverse-phone-bulk` | `leads`, 1 to 100 | `{ leads, total_leads, successful_lookups, cost_cents, cost_credits }` | | `POST /reverse-phone-bulk-async` | `leads`, 1 to 100 | A `task_id` | Poll async jobs with `GET /job-status/{task_id}`. For the result shape in job status and webhooks, see [Async Jobs](/api-reference/async-jobs#contact-lookups-in-job-status-and-webhooks). See [Credits & Pricing](/guides/credits-and-pricing) for costs. ## Errors For error responses (400, 403, 422, etc.), see [Handling Errors](/api-reference/errors). # Verify Email Source: https://docs.sixtyfour.ai/api-reference/endpoint/verify-email Check whether one email address can receive mail. Check a specific address you already have, such as an email typed into a signup or booking form, before you rely on it. The response carries the verdict. > [!NOTE] > > `/verify-email` checks the exact address you send. To discover an address for a person, use [Find Email](/api-reference/endpoint/find-email). ## Endpoint ```http theme={null} POST https://api.sixtyfour.ai/verify-email ``` > [card] [API Reference](/api-reference/enrichment/verify-email) > > See the full request/response schema and parameters in the API Reference. ## Request | Field | Type | Description | | - | - | - | | `email` | string | The address to check. Required. | ## Response | Field | Type | Description | | - | - | - | | `email` | string | The address that was checked. | | `status` | string | `OK`, `RISKY`, `INVALID`, or `UNKNOWN`. See [Statuses](#statuses). | | `score` | integer or null | 0 to 100. Higher is more likely to be delivered. | ### Statuses | Status | Meaning | | - | - | | `OK` | The mail server accepted the address. | | `RISKY` | The address can receive mail, but delivery is uncertain, for example on a domain that accepts every address. | | `INVALID` | The address or its domain does not exist, or the server rejected it. | | `UNKNOWN` | The mail server did not give a clear answer in time. | ## Usage ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/verify-email" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "jane@gmial.com"}' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/verify-email", headers={ "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, json={"email": "jane@gmial.com"} ) response.raise_for_status() result = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/verify-email", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ email: "jane@gmial.com" }) }); if (!response.ok) { throw new Error(`Verification failed (${response.status}): ${await response.text()}`); } const result = await response.json(); console.log(result); ``` ```json Response theme={null} { "email": "jane@gmial.com", "status": "INVALID", "score": 0 } ``` ## Verify many addresses To check a list of addresses, use the `verify_email` block in a [workflow](/api-reference/workflows/workflow-blocks). ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs. ## Errors For error responses (400, 403, 422, 429, 503, etc.), see [Handling Errors](/api-reference/errors). For this endpoint's limit, see [Rate Limits](/api-reference/rate-limits). # Company Intelligence (Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/company-intelligence-async https://api.sixtyfour.ai/openapi.json post /company-intelligence-async Find and enrich a single company; returns a job ID to poll. # Company Intelligence (Bulk) Source: https://docs.sixtyfour.ai/api-reference/enrichment/company-intelligence-bulk https://api.sixtyfour.ai/openapi.json post /bulk-intelligence/company Upload a CSV, JSON, JSONL or NDJSON file and enrich every row in the background. Poll `GET /job-status/{task_id}`: `progress` counts rows while it runs, and a completed job carries `results` (download links), `results_url` and `charge_credits`. A row that fails comes back un-enriched. # Company Intelligence (Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/company-intelligence-sync https://api.sixtyfour.ai/openapi.json post /company-intelligence Find and enrich a single company. # Enrich LinkedIn Profile Source: https://docs.sixtyfour.ai/api-reference/enrichment/enrich-linkedin-profile https://api.sixtyfour.ai/openapi.json post /enrich-linkedin Read a LinkedIn profile and return the fields you name in `struct`. # Find Email (Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/find-email-async https://api.sixtyfour.ai/openapi.json post /find-email-async Find a single person's email; returns a job ID to poll. # Find Email (Bulk, Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/find-email-bulk-async https://api.sixtyfour.ai/openapi.json post /find-email-bulk-async Find email addresses for a batch of leads; returns a job ID to poll. # Find Email (Bulk, Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/find-email-bulk-sync https://api.sixtyfour.ai/openapi.json post /find-email-bulk Find email addresses for a batch of leads in one call. # Find Email (Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/find-email-sync https://api.sixtyfour.ai/openapi.json post /find-email Find a single person's email address. # Find Phone (Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/find-phone-async https://api.sixtyfour.ai/openapi.json post /find-phone-async Find a single person's phone number; returns a job ID to poll. # Find Phone (Bulk, Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/find-phone-bulk-async https://api.sixtyfour.ai/openapi.json post /find-phone-bulk-async Find phone numbers for a batch of leads; returns a job ID to poll. # Find Phone (Bulk, Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/find-phone-bulk-sync https://api.sixtyfour.ai/openapi.json post /find-phone-bulk Find phone numbers for a batch of leads in one call. # Find Phone (Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/find-phone-sync https://api.sixtyfour.ai/openapi.json post /find-phone Find a single person's phone number. # Job Status Source: https://docs.sixtyfour.ai/api-reference/enrichment/job-status https://api.sixtyfour.ai/openapi.json get /job-status/{job_id} Poll an async job and read its result once it completes. See [Rate Limits](/api-reference/rate-limits#status-polling) for the polling budget. # People Intelligence (Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/people-intelligence-async https://api.sixtyfour.ai/openapi.json post /people-intelligence-async Find and enrich a single person; returns a job ID to poll. # People Intelligence (Bulk) Source: https://docs.sixtyfour.ai/api-reference/enrichment/people-intelligence-bulk https://api.sixtyfour.ai/openapi.json post /bulk-intelligence/people Upload a CSV, JSON, JSONL or NDJSON file and enrich every row in the background. Poll `GET /job-status/{task_id}`: `progress` counts rows while it runs, and a completed job carries `results` (download links), `results_url` and `charge_credits`. A row that fails comes back un-enriched. # People Intelligence (Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/people-intelligence-sync https://api.sixtyfour.ai/openapi.json post /people-intelligence Find and enrich a single person. # QA Agent (Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/qa-agent-async https://api.sixtyfour.ai/openapi.json post /qa-agent-async Research a record against your qualification criteria; returns a job ID to poll. The finished job's `result` matches `POST /qa-agent`. # QA Agent (Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/qa-agent-sync https://api.sixtyfour.ai/openapi.json post /qa-agent Research a record (a lead, a seller, a company) against your qualification criteria. Returns the agent's assessment in `notes` and the fields you define in `struct`, such as a score or a YES/NO verdict, in `structured_data`. # Reverse Email Lookup (Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/reverse-email-lookup-async https://api.sixtyfour.ai/openapi.json post /reverse-email-async Resolve a person from their email address; returns a job ID to poll. # Reverse Email Lookup (Bulk, Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/reverse-email-lookup-bulk-async https://api.sixtyfour.ai/openapi.json post /reverse-email-bulk-async Resolve people from a batch of email addresses; returns a job ID to poll. # Reverse Email Lookup (Bulk, Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/reverse-email-lookup-bulk-sync https://api.sixtyfour.ai/openapi.json post /reverse-email-bulk Resolve people from a batch of email addresses in one call. # Reverse Email Lookup (Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/reverse-email-lookup-sync https://api.sixtyfour.ai/openapi.json post /reverse-email Resolve a person from their email address. # Reverse Phone Lookup (Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/reverse-phone-lookup-async https://api.sixtyfour.ai/openapi.json post /reverse-phone-async Resolve a person from their phone number; returns a job ID to poll. # Reverse Phone Lookup (Bulk, Async) Source: https://docs.sixtyfour.ai/api-reference/enrichment/reverse-phone-lookup-bulk-async https://api.sixtyfour.ai/openapi.json post /reverse-phone-bulk-async Resolve people from a batch of phone numbers; returns a job ID to poll. # Reverse Phone Lookup (Bulk, Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/reverse-phone-lookup-bulk-sync https://api.sixtyfour.ai/openapi.json post /reverse-phone-bulk Resolve people from a batch of phone numbers in one call. # Reverse Phone Lookup (Sync) Source: https://docs.sixtyfour.ai/api-reference/enrichment/reverse-phone-lookup-sync https://api.sixtyfour.ai/openapi.json post /reverse-phone Resolve a person from their phone number. # Verify Email Source: https://docs.sixtyfour.ai/api-reference/enrichment/verify-email https://api.sixtyfour.ai/openapi.json post /verify-email Check whether one email address can receive mail. Responds in under a second for most addresses and within about two seconds at most; UNKNOWN results are not charged. # Handling Errors Source: https://docs.sixtyfour.ai/api-reference/errors Error codes, response shapes, and when to retry for the Sixtyfour API. Every Sixtyfour API error returns an HTTP status code and a JSON body. `POST /oauth/token` is the exception: it uses the [OAuth error format](#oauth-token-errors). Branch on `code`, not on the text of `detail`. ## Error body ```json theme={null} { "detail": "Insufficient API credits", "code": "payment_required", "correlation_id": "enrich_3f8b2c1e-9d4a-4c1b-8e2f-6a7b5c4d3e21" } ``` | Field | Description | | - | - | | `detail` | What went wrong. See [Error Response Format](#error-response-format). | | `code` | Stable, for programs. When `detail` is an object with its own `code`, this is that value. Otherwise it follows the status. | | `correlation_id` | Equals the `X-Correlation-ID` response header. Quote it to support. | | Status | `code` | | - | - | | 400 | `bad_request` | | 401 | `unauthenticated` | | 402 | `payment_required` | | 403 | `forbidden` | | 404 | `not_found` | | 405 | `method_not_allowed` | | 409 | `conflict` | | 410 | `gone` | | 413 | `payload_too_large` | | 422 | `invalid_request` | | 429 | `rate_limited` | | 500 | `internal_error` | | 502 | `bad_gateway` | | 503 | `unavailable` | | 504 | `timeout` | Examples on this page show only `detail`. ## Error Response Format `detail` is a string, a list of field errors, or an object. ### String Most errors carry a string `detail`: ```json theme={null} { "detail": "Insufficient API credits" } ``` ### Field errors Request validation errors (`422`) carry a `detail` array with one entry per invalid field. `loc` is the path to the field. `ctx`, when present, holds the rule the value broke, such as `{"max_length": 100000}` for a `research_plan` that is too long. ```json theme={null} { "detail": [ { "type": "missing", "loc": ["body", "lead_info"], "msg": "Field required", "input": {"struct": {"title": "Current job title"}} } ] } ``` ### Object Workflow errors carry an object with a `code` and a `message`: ```json theme={null} { "detail": { "code": "workflow_revision_required", "message": "Read the workflow and supply its edit_revision as expected_revision." } } ``` Other objects: | Response | `detail` | | - | - | | `404` from `GET /job-status/{job_id}` | `{"message": "Job not found", "job_id": "..."}` | | `404` from `GET /verify-age/{task_id}` | `{"message": "Job not found", "task_id": "..."}` | | `404` from `GET /search/status/{task_id}` or `GET /search/lookalike/{task_id}` for an unknown `task_id` | `{"message": "Task not found", "task_id": "..."}` | | `400` for an invalid workflow definition | `{"message": "Workflow validation failed", "validation_errors": [...]}` | | Errors from `POST /search/filter-field-values` | `{"code": "...", "detail": "...", "retryable": true}`. See [Filter field values errors](#filter-field-values-errors) | ### OAuth token errors `POST /oauth/token` returns errors in the OAuth format, `error` and `error_description`, without `detail`, `code` or `correlation_id`: ```json theme={null} { "error": "invalid_client", "error_description": "client_assertion aud must match OAuth issuer" } ``` | `error` | Status | Cause | | - | - | - | | `invalid_request` | 400 | The request is malformed: an unsupported `client_assertion_type`, or an assertion without integer `iat` and `exp` claims or without `jti` | | `invalid_client` | 401 | The client is unknown, disabled, or deleted, or the assertion is malformed, not signed with `RS256` by a registered key, missing `kid`, has the wrong `iss`, `sub`, or `aud`, lasts more than 300 seconds, or is issued in the future | | `invalid_grant` | 400 | The assertion has expired, or its `jti` was already used | | `invalid_scope` | 400 | A scope other than `full_access` | | `unsupported_grant_type` | 400 | A grant type other than `client_credentials` | | `invalid_target` | 400 | A `resource` other than the Sixtyfour API, more than one `resource`, or an `audience` parameter | A `429` from `POST /oauth/token` uses the `detail` format with `Retry-After`. See [Rate Limits](/api-reference/rate-limits#oauth-tokens). ## HTTP Status Codes | Status Code | Meaning | Description | | - | - | - | | 200 | OK | Request succeeded | | 202 | Accepted | Async job or monitor accepted; poll for the result | | 400 | Bad Request | Invalid request body, missing required fields, unsupported tier, or malformed data | | 401 | Unauthorized | Missing or invalid API key or OAuth token | | 402 | Payment Required | Insufficient credits, no active subscription, or the team's monthly spend limit is reached | | 403 | Forbidden | Tier or feature not enabled for your organization or team, or organization access is locked | | 404 | Not Found | Resource not found (e.g., job, workflow, run, monitor, or investigation ID), or it belongs to another team | | 409 | Conflict | The resource is not in a state that allows the request, or an identical request is still being processed | | 413 | Content Too Large | A file, workflow definition, or export is past its size limit | | 422 | Validation Error | Invalid request parameters; see the `detail` array for field-level errors | | 428 | Precondition Required | An existing workflow content update omitted `expected_revision` | | 429 | Too Many Requests | Rate limit exceeded; retry after the `Retry-After` header. See [Rate Limits](/api-reference/rate-limits) | | 500 | Internal Server Error | Server error; retry with backoff | | 502 | Bad Gateway | An upstream source failed; retry after a brief wait | | 503 | Service Unavailable | Temporarily unavailable or at capacity; retry after the `Retry-After` header. Requests rejected before execution are not charged | | 504 | Gateway Timeout | A search request timed out; retry after a brief wait | ## Common Error Types ### 400 Bad Request Causes: * Missing required fields in request body * Invalid field types or formats * Malformed JSON payload * Empty or invalid lead/company data * Company Intelligence with `"tier": "scout"`. Scout is a People Intelligence tier * A workflow definition that fails validation, such as one with no blocks, incompatible blocks, or broken edges. The response is `Workflow validation failed`, with one `validation_errors` entry per problem * A workflow definition nested more than 64 levels deep, with `detail.code` `workflow_definition_too_deep` * Sending `webhook_payload` to `/workflows/run` for a workflow whose source block is not `webhook` * Triggering a `read_csv` workflow without a valid `specs_override.resource_handle_id` from `/storage/csv/upload`, or with a `handle_id` that belongs to a different organization * `webhook_payload` is missing required columns from the `webhook` block's `input_schema`, or is malformed JSON * `webhook_payload` rows with top-level keys that start with `_`: `Invalid input payload: Webhook input contains reserved top-level keys starting with '_' ...` * Running a `webhook` workflow without `webhook_payload` when it has no saved sample input: `Webhook input is required but not provided` * A `/storage/csv/upload` file with column names that start with `_`, other than `_row_id`: `Column names must not start with an underscore: ...` * Invalid search exclusions: sending the deprecated `exclude_public_ids` in company mode (people-only) or alongside `exclude_entity_ids`, using a saved exclusion list whose entity type doesn't match the search mode or that is not yet `ready`, or sending exclusions on a cursor request — see [Search Exclusions](/api-reference/search/search-exclusions) * A Bulk Intelligence `webhook_url` that is not `https://`. Async-job endpoints return `422` for the same URL * A `webhook_url` query parameter on `POST /workflows/run`. Add an `outgoing_webhook` block instead * An invalid cron expression or timezone on a schedule: `Invalid schedule — check the cron expression and timezone.` * Invalid monitor requests. See [Monitors](#monitors) Examples: Company Intelligence with `"tier": "scout"`: ```json theme={null} { "detail": "Unsupported enrichment tier 'scout'." } ``` A workflow with no blocks: ```json theme={null} { "detail": { "message": "Workflow validation failed", "validation_errors": [ { "field": "blocks", "code": "EMPTY_BLOCKS", "message": "Workflow must have at least one block", "context": {"summary": "The workflow definition is malformed"}, "blocking": true } ] } } ``` Each save checks the schema and edges, block type compatibility, required block fields, and every column a block references against what its upstream blocks produce. Each entry in `validation_errors`: | Field | Meaning | | - | - | | `field` | Path to the value, such as `blocks[1].specs.email_column`. Block indexes count along `edges` from the source block, `blocks[0]`, or follow request order when the edges are invalid. Use `context.block_id` to find the block. | | `code` | Stable, such as `COLUMN_NOT_FOUND`, `MISSING_REQUIRED_FIELD`, `INVALID_FIELD_NAME`, `INVALID_SPECS`, `TYPE_INCOMPATIBLE` or `MULTIPLE_SOURCE_BLOCKS`. | | `message` | What is wrong. | | `blocking` | Always `true` on a `400`. | | `context.block_id`, `context.block_name` | The block. `block_id` is your ID if it was a UUID, otherwise the one assigned. Absent when the finding concerns the whole graph. | | `context.missing_columns`, `context.available_columns` | What you referenced and what exists at that point. At most 50 available names; `available_column_count` has the total when truncated. | | `context.suggestion` | A hint for people, not programs. | | `context.suggested_replacement` | A fragment to merge into the block's `specs`. | | `context.summary` | The finding in one plain line. | How to fix: * Fix each field named in `detail` and resend * For workflow runs, match the request body to the source block: `webhook_payload` for `webhook` workflows ([Incoming Webhooks](/api-reference/webhooks/incoming)) or `specs_override.resource_handle_id` for `read_csv` workflows ([Workflow Execution](/api-reference/workflows/workflows-execution)). ### 401 Unauthorized Causes: * Missing credentials * Invalid, revoked, or expired API key * Invalid or expired OAuth bearer token * An API key sent as `Authorization: Bearer` The `detail` text varies by endpoint; branch on the status or `code`. | `detail` | Cause | | - | - | | `Unauthorized` or `Authentication required: provide Bearer token or X-API-Key header` | No credentials | | `Unauthorized` or `Invalid API key` | An invalid, revoked, or expired API key | | `Invalid bearer token` or `Invalid token` | An `Authorization: Bearer` value that is not an OAuth access token, such as an API key | | `OAuth access token has expired` | The OAuth token is past its `expires_in` | | `OAuth access token has been revoked` | The OAuth token was revoked | | `OAuth client is disabled`, `OAuth client not found` | The OAuth client was disabled or deleted | | `Invalid OAuth access token` | The token was altered or did not come from `POST /oauth/token` | Example: ```json theme={null} { "detail": "Unauthorized" } ``` How to fix: * Ensure the `x-api-key` header is included in every request, with no extra whitespace, or send an OAuth token as `Authorization: Bearer`. See [Authentication](/api-reference/authentication) * Send API keys only in `x-api-key`; see [How the API picks a credential](/api-reference/authentication#how-the-api-picks-a-credential) * Check that your API key hasn't been revoked or expired * Request a new OAuth token before the old one expires * Get a new API key from the [dashboard](https://app.sixtyfour.ai) ### 402 Payment Required Causes: * Your balance doesn't cover the request's estimated cost. See [Credits & Pricing](/guides/credits-and-pricing) * Your organization has no active subscription * Your team has reached its monthly spend limit, including when you create a monitor or change its tier Examples: ```json theme={null} { "detail": "Insufficient API credits" } ``` ```json theme={null} { "detail": "This team has reached its monthly spend limit. Ask an organization admin to raise it." } ``` When automatic top-ups are on and one has been requested, a workflow run or Bulk Intelligence start returns a `402` with `Retry-After: 30`. Retry after the delay; once the top-up is paid, the start goes through. ```json theme={null} { "detail": "Insufficient API credits to start this run; a top-up has been requested." } ``` If the balance stops covering a run after it is accepted, an async job ends `failed` with `error` set to `Insufficient API credits`, and a sync request returns `500`. How to fix: * Add credits or upgrade your plan in the [billing section](https://app.sixtyfour.ai/billing) before resubmitting * Reduce the row count or use a cheaper tier to lower the estimated cost. See [Credits & Pricing](/guides/credits-and-pricing) * For a team spend limit, ask an organization admin to raise it Nothing is charged when a request returns 402. ### 403 Forbidden Causes: * Requesting a tier that is not enabled for your organization: `high`, `scout`, or `xhigh` (`micro`, `low`, and `medium` are available to every organization) * Requesting a tier your organization has but your team is excluded from. The response is the same `403` an organization without the tier gets * Calling `/v1/atlas/*` when Atlas is not enabled for your organization * Calling any `/monitors` endpoint when Monitors are not enabled for your organization * Creating or updating a monitor with the `5m` frequency or `micro` tier when they are not enabled for your organization * Your organization's access to Sixtyfour has been locked Examples: ```json theme={null} { "detail": "High tier company enrichment is not enabled for this org. Contact sales to request access." } ``` ```json theme={null} { "detail": "Scout tier lead enrichment is not enabled for this org. Contact sales to request access." } ``` ```json theme={null} { "detail": "This tier is not enabled for your organization." } ``` ```json theme={null} { "detail": "Monitors are not enabled for your organization." } ``` ```json theme={null} { "detail": "This organization no longer has access to Sixtyfour. Contact your representative at Sixtyfour." } ``` How to fix: * Contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai) to request access to a restricted tier or feature * Use a tier your organization has access to, such as `low` or `medium` ### 404 Not Found Causes: * Workflow ID does not exist or was deleted * Run or job ID does not exist, has expired, or is invalid * Resource does not belong to your organization * The workflow or run belongs to another team. API keys and OAuth clients see only their own team's workflows and runs * Monitor or row ID does not exist, or the monitor belongs to another team * A monitor `handle_id` that does not exist or belongs to another team: `Upload handle not found.` * `GET /monitors/for-run/{workflow_run_id}/{block_id}` for a step that started no monitor: `No monitor was started by that step.` * `GET /v1/atlas/investigations/{investigation_id}/report.pdf` before a report was requested, or after the last build failed. Send `POST /v1/atlas/investigations/{investigation_id}/report`, with `retry_failed: true` after a failure. It is also `404` once nothing reportable is left in the investigation Examples: ```json theme={null} { "detail": "Workflow not found" } ``` `GET /job-status/{job_id}` returns an object: ```json theme={null} { "detail": { "message": "Job not found", "job_id": "3f8b2c1e-7a4d-4e9b-8c21-5d6f0a9b1e47" } } ``` How to fix: * Verify the workflow ID using `GET /workflows` * Verify the job\_id from your run response * Ensure you're accessing resources created by your organization, with a key or OAuth client from the team that owns them ### 409 Conflict (Monitors) Causes: * Requesting `GET /monitors/{monitor_id}/runs/{run_id}/changes` when the run is `running` or `failed`. Changes are available only for `completed` runs; check the status with [List monitor runs](/api-reference/monitors/monitors-endpoints#list-monitor-runs). * Triggering a monitor that is cancelled or paused * Updating a cancelled monitor or switching one of its rows on or off * Reusing a monitor idempotency key with different creation settings or file contents * Triggering a monitor, or changing its `frequency` or `is_active`, while its `status` is `starting` or `failed`, before every row from an uploaded file is added * Sending a create request while an identical one, with the same idempotency key, is still being processed Examples: ```json theme={null} { "detail": "This monitor is not active." } ``` ```json theme={null} { "detail": "This request is already being processed. Retry shortly." } ``` How to fix: * For a paused monitor, resume it with `POST /monitors/{monitor_id}/update` and `"is_active": true`, then trigger it * For a monitor whose `status` is `starting`, wait until it is `active`. For one whose `status` is `failed`, send the same upload request again, with the same file and settings, to finish it * A cancelled monitor cannot be resumed or changed. Create a new monitor to start checks again * For an idempotency conflict, retry with the original creation payload, or use a new key for a new monitor * If the request is still being processed, retry after a few seconds with the same payload and key; it returns what the first attempt created ### 409 Conflict (Atlas) `POST /people-intelligence-async` with `exclusive_run: true` on an existing investigation returns `409` while that investigation already has a run starting or running. Wait for the current run to finish, then retry. `POST /v1/atlas/investigations/{investigation_id}/runs` has no `exclusive_run` option. ```json theme={null} { "detail": "This investigation is already running. Send guidance to its agent or stop it before deploying again" } ``` Starting a run or requesting a report while the investigation is being deleted also returns `409`: `Atlas investigation is being deleted` or `investigation is being deleted`. ### 409 Conflict and 428 Precondition Required (Workflow Edits) An existing workflow content update requires `expected_revision`, copied from the latest workflow read's `edit_revision`. Missing it returns `428` with `detail.code: "workflow_revision_required"`. A stale revision returns `409` with `detail.code: "workflow_draft_conflict"`, `current_revision`, and the current workflow in `current_workflow`. Nothing from the stale update is saved. Apply your changes to `current_workflow` and resend with `current_revision` as `expected_revision`. Resending the old definition with the new revision overwrites the other edits. Updating a deleted workflow returns `404`. Changing the blocks of a locked workflow returns `409` (`Workflow is locked. Unlock it before changing its blocks.`); name and description changes still apply. A run with `expected_revision` returns `409` if the revision changed (`workflow_draft_conflict`) or the workflow has a draft that failed validation (`workflow_draft_not_runnable`). No new run starts for either refusal. ### 409 Conflict (Workflow Runs) `POST /workflows/create_workflow` with an `id` already in use, and a run of a workflow with no valid saved definition, also return `409`. | `detail` | Cause | How to fix | | - | - | - | | `A request with this Idempotency-Key is still starting. Retry shortly.` | A `POST /workflows/run` with the same `Idempotency-Key` is still starting. Carries `Retry-After: 1` | Retry with the same key after `Retry-After`; it returns the run the first request started | | `{"message": "This workflow or run was deleted. Create a new id or use a new idempotency key.", "code": "workflow_input_owner_deleted"}` | The workflow ID or idempotency key belonged to a deleted workflow or run | Use a new workflow ID or idempotency key | ### 413 Content Too Large Causes: * A `/storage/csv/upload` file larger than 45 MB: `Tabular files must be 45MB or less` * A workflow definition past its size or block-count limit, with `detail.code` `workflow_definition_too_large` * A monitor `handle_id` file larger than the monitor upload limit: `Uploaded file exceeds the monitor upload size limit.` * A monitor history export that is too large: `History export is too large. Select fewer dates.` or `Too many snapshots. Select a smaller date range.` Nothing is saved or started. Send a smaller file, definition, or date range. ### 422 Validation Error (Monitors) `POST /monitors/{monitor_id}/update` returns `422` for any request containing `struct`, even `null` or the current definition, and applies nothing. To change watched fields, create a new monitor. `POST /monitors` and `POST /monitors/{monitor_id}/update` return `422`, with a field-level `detail` array, when `webhook_event_types` is empty or names an unknown event. See [Monitor Endpoints](/api-reference/monitors/monitors-endpoints). ### 422 Validation Error (Workflows and Other Monitor Requests) Causes: * A workflow name, description, block field, or spec value past its limit, or a block name that does not exist, with `detail.code` `workflow_field_too_long` and one `validation_errors` entry per value * Missing required fields in create/update requests * Unknown fields in monitor create, update, row-toggle, source, or history-export JSON requests, such as `frequncy` instead of `frequency` * Toggling a monitor row without the required JSON body `{"is_active": false}` or `{"is_active": true}`; the query parameter alone is not accepted * A monitor `handle_id` that is not a tabular upload: `Use a tabular upload handle from /storage/csv/upload.` * A `cursor` for `GET /monitors/for-run/{run_id}` that did not come from a previous response: `Invalid monitor page cursor.` Example: ```json theme={null} { "detail": { "message": "workflow_name is 250 characters; the limit is 200.", "code": "workflow_field_too_long", "validation_errors": [ { "field": "workflow_name", "code": "too_long", "message": "workflow_name is 250 characters; the limit is 200.", "context": {"limit": 200, "length": 250}, "blocking": true } ] } } ``` How to fix: * Shorten each value named in `validation_errors` * Check block compatibility (e.g., webhook → company\_enrichment) * See `GET /workflows/blocks` for block specs ### 429 Too Many Requests A request went over a rate or concurrency limit and did not run. Retry after the `Retry-After` header. See [Rate Limits](/api-reference/rate-limits). ### 500 Internal Server Error Example: ```json theme={null} { "detail": "Internal server error" } ``` A sync People Intelligence, Company Intelligence, or QA Agent request also returns `500` when its job fails after it was accepted, for example when the balance no longer covers it or a `micro` request asks for more than 10 fields. The `-async` endpoints report the same failure as a `failed` job. `Failed to fetch user organization from API key` means the API key could not be checked. Retry the request. How to fix: * Retry the request with exponential backoff * If the error persists, contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai) with the response's `correlation_id` ### 502 Bad Gateway | Endpoint | `detail` | Fix | | - | - | - | | `POST /monitors/{monitor_id}/cancel` | `Monitor is cancelled, but its schedule could not be paused. Retry cancellation to finish cleanup.` | The monitor is cancelled and runs no more checks. Retry the cancel to finish; repeating it is safe | | `POST /monitors/{monitor_id}/trigger` | `Could not start the run.` | Retry the trigger | | `POST /monitors/{monitor_id}/update` | `3 of 120 rows did not take the update; the monitor is unchanged. Retry to apply it.` | The monitor keeps its old settings. Send the same update again | ### 503 Service Unavailable Sixtyfour is briefly unavailable or at capacity. Requests refused before they run are not charged. Retry after the `Retry-After` header. For `POST /workflows/run`, a `503` with code `workflow_input_unavailable` means the run's `webhook_payload` could not be stored within 15 seconds. No run was started and nothing was charged. Retry after `Retry-After` with the same body. ```json theme={null} { "detail": { "message": "Workflow input could not be stored. No run was started and nothing was charged; retry, with the same Idempotency-Key if you send one.", "code": "workflow_input_unavailable" }, "code": "workflow_input_unavailable", "correlation_id": "enrich_3f8b2c1e-9d4a-4c1b-8e2f-6a7b5c4d3e21" } ``` For `POST /workflows/cancel`, a `503` means the cancellation is not confirmed. Retry the cancel for the same `job_id` and poll [live status](/api-reference/workflows/workflows-execution#get-live-status) until the run ends. Do not start another run. | `detail` | When | `Retry-After` | | - | - | - | | `Could not start this request right now. You were not charged. Retry shortly.` | The request could not be started | 5 | | `Could not verify the API key right now. Retry shortly.` | The API key could not be checked | 5 | | `Access could not be checked right now. Try again shortly.` | Your tier or team access could not be checked | 5 | | `Enrichment is at capacity right now. Nothing was charged. Retry after the Retry-After header.` | A sync People Intelligence, Company Intelligence, or QA Agent request found no capacity | 30 | | `Search is at capacity right now. Retry shortly.` | Search is at capacity | 30 | | `Live status is temporarily unavailable. Retry after the Retry-After interval.` | `GET /workflows/runs/{run_id}/live_status` could not read the run's status. The run is unaffected | 5 | | `Workflow results are temporarily unavailable. Please retry shortly.` | A workflow's results could not be read. The run is unaffected | 15 | | `Some JSON results are still being saved. Retry shortly, or download the CSV now.`, or the same with `NDJSON` | A JSON or NDJSON link request for a step whose files are still saving | 60 | | `History export capacity is temporarily unavailable.` or `History export timed out. Select fewer dates and retry later.` | `POST /monitors/{monitor_id}/history-export` | 15 | | Starts with `Request capacity is temporarily unavailable.` | An endpoint with a burst budget is briefly unavailable | 15 | | `detail.code` is `workflow_input_unavailable` | The run's input is briefly unavailable | 5 | | `Email verification is temporarily unavailable. Retry shortly.` | `POST /verify-email` could not reach a verdict | — | The `-async` endpoints never return the capacity `503`; their jobs wait. See [Async Jobs](/api-reference/async-jobs#statuses). ### Search | Status | `detail` | Cause | | - | - | - | | 400 | `Invalid cursor.` | The `cursor` was altered or did not come from this search | | 400 | `Cursor expired. Start a new search.` | The cursor was already used or has expired. Start again from the `search_id` | | 400 | `Cursor requests must only include cursor. Omit ...` | A cursor request also sent other fields | | 400 | `Cursor references an exclusion list that is no longer available. Start a new search.` | An exclusion list the search used was deleted | | 400 | `Search is not completed (status: ...)` | Replaying a search, or building an exclusion list from it, before it finished | | 400 | `max_results must be between 1 and ...` | `POST /search/export` with `max_results` out of range | | 400 | `Lookalike tasks must be polled via GET /search/lookalike/{task_id}` | A lookalike `task_id` sent to `GET /search/status/{task_id}` | | 400 | `Exclusion list limit (20) reached. Delete unused lists first.` | Your organization has the maximum number of exclusion lists | | 400 | `name is required for this source type.` | Creating an exclusion list from a source other than a `search_id` without a `name` | | 404 | Starts with `Search not found` | Replaying a `search_id` that does not exist or cannot be replayed | | 404 | `Task not found` | A `task_id` from another organization or team. An unknown `task_id` returns the object shown in [Object](#object) | | 409 | `Cursor is being processed. Try again.` | Another request is using the same cursor. Retry it after that request finishes | | 409 | `Export already in progress. Poll /search/status/...` | An export of the same search is still running | | 409 | Starts with `Saved lookalike`, or says the seed profiles are gone | The lookalike search cannot be replayed. Run it again | | 500 | `Could not save this search.` | `POST /search/start-deep-search` could not save the search. A `search_id` that is not a UUID or was already used returns this on every retry; send a new UUID | | 503 | `People search is unavailable right now.` or `Company search is unavailable right now.` | Search cannot serve this request right now. There is no `Retry-After`; retry with backoff | | 503 | `Lookalike results are temporarily unavailable` | `GET /search/lookalike/{task_id}` could not load results. There is no `Retry-After`; retry after a few seconds | | 504 | `Seed identity resolution timed out; retry the request.` | `POST /search/lookalike` could not resolve the seed profiles in time. No search started, so retrying is safe | ### Filter field values errors `POST /search/filter-field-values` puts a `code` in `detail`. `search_backend_*` errors also carry `retryable`: ```json theme={null} { "detail": { "code": "search_backend_timeout", "detail": "Field-values aggregation timed out. Retry with narrower filters or a smaller top_k.", "retryable": true } } ``` | Status | `code` | Meaning | Retryable | | - | - | - | - | | 400 | `invalid_request`, `unsupported_mode`, `invalid_scoped_filters`, `unsupported_field`, `top_values_not_supported` | The request is invalid; fix it | No | | 500 | `search_backend_rejected_query` | The search service rejected the query | No | | 500 | `aggregation_failed` | The values could not be fetched | — | | 502 | `search_backend_auth_error` | Sixtyfour could not authenticate with the search service | No | | 502 | `search_backend_transport_error` | The connection to the search service failed | Yes | | 503 | `search_backend_unavailable` | The search service could not be reached | Yes | | 503 | `search_backend_overloaded` | The search service is overloaded | Yes | | 503 | `search_backend_upstream_error` | The search service returned a server error | Yes | | 504 | `search_backend_timeout` | The search service timed out | Yes | ### Monitors These requests return `400`. See [Monitor Endpoints](/api-reference/monitors/monitors-endpoints). | Request | Cause | | - | - | | Create or update | A `frequency` outside `1h`–`365d`, a `tier` other than `low`, `medium`, or `high`, a `subject_type` other than `company` or `lead`, an empty `struct`, a column used as both a key column and a watched field, a row with no value in any key column, two rows with the same key values, or a non-public `webhook_url` | | [Create from a file](/api-reference/monitors/monitors-endpoints#create-monitor-from-a-file) | A file that is not `.csv`, `.json`, `.jsonl`, or `.ndjson`, is larger than 100 MB, has no rows or more than 100,000 rows, or cannot be parsed; a `config` that is not valid JSON, contains an unknown field, has an empty or invalid `webhook_event_types` list, or names a `key_columns` entry missing from the file | | `handle_id` | A file that cannot be parsed, or has no rows or more than 100,000 rows | | [List rows](/api-reference/monitors/monitors-endpoints#list-rows), `GET /monitors/{monitor_id}/runs` | A `cursor` that did not come from a previous `next_cursor` | | `GET /monitors` | A `cursor` that is not an ISO timestamp with a timezone | | Update | The new schedule cannot be applied: `Could not apply the new schedule.` Retry the update | | History export | An end date before the start date, or a range with no snapshots (`No snapshots in this date range.`) | Two rows with the same key values: ```json theme={null} { "detail": "Rows 0 and 3 have the same identity values (Acme Robotics, acmerobotics.com). Add a column that tells them apart, or remove the duplicate." } ``` ### Signed download URLs A `download_url` from workflow results or webhooks, and the `url` from `GET /search/download`, needs an API key or OAuth token from the same organization. | Status | `detail` | Cause | | - | - | - | | 401 | `Invalid or expired download token` | The link is older than 15 minutes or was altered. Request a fresh link | | 403 | `Access denied` | The credential belongs to another organization | | 422 | `This file could not be made safe to open in a spreadsheet: ...` | The CSV could not be escaped. Retry with `spreadsheet_safe=false` to download the file exactly as stored | Downloads share an organization budget; see [Rate Limits](/api-reference/rate-limits#workflows). ### Workflow result download retries `GET /workflows/runs/{run_id}/results/download-links` returns `429` or `503` with `Retry-After`. Wait that long and send the same request. Neither response affects the run or its results, so do not start a new run to recover a download. See [Rate Limits](/api-reference/rate-limits#workflows). ### Private runs | Status | `detail` | Cause | | - | - | - | | 403 | `Private runs aren't available for your organization yet` | Private runs are not enabled for your organization | | 403 | `Your team doesn't allow private runs` | Your team's settings turn private runs off | | 400 | `Private runs need a signed-in user or an API key with a known creator` | The API key has no creator on record. Use a key created by a team member | | 400 | `private on xhigh (or scout with graph) is only supported on the async endpoint: use /people-intelligence-async.` | `private` sent to the sync endpoint | | 400 | `private applies only to a new investigation, so it can't be combined with investigation_id.` | `private` sent with `investigation_id` | | 409 | `This dispatch_id was already used for a run with a different owner. Send a new dispatch_id.` | The `dispatch_id` belongs to another owner's run | ### Lattice | Status | `detail` | Cause | | - | - | - | | 403 | `The knowledge graph is not enabled for your organization.` | Lattice is not enabled for your organization | | 409 | `The knowledge graph is unavailable for your organization. Contact support.` | Organizations with customer-managed encryption need Sixtyfour to set up Lattice first | | 401 | `Dashboard authentication required for Lattice` | Graphs open only in the Sixtyfour app; an API key can run a workflow that builds one | | 409 | `Another team already has a graph with this name. Choose a different graph name.` | Graph names are shared across your organization's teams | ### Webhook test events | Status | `detail` | Cause | | - | - | - | | 422 | `Unknown event; use GET /webhooks/test-events` | The `event` is not in the catalog | | 422 | `url must use HTTPS`, or why the URL was refused | The `url` is not a public HTTPS address | | 409 | `Organization has no active signing secret; no webhook sent` | `signing` is `required` and your organization has no active signing secret | | 503 | `Test events are unavailable right now; no webhook sent` or `The signing secret could not be read; no webhook sent` | Retry the request | No webhook is sent for any of these. ## LinkedIn monitor validation and access | Status | Cause | | - | - | | 422 | `POST /monitors/linkedin` with an invalid or company URL, no or unsupported fields, unknown fields, extra columns, a duplicate profile, or a frequency outside 7–365 days | | 400 | An update with a frequency outside 7–365 days | | 422 | An update that changes the selected fields or sends a tier | | 403 | LinkedIn monitors are not enabled for your organization; listings omit them | | 409 | An idempotency key reused with different creation content | ## LinkedIn monitor check failures A LinkedIn monitor accepts any valid profile URL. A profile Sixtyfour has no data for reports a row `error` event on each check: | `payload.reason` | Meaning | | - | - | | `linkedin_profile_not_found` | Sixtyfour has no data for this profile. | | `linkedin_profile_deleted` | The matching profile is marked deleted. | A failed check keeps the previous baseline and reports no changes; the next scheduled check tries again. See [LinkedIn Monitors](/api-reference/monitors/linkedin-monitors). A LinkedIn check that fails because the balance is too low writes no `error` event. It sends `monitor.execution.failed` instead. ## Common Troubleshooting Scenarios ### Issue: Request times out Research calls take up to 10 minutes. Set your client timeout to at least 15 minutes, or use the `-async` endpoints with [polling](/api-reference/async-jobs#polling) or `webhook_url`. A sync request keeps running, and is charged, after your client disconnects; see [Sync calls and retries](/api-reference/async-jobs#sync-calls-and-retries). *** ### Issue: Empty or incomplete results Send more identifiers (name, company, LinkedIn URL) and specific `struct` descriptions; check `confidence_score`. *** ### Issue: Workflow run stuck or not progressing Check: * Are you polling `GET /workflows/runs/{run_id}/live_status`? * Is the `overall_status` still `queued`, `running`, or `finalizing`? Solution: * Poll every 10 seconds or slower and honor `Retry-After`. See [Status polling](/api-reference/rate-limits#status-polling) * Any `overall_status` other than `queued`, `running` and `finalizing` is final * Use `overall_progress_percentage` and `blocks` to see per-block progress * To stop a run: `POST /workflows/cancel?job_id={job_id}` * Check each block's `error_message`. Cancel and rerun a run that stops progressing *** ### Issue: Webhook signature verification fails or deliveries stop Check: * Is your verifier using the **raw request body bytes**, not a re-serialized JSON parse? * During rotation, does it iterate every `v1=` segment (not only the last)? * Is your server clock within 5 minutes of UTC? * Are you using the plaintext secret (`sk_whsec_<64 hex>`), not the masked preview shown in the dashboard? Solution: * Log the raw body, the `Sixtyfour-Signature` header, and the HMAC your server computes; compare against what we sent. * See [Signing Secrets & Verification](/api-reference/webhooks/signing-secrets) for the verification algorithm and reference code. * When your organization has a signing secret and Sixtyfour cannot sign a delivery, it sends nothing. Results stay available at `GET /job-status/{task_id}`. ### Verify Phone column mappings Rows with a phone but no value in the configured `name_column` skip verification without failing the batch. They return `Missing Name` in `phone_warnings` and `MissingRequiredInput` details in `phone_errors`. The name mapping must match the input column. For example, data containing `fullName` requires `name_column: "fullName"`. See [Verify Phone](/guides/building-workflows#intelligence) to update a saved mapping. A missing phone column fails the block; a blank phone value skips the row. ## Getting Help If you're experiencing issues not covered in this guide: 1. Check the endpoint's page for its requirements; the [quick start](/api-quick-start#endpoints) lists every area 2. Check [status.sixtyfour.ai](https://status.sixtyfour.ai) for incidents 3. Contact support at [support@sixtyfour.ai](mailto:support@sixtyfour.ai) 4. Include the following in your support request: * The `correlation_id` from the response * Request/response details (remove sensitive data) * Error message * Endpoint being called * Approximate timestamp of the error # Age Verification Source: https://docs.sixtyfour.ai/api-reference/intelligence/age-verification https://api.sixtyfour.ai/openapi.json post /verify-age Start an age verification for a person. Returns a task_id to poll `GET /verify-age/{task_id}`. # Age Verification Status Source: https://docs.sixtyfour.ai/api-reference/intelligence/age-verification-status https://api.sixtyfour.ai/openapi.json get /verify-age/{task_id} Poll an age verification job. Returns `{task_id, status}` while running or in a terminal non-completed state, and the age/DOB envelope once completed. # Fetch a profile photo from people search Source: https://docs.sixtyfour.ai/api-reference/media/fetch-a-profile-photo-from-people-search https://api.sixtyfour.ai/openapi.json get /v1/media/profiles/{key} Serve a profile photo returned by people search. Permanent and credential-free; embed or link directly. Photos may be cached for a day. # Fetch an image or PDF returned by an enrichment Source: https://docs.sixtyfour.ai/api-reference/media/fetch-an-image-or-pdf-returned-by-an-enrichment https://api.sixtyfour.ai/openapi.json get /v1/media/{token} Serve one image or PDF referenced by an enrichment result. Unguessable, and kept until the investigation that saved the image, or its organization, is permanently deleted; embed or link directly. Organizations with customer-managed encryption must send their API key. # Cancel Monitor Source: https://docs.sixtyfour.ai/api-reference/monitors/cancel-monitor https://api.sixtyfour.ai/openapi.json post /monitors/{monitor_id}/cancel Permanently stop future checks. Create a new monitor to start again. # Create LinkedIn Monitor Source: https://docs.sixtyfour.ai/api-reference/monitors/create-linkedin-monitor https://api.sixtyfour.ai/openapi.json post /monitors/linkedin Watch one LinkedIn profile for changes to the fields you select. The first check records its values; later checks report exact changes, without research. A check your balance cannot cover leaves the values unchanged and is reported as failed. # Create Monitor Source: https://docs.sixtyfour.ai/api-reference/monitors/create-monitor https://api.sixtyfour.ai/openapi.json post /monitors Create one monitor from a table, with a row per subject. Cells you supply for watched fields become their starting values. A row missing any is enriched once before its recurring checks start. # Create Monitor From A File Source: https://docs.sixtyfour.ai/api-reference/monitors/create-monitor-from-a-file https://api.sixtyfour.ai/openapi.json post /monitors/upload Create one monitor from a file, one row per subject. Returns `202` with `status: starting` once the file is checked. Rows are then added in the background, and `status` becomes `active` when all are in. Resending the same request returns the same monitor and finishes an interrupted upload. # Download Monitor Snapshot Source: https://docs.sixtyfour.ai/api-reference/monitors/download-monitor-snapshot https://api.sixtyfour.ai/openapi.json get /monitors/{monitor_id}/snapshots/{as_of} One snapshot as a CSV: a row per subject, watched fields as columns. `as_of` is a timestamp from /snapshots, or `latest` for the present. # Download Selected Snapshots As ZIP Source: https://docs.sixtyfour.ai/api-reference/monitors/download-selected-snapshots-as-zip https://api.sixtyfour.ai/openapi.json post /monitors/{monitor_id}/history-export The CSVs of the selected snapshots in one ZIP. Omit `timestamps` to include every snapshot from `start` to `end` (UTC, inclusive). Up to 5,000 snapshots and 50 MiB of CSV per export. # Get Monitor for Workflow Block Source: https://docs.sixtyfour.ai/api-reference/monitors/get-monitor-for-workflow-block https://api.sixtyfour.ai/openapi.json get /monitors/for-run/{workflow_run_id}/{block_id} The monitor a run's `monitor` block started. Its rows are read from `GET /monitors/{monitor_id}/rows`. Team-gated: the run being visible does not make the monitor visible. # LinkedIn Monitors Source: https://docs.sixtyfour.ai/api-reference/monitors/linkedin-monitors Discover supported profile fields and create a LinkedIn monitor. Watch selected fields in Sixtyfour's stored LinkedIn profiles. Requires LinkedIn monitoring access for your organization. ## List supported fields ```http theme={null} GET /monitors/linkedin/fields ``` Returns `200` with all supported fields, without pagination. Response excerpt: ```json theme={null} [ { "key": "full_name", "label": "Full name" }, { "key": "headline", "label": "Headline" } ] ``` ## Create a LinkedIn monitor ```http theme={null} POST /monitors/linkedin ``` | Field | Required | Description | | - | - | - | | `linkedin_url` | Yes | LinkedIn person URL (`/in/`). Stored as `https://www.linkedin.com/in/{name}`, without a query string or fragment. | | `fields` | Yes | Nonempty selection of keys from field discovery. Fixed after creation. | | `frequency` | No | Default `1w`. Range: 7–365 days. Units: `d`, `w`, `m`. On a LinkedIn monitor `m` means 30 days, not minutes as on a research monitor. A frequency in `m` is stored in days, so `1m` is returned as `30d`. | Other options (`name`, `first_check`, `is_active`, webhook settings, `metadata`, and `idempotency_key`) follow [standard monitor creation](/api-reference/monitors/monitors-endpoints#create-monitor). Here, `name` defaults to `LinkedIn monitor`; `tier` and `research_plan` are not accepted. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/monitors/linkedin" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "linkedin_url": "https://www.linkedin.com/in/example", "fields": ["headline", "current_company_name", "skills"], "frequency": "1w" }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/monitors/linkedin", headers={"x-api-key": "YOUR_API_KEY"}, json={ "linkedin_url": "https://www.linkedin.com/in/example", "fields": ["headline", "current_company_name", "skills"], "frequency": "1w", }, ) response.raise_for_status() monitor = response.json() print(monitor["id"], monitor["status"], monitor["next_run_times"]) ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/monitors/linkedin", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ linkedin_url: "https://www.linkedin.com/in/example", fields: ["headline", "current_company_name", "skills"], frequency: "1w", }), }); if (!response.ok) { throw new Error(`Failed to create monitor: ${await response.text()}`); } const monitor = await response.json(); console.log(monitor.id, monitor.status, monitor.next_run_times); ``` Returns `201` with the standard monitor response: `monitor_type: "linkedin"`, one row, and the selected fields in `struct`. `tier` reads `low` but has no effect, because LinkedIn checks run no research. ```json theme={null} { "monitor_type": "linkedin", "id": "4c2a9e7b-1f3d-4b8a-9c6e-2d7f5a1b3e90", "org_id": "acme", "team_id": "5e1d9c3a-7b2f-4e6a-8c1d-3f9a2b4c6d8e", "name": "LinkedIn monitor", "frequency": "1w", "struct": { "current_company_name": "Current company", "headline": "Headline", "skills": "Skills" }, "tier": "low", "subject_type": "lead", "research_plan": null, "is_active": true, "disabled_reason": null, "webhook_url": null, "webhook_event_types": ["monitor.field.changed"], "metadata": null, "last_run_at": null, "next_run_times": [ "2026-10-01T00:00:00+00:00", "2026-10-08T00:00:00+00:00", "2026-10-15T00:00:00+00:00" ], "row_count": 1, "active_row_count": 1, "status": "active", "created_at": "2026-09-28T17:05:12.482Z", "updated_at": "2026-09-28T17:05:12.482Z" } ``` ## CSV upload Use [POST /monitors/upload](/api-reference/monitors/monitors-endpoints#create-monitor-from-a-file) with `monitor_type: "linkedin"` in `config`. These rules differ from research uploads: * `config` needs `key_columns: ["linkedin_url"]` and `subject_type: "lead"`, and takes no `research_plan`. `struct` keys must be field keys from discovery; their values are replaced with the field labels. `frequency` defaults to `1w`, with the range above. `tier` is ignored. * The CSV must contain exactly one column of LinkedIn person URLs with a nonempty header, under any name (for example, `linkedin_url`). The first line is always treated as the header. Empty lines are skipped, and every other line must hold a valid person URL. * Duplicate profiles are rejected after URL normalization. To use a file uploaded earlier with `POST /storage/csv/upload`, send its `handle_id` to [Create monitor](/api-reference/monitors/monitors-endpoints#rows-from-an-upload-handle) with the same settings. The response is `201` with `status: "starting"`, and rows are added in the background. ## Update a LinkedIn monitor [Update monitor](/api-reference/monitors/monitors-endpoints#update-monitor) applies, with these rules: * `frequency` must stay within 7–365 days. `m` means 30 days here too, and is stored in days. * `tier` is not accepted. * The selected fields cannot change. Create a new monitor to watch other fields. ## Check behavior The first successful check establishes a baseline. Later checks report exact field differences; see [Values](/api-reference/monitors/monitors-overview#values). Checks read Sixtyfour's stored profile data and run no research. A check reads the latest copy of the profile that Sixtyfour holds, so a change on LinkedIn is reported only after that copy is updated. The `payload.search_updated_at` of each `change` and `established` event says when the copy it read was last updated. A missing or deleted profile records a failed check, preserves the baseline, and is retried on future scheduled checks. A check that finds changes your balance cannot cover sends `monitor.execution.failed`, records no `change` or `error` event, and keeps the previous values. Its `completion` event, listed with `include_completions=true`, still records the values read. A later check reports the changes once your balance covers them. ## Event payloads LinkedIn monitors write the [standard event types](/api-reference/monitors/monitors-overview#events). Their `previous_value` and `current_value` hold text values as they are and other values as compact JSON. The `payload` differs from a research monitor's: | `event_type` | `payload` | | - | - | | `established`, `change` | `source` (`"linkedin"`), `sources` (the profile URL), `previous` and `current` (the raw values, such as a list rather than its JSON text), `search_updated_at` (when the copy read was updated), and the monitor's `metadata`. There is no `confidence` or `justification`. | | `completion` | Written for every check that reads the profile, with or without changes. Holds `values` (every selected field as read), `checked_at`, `result` (a summary of the check), and `search_updated_at`. | | `error` | `reason`: `linkedin_profile_not_found` or `linkedin_profile_deleted`. | ## Pricing See [Credits & Pricing](/guides/credits-and-pricing#monitors). ## Errors See [Handling Errors](/api-reference/errors#linkedin-monitor-validation-and-access). # List A Monitor Run's Changed Rows Source: https://docs.sixtyfour.ai/api-reference/monitors/list-a-monitor-runs-changed-rows https://api.sixtyfour.ai/openapi.json get /monitors/{monitor_id}/runs/{run_id}/changes One entry per changed row, with each changed field's `previous` and `current` value. Starting values and checks with no change are left out. A completed run with no changes returns `changes: []`; a run that has not completed returns 409. Failed row checks are counted on the run, not listed here. Page with `next_cursor`; a page never splits a row. # List A Monitor's Rows Source: https://docs.sixtyfour.ai/api-reference/monitors/list-a-monitors-rows https://api.sixtyfour.ai/openapi.json get /monitors/{monitor_id}/rows A page of the monitor's rows, switched-off rows included, in the order they were added. A row has no schedule of its own, so its `next_run_times` are the monitor's. # List A Row's Events Source: https://docs.sixtyfour.ai/api-reference/monitors/list-a-rows-events https://api.sixtyfour.ai/openapi.json get /monitors/{monitor_id}/rows/{row_id}/events One row's events, newest first. `field` narrows to a single cell's whole history. Quiet runs are hidden unless `include_completions` is set. # List Latest Changes Source: https://docs.sixtyfour.ai/api-reference/monitors/list-latest-changes https://api.sixtyfour.ai/openapi.json get /monitors/{monitor_id}/changes Each row's latest change time per watched field, as `{row_id: {field: timestamp}}` (ISO 8601). Only `change` events count; rows and fields with no change are left out, and an empty history returns `{}`. # List LinkedIn Monitor Fields Source: https://docs.sixtyfour.ai/api-reference/monitors/list-linkedin-monitor-fields https://api.sixtyfour.ai/openapi.json get /monitors/linkedin/fields Return every available profile field in one request, without pagination. Choose any nonempty subset of the returned keys and pass it as `fields` to `POST /monitors/linkedin`. No profile URL is required to discover fields. # List Monitor Runs Source: https://docs.sixtyfour.ai/api-reference/monitors/list-monitor-runs https://api.sixtyfour.ai/openapi.json get /monitors/{monitor_id}/runs The monitor's check runs, newest first. `rows_checked` counts successful checks, including ones with no change; failed and skipped rows are counted separately. Counts are final when the run ends. A run is `failed` when it could not produce a complete result. Send `next_cursor` as `cursor` for the next page. # List Monitor Snapshots Source: https://docs.sixtyfour.ai/api-reference/monitors/list-monitor-snapshots https://api.sixtyfour.ai/openapi.json get /monitors/{monitor_id}/snapshots Every moment this table changed, newest first, each with a download link. Includes initial values and subsequent changes, not unchanged checks. Pass the last as_of as before to fetch the next page. # List Monitors Source: https://docs.sixtyfour.ai/api-reference/monitors/list-monitors https://api.sixtyfour.ai/openapi.json get /monitors One page of monitors, newest first. Set include_inactive=true and include_cancelled=false to include paused and other disabled monitors while hiding cancellations. # List Monitors For Run Source: https://docs.sixtyfour.ai/api-reference/monitors/list-monitors-for-run https://api.sixtyfour.ai/openapi.json get /monitors/for-run/{run_id} The monitors a workflow run started in your team, newest first, at most 50 per page. For the next page, pass `cursor` as `|` of the last monitor on this page. LinkedIn monitors are listed only when your organization has them enabled. # Monitor Notifications Source: https://docs.sixtyfour.ai/api-reference/monitors/monitor-notifications Webhook notifications a monitor sends after each check, their payload, and how to read the events behind them. Get told when a monitored field changes or a check fails, instead of polling. A monitor can notify you after each row check or after all selected rows finish a run. Your receiver reads row events or downloads the whole table from the API. [LinkedIn monitors](/api-reference/monitors/linkedin-monitors) use the same subscriptions and notification payloads. ## Subscribe Set `webhook_url` and `webhook_event_types` when you [create the monitor](/api-reference/monitors/monitors-endpoints#create-monitor) or [update it](/api-reference/monitors/monitors-endpoints#update-monitor). A monitor sends only the event types you list. The default subscription is `["monitor.field.changed"]`. | Event type | Sent when | | - | - | | `monitor.field.changed` | A row check reported at least one `change` event. | | `monitor.execution.completed` | A row check succeeded without a `change` event, including a first check that only establishes initial values. | | `monitor.execution.failed` | A row check could not run. The next check re-examines the same time window. | | `monitor.run.completed` | Every row selected for one monitor run finished. Sent once per run, with row counts. | The three row event types send at most one notification per row per check. `webhook_url` must be a public HTTPS or HTTP address. Skipped checks send no row notification. A check is skipped when your balance cannot cover it, which records an `error` event on the row, or when the team's spend limit is reached, which records nothing. A row's `last_run_at` advances only when a check of that row completes, so compare it with the monitor's frequency to catch skipped rows. The monitor's own `last_run_at` advances at the start of every tick. LinkedIn monitors handle a low balance differently; see [LinkedIn check behavior](/api-reference/monitors/linkedin-monitors#check-behavior). ### First-run notifications There is no separate initial-findings event. A row missing watched values gets a full first check: * If it only fills in missing values, it sends `monitor.execution.completed`. Each value found is recorded as an `established` event, which is not a change. * If it finds that a supplied value is out of date, it sends `monitor.field.changed`. * If it fails, it sends `monitor.execution.failed`. Rows that supply every watched value skip setup and send nothing at creation; their first check runs on the schedule. Subscribe to both `monitor.field.changed` and `monitor.execution.completed` to receive every successful first-run result. ## Row notification payload ```json theme={null} { "type": "monitor.field.changed", "monitor_id": "d5a8c2f1-7e6b-4a93-b012-8f6e3d9c5a42", "row_id": "8c3e1f2a-6b4d-4c9e-a1f7-2e5d9b3c7a10", "event_group_id": "a7d2e9f1-3c5b-4e8a-9b1d-6f4c2e8a0b37", "metadata": { "portfolio_id": "p_42" } } ``` | Field | Description | | - | - | | `type` | The event type. | | `monitor_id` | The ID of the monitor containing the checked row. Use it as `{monitor_id}` in the events URL. | | `row_id` | The ID of the row that was checked, used as `{row_id}` in the events URL. | | `event_group_id` | Identifies one check of the specified row. Each check gets a new ID; all events from that check share it. | | `metadata` | The monitor's `metadata`, echoed so you can route the notification without a lookup. It includes any [keys Sixtyfour adds](/api-reference/monitors/monitors-endpoints#metadata). | The payload identifies the check but does not include the changed values. Read them with [List row events](/api-reference/monitors/monitors-endpoints#list-row-events), filtered to the check: ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/rows/{row_id}/events?event_group_id={event_group_id}&limit=500 ``` For a `monitor.execution.completed` notification, use the same endpoint with `include_completions=true`: ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/rows/{row_id}/events?event_group_id={event_group_id}&include_completions=true&limit=500 ``` On a first check with no changes, the response holds one `established` event per value found, with the value in `current_value`, and a `completion` event whose `payload` looks like: ```json theme={null} { "first_run": true, "recheck_ran": true, "fields_established": 1 } ``` ## Whole-monitor completion Subscribe to `monitor.run.completed` to receive one notification after every row selected for a run has finished, including the initial run. Use it alone (`"webhook_event_types": ["monitor.run.completed"]`) or alongside row events. ```json theme={null} { "type": "monitor.run.completed", "monitor_id": "d5a8c2f1-7e6b-4a93-b012-8f6e3d9c5a42", "run_id": "c93dc1be-fdb7-4b36-9f74-23b284eaf56e", "completed_at": "2026-10-01T10:00:00+00:00", "rows": { "total": 100, "succeeded": 97, "skipped": 3 }, "metadata": { "portfolio_id": "p_42" } } ``` | Field | Description | | - | - | | `monitor_id` | The monitor whose run finished. | | `run_id` | Identifies this whole-monitor run. Stable across delivery retries; use with `type` to deduplicate. | | `completed_at` | When the run finished, as ISO 8601. The same on every delivery attempt. | | `rows.total` | Number of active rows selected at the start of the run. Excluded rows are not counted. | | `rows.succeeded` | Rows whose check succeeded, with or without changes. | | `rows.skipped` | Rows without a confirmed successful check, including errors and checks skipped because a spending limit was reached. | | `metadata` | The monitor's `metadata` at delivery time, including any [keys Sixtyfour adds](/api-reference/monitors/monitors-endpoints#metadata), or `null` when unset. | The notification means the run finished, not that every row was refreshed: skipped rows keep their previous values. `rows.succeeded + rows.skipped` equals `rows.total`. No run notification is sent when the monitor is paused or cancelled, or has no active rows, when the run starts. Pausing a monitor mid-run does not stop that run. A `failed` run sends no notification; see [List monitor runs](/api-reference/monitors/monitors-endpoints#list-monitor-runs). Read only the rows this run changed using [Get a run's changed rows](/api-reference/monitors/monitors-endpoints#get-a-runs-changed-rows), with the notification's `run_id`: ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/runs/{run_id}/changes ``` Or download the whole table using [Download snapshot](/api-reference/monitors/monitors-endpoints#download-snapshot): ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/snapshots/{completed_at} ``` URL-encode `completed_at` for the path. The CSV holds every row as of that time, including switched-off rows, so it can have more rows than `rows.total`. Use `snapshots/latest` for current values. A run notification has no `row_id` or `event_group_id`, and `run_id` is not a filter for row events. ## Delivery * Notifications are signed when your organization has a signing secret. See [Signing Secrets & Verification](/api-reference/webhooks/signing-secrets). * A notification can arrive more than once. Signed notifications carry `Sixtyfour-Event-Type`, the event type, and `Sixtyfour-Event-Id`, the row notification’s `event_group_id` or the whole-monitor notification’s `run_id`; use the pair to deduplicate. Unsigned notifications carry neither header: deduplicate on the body's `type` with its `event_group_id` or `run_id`. * A failed delivery is retried with exponential backoff, up to 5 attempts in total. Each attempt times out after 10 seconds. * A notification that cannot be delivered does not affect or rerun the check. Its events are stored and readable from the API. * To send a sample notification to your receiver, call `POST /webhooks/test-events` with `event` set to a monitor event type, such as `monitor.field.changed`, and `url` set to an HTTPS receiver. The sample has the production payload and signing, but its `monitor_id` and `row_id` do not belong to a real monitor, so there are no events to read for it. ## Example receiver Verify each notification against its raw body, then read the check's events using `monitor_id`, `row_id`, and `event_group_id` from the payload. The examples process field changes. > [!NOTE] > > This receiver requires your organization to have a webhook signing secret. Without one, notifications arrive unsigned and this receiver rejects them with `400`. Generate a secret in [Settings → Webhooks](https://app.sixtyfour.ai/settings/webhooks). ```python Python theme={null} import hashlib import hmac import json import os import time import requests from flask import Flask, abort, request API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.sixtyfour.ai" SECRET = os.environ["SIXTYFOUR_WEBHOOK_SECRET"] TOLERANCE = 300 app = Flask(__name__) def verify(raw: bytes, header: str, secret: str) -> bool: parts = [p.strip() for p in header.split(",")] try: t = int(next(p.split("=", 1)[1] for p in parts if p.startswith("t="))) except (StopIteration, ValueError): return False if abs(int(time.time()) - t) > TOLERANCE: return False expected = hmac.new( secret.encode("utf-8"), f"{t}.".encode("utf-8") + raw, hashlib.sha256 ).hexdigest() return any( p.startswith("v1=") and hmac.compare_digest(expected, p.split("=", 1)[1]) for p in parts ) @app.post("/hooks/sixtyfour") def monitor_notification(): raw = request.get_data() # raw bytes, before JSON parse if not verify(raw, request.headers.get("Sixtyfour-Signature", ""), SECRET): abort(400) body = json.loads(raw) if body["type"] != "monitor.field.changed": return "", 204 events = requests.get( f"{BASE_URL}/monitors/{body['monitor_id']}/rows/{body['row_id']}/events", headers={"x-api-key": API_KEY}, params={"event_group_id": body["event_group_id"], "limit": 500}, ) events.raise_for_status() for event in events.json(): if event["event_type"] == "change": print(event["field"], event["previous_value"], "->", event["current_value"]) return "", 204 ``` ```javascript JavaScript theme={null} import crypto from "crypto"; import express from "express"; const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://api.sixtyfour.ai"; const SECRET = process.env.SIXTYFOUR_WEBHOOK_SECRET; const TOLERANCE = 300; const app = express(); app.use(express.raw({ type: "application/json" })); function verify(raw, header, secret) { const parts = header.split(",").map((p) => p.trim()); const tPart = parts.find((p) => p.startsWith("t=")); if (!tPart) return false; const t = parseInt(tPart.slice(2), 10); if (!Number.isFinite(t)) return false; if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE) return false; const expected = crypto .createHmac("sha256", secret) .update(Buffer.concat([Buffer.from(`${t}.`), raw])) .digest("hex"); return parts.some((p) => { if (!p.startsWith("v1=")) return false; const got = Buffer.from(p.slice(3), "utf8"); const exp = Buffer.from(expected, "utf8"); return got.length === exp.length && crypto.timingSafeEqual(got, exp); }); } app.post("/hooks/sixtyfour", async (req, res) => { if (!verify(req.body, req.header("Sixtyfour-Signature") || "", SECRET)) { return res.status(400).send("invalid signature"); } const body = JSON.parse(req.body.toString("utf8")); if (body.type !== "monitor.field.changed") return res.sendStatus(204); const params = new URLSearchParams({ event_group_id: body.event_group_id, limit: "500" }); const response = await fetch( `${BASE_URL}/monitors/${body.monitor_id}/rows/${body.row_id}/events?${params}`, { headers: { "x-api-key": API_KEY } }, ); if (!response.ok) return res.status(502).send("could not read events"); const events = await response.json(); for (const event of events.filter((e) => e.event_type === "change")) { console.log(event.field, event.previous_value, "->", event.current_value); } res.sendStatus(204); }); app.listen(3000); ``` ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing#monitors) for more information. ## Errors For error responses (400, 403, 404, etc.), see [Handling Errors](/api-reference/errors). # Monitor Endpoints Source: https://docs.sixtyfour.ai/api-reference/monitors/monitors-endpoints Create monitors from rows or a file, read, update, trigger, and cancel them, and read change history, snapshots, and history exports. Watch fields on a list of people or companies and read what changed. Use these endpoints to create a monitor from rows or from a file, inspect its rows and events, export the table at any point in time or across a date range, and change or stop it. See [Monitors Overview](/api-reference/monitors/monitors-overview) for how checks work. > [!NOTE] > > Monitors require access for your organization. Contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai). > [card] [API Reference](/api-reference/monitors/create-monitor) > > See the full request/response schema and parameters in the API Reference. LinkedIn-specific field discovery and single-profile creation are documented in [LinkedIn Monitors](/api-reference/monitors/linkedin-monitors). The read, update, trigger, cancel, history, and export endpoints here apply to both monitor types. ## Create monitor Create one monitor from a table, with one row per subject. ```http theme={null} POST https://api.sixtyfour.ai/monitors ``` | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | Monitor name, 1–120 characters. Each row is named after it, followed by the row's key values. | | `rows` | array of objects | One of `rows` or `handle_id` | The table, one object per subject. | | `handle_id` | string (UUID) | One of `rows` or `handle_id` | A CSV upload handle from [`POST /storage/csv/upload`](/api-reference/workflows/workflows-overview#uploading-an-input-file), used instead of `rows`. See [Rows from an upload handle](#rows-from-an-upload-handle). | | `key_columns` | array of strings | Yes | Columns that identify each row. Every row needs a value in at least one of them. | | `struct` | object | Yes | Watched fields: field name → description, or an object with `type`, `description`, and `subfields`. At least one field. A row column with the same name as a watched field sets that field's starting value; send starting values as strings (see [Values](/api-reference/monitors/monitors-overview#values)). | | `frequency` | string | No | How often to check, `1h` to `365d`. Default `1d`. LinkedIn monitors differ. See [Frequency](/api-reference/monitors/monitors-overview#frequency). | | `tier` | string | No | Depth of the full re-check: `low` (default), `medium`, or `high`. `high` requires High tier access for your organization. | | `subject_type` | string | No | `company` (default) or `lead`. | | `monitor_type` | string | No | `research` (default) or `linkedin`. See [Create a LinkedIn monitor from multiple profiles](#create-a-linkedin-monitor-from-multiple-profiles). | | `research_plan` | string | No | Instructions for the enrichment that re-derives the fields. Up to 100,000 characters. | | `webhook_url` | string | No | Public URL that receives notifications. See [Monitor Notifications](/api-reference/monitors/monitor-notifications). | | `webhook_event_types` | array of strings | No | Nonempty list of `monitor.field.changed`, `monitor.execution.completed`, `monitor.execution.failed`, or `monitor.run.completed`. Defaults to `["monitor.field.changed"]`. | | `metadata` | object | No | Free-form data stored on the monitor and echoed in events and notifications. Sixtyfour adds keys to it; see [Metadata](#metadata). | | `is_active` | boolean | No | Default `true`. `false` creates the monitor paused: nothing is checked until you resume it with [Update monitor](#update-monitor). See [Disabled reasons](#disabled-reasons). | | `first_check` | string | No | `now` (default) immediately runs setup only for rows missing watched values. Complete rows wait for the schedule, including in mixed tables. `next_tick` defers every row to the first scheduled fire. | | `idempotency_key` | string | No | Up to 255 characters. A resend with the same key, or an identical body without one, returns the monitor the first attempt created, even if it has since been paused, updated, or cancelled. Send a new key to create the monitor again. | Send exactly one of `rows` or `handle_id`; a monitor needs at least one row. For large tables, use [Create monitor from a file](#create-monitor-from-a-file). See [Billing](/api-reference/monitors/monitors-overview#billing) for what creation checks. Creation is all or nothing. If any row is invalid, nothing is created. The response is `201` with the monitor, without its rows. Read them with [List rows](#list-rows). ### Example request ```json theme={null} { "name": "Portfolio leadership", "key_columns": ["company_name", "domain"], "rows": [ { "company_name": "Acme Robotics", "domain": "acmerobotics.com", "ceo_name": "Dana Lee" }, { "company_name": "Northwind Labs", "domain": "northwindlabs.io" } ], "struct": { "ceo_name": "Full name of the current CEO", "headcount": { "type": "int", "description": "Current number of employees" } }, "frequency": "1d", "tier": "low", "subject_type": "company", "webhook_url": "https://example.com/hooks/sixtyfour", "webhook_event_types": ["monitor.field.changed", "monitor.execution.completed", "monitor.execution.failed"], "metadata": { "portfolio_id": "p_42" } } ``` ### Example response ```json theme={null} { "monitor_type": "research", "id": "0b7f2c6e-4a1d-4a8e-9f3b-2d5c8e1a7b90", "org_id": "acme", "team_id": "5e1d9c3a-7b2f-4e6a-8c1d-3f9a2b4c6d8e", "name": "Portfolio leadership", "frequency": "1d", "struct": { "ceo_name": "Full name of the current CEO", "headcount": { "type": "int", "description": "Current number of employees" } }, "tier": "low", "subject_type": "company", "research_plan": null, "is_active": true, "disabled_reason": null, "webhook_url": "https://example.com/hooks/sixtyfour", "webhook_event_types": ["monitor.field.changed", "monitor.execution.completed", "monitor.execution.failed"], "metadata": { "portfolio_id": "p_42" }, "last_run_at": null, "next_run_times": [ "2026-09-29T00:00:00+00:00", "2026-09-30T00:00:00+00:00", "2026-10-01T00:00:00+00:00" ], "row_count": 2, "active_row_count": 2, "status": "active", "created_at": "2026-09-28T17:05:12.482Z", "updated_at": "2026-09-28T17:05:12.482Z" } ``` ### Rows from an upload handle `handle_id` reads the rows of a `.csv`, `.json`, `.jsonl`, or `.ndjson` file uploaded with `POST /storage/csv/upload`. The upload must belong to the same team as the request, and the file can hold up to 100,000 rows. The file is read when the monitor is created, and its rows pass the same checks as `rows`. With `monitor_type: "linkedin"`, the file needs exactly one column of profile URLs, under any header. The response is still `201`, but with `status: "starting"`: rows are added in the background, as in [Create monitor from a file](#create-monitor-from-a-file). ### Metadata `metadata` is stored on the monitor and its rows. It is echoed in the `payload` of `change` and `established` events and in every notification. Sixtyfour adds keys that record where the rows came from: | Key | Added when | | - | - | | `source_handle_id` | The monitor was created from a file, with `POST /monitors/upload` or `handle_id`. Holds the upload handle the rows were read from. | | `workflow_run_id` | The monitor was created in the web app from a workflow run's results. Holds that run's ID. | Your own keys sit beside them. [Updating](#update-monitor) `metadata` replaces the whole object, including these keys. *** ## Create monitor from a file Create one monitor from an uploaded file, with one row per subject. Use this endpoint for large tables: the request returns as soon as the file is validated, and the rows are added afterwards. ```http theme={null} POST https://api.sixtyfour.ai/monitors/upload ``` The request is `multipart/form-data` with two parts. | Part | Type | Required | Description | | - | - | - | - | | `file` | file | Yes | A `.csv`, `.json`, `.jsonl`, or `.ndjson` file. Maximum 100 MB and 100,000 rows. CSV files need a header row. | | `config` | string | Yes | The monitor's settings as a JSON-encoded string. Maximum 1,000,000 characters. | `config` takes the fields of [Create monitor](#create-monitor) except `rows`, `handle_id`, and `idempotency_key`. Every column in `key_columns` must exist in the file. | Header | Required | Description | | - | - | - | | `Idempotency-Key` | No | Up to 255 characters. Works like `idempotency_key` on [Create monitor](#create-monitor); without it, the key is derived from `config` and the file. | The file is validated in full before anything is created. If any row is invalid, nothing is created. The response is `202` with the monitor, without its rows. `status` is `starting` and `row_count` is `0`. Rows are then added in the background, in the file's order, and `row_count` rises as they arrive. When every row is in, `status` becomes `active`, `next_run_times` is populated, and the first check follows `first_check`. Poll `GET /monitors/{monitor_id}` to follow progress. While `status` is `starting`, no row is checked or billed. The monitor cannot be triggered, and an update cannot change its `frequency` or `is_active`. `POST /monitors/{monitor_id}/cancel` is accepted: the rows finish adding and the monitor stays cancelled. Uploads are rate limited; see [Rate Limits](/api-reference/rate-limits#monitors). ### Example request ```bash theme={null} curl -X POST "https://api.sixtyfour.ai/monitors/upload" \ -H "x-api-key: YOUR_API_KEY" \ -F "file=@companies.csv" \ -F 'config={ "name": "Portfolio leadership", "key_columns": ["company_name", "domain"], "struct": { "ceo_name": "Full name of the current CEO", "headcount": {"type": "int", "description": "Current number of employees"} }, "frequency": "1d", "tier": "low", "subject_type": "company" }' ``` ### Example response ```json theme={null} { "monitor_type": "research", "id": "0b7f2c6e-4a1d-4a8e-9f3b-2d5c8e1a7b90", "org_id": "acme", "team_id": "5e1d9c3a-7b2f-4e6a-8c1d-3f9a2b4c6d8e", "name": "Portfolio leadership", "frequency": "1d", "struct": { "ceo_name": "Full name of the current CEO", "headcount": { "type": "int", "description": "Current number of employees" } }, "tier": "low", "subject_type": "company", "research_plan": null, "is_active": true, "disabled_reason": "Rows are still being added.", "webhook_url": null, "webhook_event_types": ["monitor.field.changed"], "metadata": { "source_handle_id": "3f8b2c1e-9d4a-4e7b-8c2f-6a1d5e9b7c30" }, "last_run_at": null, "next_run_times": [], "row_count": 0, "active_row_count": 0, "status": "starting", "created_at": "2026-09-28T17:05:12.482Z", "updated_at": "2026-09-28T17:05:12.482Z" } ``` ### Interrupted uploads If adding rows fails, `status` becomes `failed` and `disabled_reason` explains the failure. Send the same request again to finish it: rows that were already added are kept, and only the missing rows are written. If `status` stays `starting` and `updated_at` is more than five minutes old, send the same request again. *** ## List monitors Retrieve your team's monitors, newest first. Rows are not included. ```http theme={null} GET https://api.sixtyfour.ai/monitors ``` | Query parameter | Default | Description | | - | - | - | | `include_inactive` | `false` | Include inactive monitors. | | `include_cancelled` | `true` | With `include_inactive=true`, `false` leaves out cancelled monitors. | | `limit` | `50` | Page size, 1–200. | | `cursor` | — | `next_cursor` from the previous page. | The response is `{ "monitors": [...], "next_cursor": "..." }`. A page can hold fewer than `limit` monitors, or none, before the last page. Keep requesting pages until `next_cursor` is `null`. URL-encode `next_cursor` when you send it back. *** ## Get monitor Retrieve a monitor: its settings, `status`, and row counts. Rows are not included; read them with [List rows](#list-rows). ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id} ``` Every monitor response includes `status`: | `status` | Meaning | | - | - | | `starting` | Rows from an uploaded file are still being added. Nothing is checked yet. | | `failed` | Adding rows from an uploaded file failed. Nothing is checked. Send the upload request again to finish it. | | `active` | The monitor is checking its rows on schedule. | | `paused` | Checks are paused and can be resumed. `is_active` is `false`; `disabled_reason` says why. | | `cancelled` | Future checks are permanently stopped. The monitor cannot be resumed or updated. | Paused and cancelled monitors return `next_run_times: []`. ### Disabled reasons `disabled_reason` says why a monitor or one of its rows is not being checked. It is `null` while checks run. | `disabled_reason` | Set on | Meaning | To resume | | - | - | - | - | | `Rows are still being added.` | Monitor | An uploaded file's rows are being added. `status` is `starting`. | Wait until `status` is `active`. | | Starts with `Adding rows failed` | Monitor | Adding an uploaded file's rows failed. `status` is `failed`. | Send the same upload request again. | | `paused` | Monitor | Paused with [Update monitor](#update-monitor), or created with `is_active: false`. | Update with `"is_active": true`. | | `cancelled` | Monitor | [Cancelled](#cancel-monitor). | Create a new monitor with a new `idempotency_key`. | | `team_archived` | Monitor and its rows | The monitor's team was archived. | Create a new monitor on an active team. | | `excluded` | Row | [Switched off](#switch-a-row-on-or-off). | Switch the row on. | | `key_revoked` | Row | The API key that created the monitor was revoked or expired. | Create a new monitor with an active key. | | `owner_removed` | Row | The user who created the monitor is no longer in the organization. | Create a new monitor. | | `team_removed` | Row | The user who created the monitor is no longer in its team. | Create a new monitor. | A monitor created with `is_active: false` whose rows were added in the background has `status: "paused"` and `disabled_reason: null`. Rows paused with `key_revoked`, `owner_removed`, `team_removed`, or `team_archived` pause again at their next check if switched back on. Pausing a monitor does not change its rows' `disabled_reason`. *** ## List rows Retrieve a page of a monitor's rows, switched-off rows included, in the order they were added. ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/rows ``` | Query parameter | Default | Description | | - | - | - | | `limit` | `100` | Rows per page, 1–1000. | | `cursor` | — | `next_cursor` from the previous page. | The response is `{ "rows": [...], "next_cursor": "..." }`. `next_cursor` is `null` on the last page. To read the whole table at once as CSV, [download a snapshot](#download-snapshot) instead. A row has no schedule of its own. Its `next_run_times` are the monitor's, and are empty when the row is switched off or the monitor is paused or cancelled. ### Example response ```json theme={null} { "rows": [ { "monitor_type": "research", "id": "8c3e1f2a-6b4d-4c9e-a1f7-2e5d9b3c7a10", "org_id": "acme", "team_id": "5e1d9c3a-7b2f-4e6a-8c1d-3f9a2b4c6d8e", "name": "Portfolio leadership — Acme Robotics, acmerobotics.com", "frequency": "1d", "subject": { "company_name": "Acme Robotics", "domain": "acmerobotics.com" }, "struct": { "ceo_name": "Full name of the current CEO", "headcount": { "type": "int", "description": "Current number of employees" } }, "baseline": { "ceo_name": "Dana Lee" }, "tier": "low", "subject_type": "company", "research_plan": null, "is_active": true, "disabled_reason": null, "webhook_url": "https://example.com/hooks/sixtyfour", "webhook_event_types": ["monitor.field.changed", "monitor.execution.completed", "monitor.execution.failed"], "metadata": { "portfolio_id": "p_42" }, "last_run_at": null, "next_run_times": ["2026-09-29T00:00:00+00:00"], "created_at": "2026-09-28T17:05:12.531Z", "updated_at": "2026-09-28T17:05:12.531Z" } ], "next_cursor": null } ``` *** ## List monitor runs List a monitor's runs, newest first. A run is one scheduled or triggered check of the monitor, not a workflow run. ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/runs?limit=100 ``` ```json theme={null} { "runs": [ { "run_id": "01a13cb3-4b52-763d-a4ea-c6261a32c731", "status": "completed", "started_at": "2026-10-15T06:00:00+00:00", "completed_at": "2026-10-15T06:54:17+00:00", "rows_total": 500000, "rows_checked": 500000, "rows_failed": 0, "rows_skipped": 0, "rows_changed": 1 } ], "next_cursor": null } ``` `status` is `running`, `completed`, or `failed`. Counts are final once `status` is `completed` or `failed`. `rows_checked` includes successful checks with no changes. A completed run can include failed row checks, reported in `rows_failed`. A run is `failed` when any row's check ends in an error before recording a result, or the run stops before every row finishes. A failed run sends no `monitor.run.completed` notification, and its changed rows are not available from [Get a run's changed rows](#get-a-runs-changed-rows). The other rows' changes are still recorded: read them from [row events](#list-row-events), [List latest changes](#list-latest-changes), or [snapshots](#list-snapshots). Poll this list to catch runs that end without a notification. Both run endpoints accept `limit` (default `100`, maximum `500`) and `cursor`. Pass the response's `next_cursor` unchanged as the next request's `cursor`; `null` means there are no more pages. ## Get a run's changed rows Retrieve only the rows changed by a completed run, with previous and current values for each changed field. Initial values and unchanged rows are excluded; a completed run with no changes returns `changes: []`. ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/runs/{run_id}/changes?limit=100 ``` ```json theme={null} { "run_id": "01a13cb3-4b52-763d-a4ea-c6261a32c731", "changes": [ { "row_id": "8c3e1f2a-6b4d-4c9e-a1f7-2e5d9b3c7a10", "subject": { "name": "Jane Doe" }, "fields": { "job_title": { "previous": "Manager", "current": "Director" } } } ], "next_cursor": null } ``` Each entry contains all changed fields for that row. *** ## List latest changes Retrieve, for every row, when each watched field last changed. Use it to highlight recently changed cells without reading every row's events. ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/changes ``` The response maps row ID → field name → timestamp (ISO 8601, UTC) of that field's latest `change` event. Only `change` events count; `established` values and checks that found nothing are excluded. Rows and fields that never changed are omitted, so a monitor with no changes returns `{}`. For the values themselves, [list the row's events](#list-row-events) or [download a snapshot](#download-snapshot). ```json theme={null} { "8c3e1f2a-6b4d-4c9e-a1f7-2e5d9b3c7a10": { "ceo_name": "2026-10-02T00:04:51.118000+00:00" } } ``` *** ## Get the monitor a workflow run started Retrieve the monitor that a workflow's `monitor` block started. Read its rows with [List rows](#list-rows). ```http theme={null} GET https://api.sixtyfour.ai/monitors/for-run/{workflow_run_id}/{block_id} ``` Only the monitor's team can read it. See the [`monitor` block](/api-reference/workflows/workflow-blocks#monitor). ## List a workflow run's monitors Retrieve every monitor started from one workflow run, newest first. Rows are not included. ```http theme={null} GET https://api.sixtyfour.ai/monitors/for-run/{workflow_run_id} ``` The response is a JSON array of up to 50 monitors. LinkedIn monitors are listed only when your organization has LinkedIn monitoring. For the next page, set `cursor` to `{created_at}|{id}` of the last monitor on the page, URL-encoded, with its time zone. An empty array ends the list. *** ## Update monitor Change a monitor’s settings, including how often it runs, how deeply it researches, and where it notifies. ```http theme={null} POST https://api.sixtyfour.ai/monitors/{monitor_id}/update ``` `struct` is fixed at creation. Changing watched fields requires a new monitor, with its own baseline and history. All body fields are optional: `name`, `frequency`, `tier`, `webhook_url`, `webhook_event_types`, `metadata`, `is_active`. Changes apply to every row. Send `null` for `webhook_url` or `metadata` to clear them. Changing `tier` checks tier access and the team's spend limit. For LinkedIn monitors, see [Update a LinkedIn monitor](/api-reference/monitors/linkedin-monitors#update-a-linkedin-monitor). `webhook_event_types` replaces the subscription. Omit it to keep the current one, or send `null` to restore `["monitor.field.changed"]`. `is_active: false` pauses the whole monitor; `is_active: true` resumes a paused monitor. Pausing through this endpoint sets `disabled_reason: "paused"`, and resuming clears it. A row you switched off stays off when the monitor resumes. ### Example request ```json theme={null} { "frequency": "6h", "tier": "medium", "webhook_event_types": ["monitor.field.changed", "monitor.execution.completed"] } ``` *** ## Trigger monitor Check every active row now, without waiting for the next scheduled fire. ```http theme={null} POST https://api.sixtyfour.ai/monitors/{monitor_id}/trigger ``` Returns `202` with `{ "status": "accepted", "monitor_id": "..." }`. Only a monitor whose `status` is `active` can be triggered. If a check of the monitor is already running, the triggered one is skipped, with the same response. A triggered check runs every active row through the same steps as a scheduled check and is billed the same way. See [Credits & Pricing](/guides/credits-and-pricing#monitors). *** ## Cancel monitor Permanently stop future checks for the whole monitor. Existing baselines and history remain readable. Cancellation cannot be undone. To start checks again, create a new monitor with a new idempotency key. To stop checks temporarily, [pause the monitor](#update-monitor) instead. ```http theme={null} POST https://api.sixtyfour.ai/monitors/{monitor_id}/cancel ``` Returns the monitor with `status: "cancelled"`, `is_active: false`, `disabled_reason: "cancelled"`, and `next_run_times: []`. Cancelling again returns the cancelled monitor. *** ## Get row Retrieve one row, including the values its watched fields currently hold in `baseline`. ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/rows/{row_id} ``` *** ## Switch a row on or off Include or exclude one row from checks. ```http theme={null} POST https://api.sixtyfour.ai/monitors/{monitor_id}/rows/{row_id} Content-Type: application/json {"is_active": false} ``` `is_active` is a required boolean in the JSON body. Send `true` to include the row again. Rows of a cancelled monitor cannot be changed. A switched-off row is skipped on every tick, is not billed, and keeps its baseline and history. Its `disabled_reason` is `excluded`. *** ## List row events Retrieve one row's events, newest first. ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/rows/{row_id}/events ``` | Query parameter | Default | Description | | - | - | - | | `event_group_id` | — | Only the events from one check, for example the `event_group_id` from a notification. | | `field` | — | Only one field's history. | | `include_completions` | `false` | Include `completion` events for checks that found no change. | | `limit` | `20` | 1–500. | There is no cursor: a request returns at most the newest 500 events that match its filters. Narrow it with `field` or `event_group_id`, or read older values from [snapshots](#list-snapshots) or [Export history](#export-history). ### Example response ```json theme={null} [ { "id": "e41c7a2b-9d3f-4b6e-8a1c-5f2d7e9b3a60", "event_group_id": "a7d2e9f1-3c5b-4e8a-9b1d-6f4c2e8a0b37", "event_type": "change", "field": "ceo_name", "previous_value": "Dana Lee", "current_value": "Priya Raman", "payload": { "sources": ["https://acmerobotics.com/about/leadership"], "confidence": 92, "justification": "The company's leadership page lists Priya Raman as CEO.", "hint": "Leadership page updated with a new CEO.", "metadata": { "portfolio_id": "p_42" } }, "created_at": "2026-10-02T00:04:51.118Z" } ] ``` See [Events](/api-reference/monitors/monitors-overview#events) for every event type. *** ## List snapshots List every moment the monitor's data changed, newest first, each with a download link. A snapshot is recorded for each second in which a value was established or changed, so one run of a large monitor records many snapshots. ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/snapshots ``` | Query parameter | Default | Description | | - | - | - | | `limit` | `100` | Snapshots per page, 1–500. | | `before` | — | Only snapshots before this ISO 8601 timestamp. Pass the last `as_of` from the previous page to read the next page. | | `start` | — | Only snapshots at or after this ISO 8601 timestamp. | | `end` | — | Only snapshots at or before this ISO 8601 timestamp. Must not be earlier than `start`. | ### Example response ```json theme={null} [ { "as_of": "2026-10-02T00:04:51+00:00", "changed_fields": ["ceo_name"], "subjects_changed": 1, "url": "https://api.sixtyfour.ai/monitors/0b7f2c6e-4a1d-4a8e-9f3b-2d5c8e1a7b90/snapshots/2026-10-02T00%3A04%3A51%2B00%3A00" } ] ``` *** ## Download snapshot Download the table as it stood at a moment, as CSV: one row per subject, with the key columns followed by the watched fields. ```http theme={null} GET https://api.sixtyfour.ai/monitors/{monitor_id}/snapshots/{as_of} ``` `as_of` is an `as_of` value from the snapshot list, URL-encoded, or `latest` for the current values. Any ISO 8601 timestamp is accepted and returns the values held at that time. | Query parameter | Default | Description | | - | - | - | | `limit` | — | Return only the first 1–1000 rows, for a preview. Omit it for the whole table. | Cells hold the stored values unchanged, including any a spreadsheet would run as a formula. *** ## Export history Download many snapshots at once as a ZIP of CSVs, one file per snapshot. Each file has the same columns as [Download snapshot](#download-snapshot) and is named after its timestamp, with `:` replaced by `-`. ```http theme={null} POST https://api.sixtyfour.ai/monitors/{monitor_id}/history-export ``` Send the snapshot selection in the JSON body. CSV cells hold the stored values unchanged, as in [Download snapshot](#download-snapshot). | Field | Type | Required | Description | | - | - | - | - | | `timestamps` | array of strings | No | Snapshot `as_of` values to export, 1–5,000. Omit to export every snapshot between `start` and `end`. | | `start` | string | No | ISO 8601 timestamp, inclusive. Omit to start from the first snapshot. | | `end` | string | No | ISO 8601 timestamp, inclusive. Defaults to now. Must not be earlier than `start`. | | `exclude` | array of strings | No | Snapshot `as_of` values to leave out, up to 5,000. | Timestamps without a time zone are read as UTC. An export is limited to 5,000 snapshots and 50 MiB of CSV. A larger export, one that takes too long to build, or a range with no snapshots is refused before any data is sent; select fewer snapshots or a shorter range. Exports are rate limited; see [Rate Limits](/api-reference/rate-limits#monitors). On `429` or `503`, wait for `Retry-After` and send the request again, as the examples do. ```bash cURL theme={null} # --retry waits for Retry-After on 429 and 503 responses (curl 7.66 or later). curl -X POST "https://api.sixtyfour.ai/monitors/MONITOR_ID/history-export" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"start": "2026-01-01T00:00:00Z", "end": "2026-03-31T23:59:59Z"}' \ --retry 5 \ -o monitor-history.zip ``` ```python Python theme={null} import time import requests for _ in range(5): response = requests.post( "https://api.sixtyfour.ai/monitors/MONITOR_ID/history-export", headers={"x-api-key": "YOUR_API_KEY"}, json={"start": "2026-01-01T00:00:00Z", "end": "2026-03-31T23:59:59Z"}, ) if response.status_code not in (429, 503): break time.sleep(int(response.headers.get("Retry-After", "15"))) response.raise_for_status() with open("monitor-history.zip", "wb") as f: f.write(response.content) ``` ```javascript JavaScript theme={null} import { writeFile } from "node:fs/promises"; let response; for (let attempt = 0; attempt < 5; attempt++) { response = await fetch( "https://api.sixtyfour.ai/monitors/MONITOR_ID/history-export", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ start: "2026-01-01T00:00:00Z", end: "2026-03-31T23:59:59Z" }), }, ); if (response.status !== 429 && response.status !== 503) break; const waitSeconds = Number(response.headers.get("Retry-After") ?? 15); await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000)); } if (!response.ok) throw new Error(`Export failed: ${response.status}`); await writeFile("monitor-history.zip", Buffer.from(await response.arrayBuffer())); ``` *** ## Example usage ### Create from a file Upload a file, then wait until every row is added. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/monitors/upload" \ -H "x-api-key: YOUR_API_KEY" \ -F "file=@companies.csv" \ -F 'config={"name": "Portfolio leadership", "key_columns": ["company_name", "domain"], "struct": {"ceo_name": "Full name of the current CEO"}, "frequency": "1d", "subject_type": "company"}' curl "https://api.sixtyfour.ai/monitors/MONITOR_ID" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import json import time import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.sixtyfour.ai" headers = {"x-api-key": API_KEY} config = { "name": "Portfolio leadership", "key_columns": ["company_name", "domain"], "struct": {"ceo_name": "Full name of the current CEO"}, "frequency": "1d", "subject_type": "company", } with open("companies.csv", "rb") as f: response = requests.post( f"{BASE_URL}/monitors/upload", headers=headers, files={"file": ("companies.csv", f, "text/csv")}, data={"config": json.dumps(config)}, ) response.raise_for_status() monitor = response.json() while monitor["status"] == "starting": time.sleep(10) poll = requests.get(f"{BASE_URL}/monitors/{monitor['id']}", headers=headers) if poll.status_code in (429, 503): time.sleep(int(poll.headers.get("Retry-After", "10"))) continue poll.raise_for_status() monitor = poll.json() if monitor["status"] == "failed": raise RuntimeError(monitor["disabled_reason"]) print(monitor["status"], monitor["row_count"]) ``` ```javascript JavaScript theme={null} import { readFile } from "node:fs/promises"; const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": API_KEY }; const form = new FormData(); form.append( "file", new Blob([await readFile("companies.csv")], { type: "text/csv" }), "companies.csv", ); form.append( "config", JSON.stringify({ name: "Portfolio leadership", key_columns: ["company_name", "domain"], struct: { ceo_name: "Full name of the current CEO" }, frequency: "1d", subject_type: "company", }), ); const response = await fetch(`${BASE_URL}/monitors/upload`, { method: "POST", headers, body: form, }); if (!response.ok) { throw new Error(`Failed to create monitor: ${await response.text()}`); } let monitor = await response.json(); while (monitor.status === "starting") { await new Promise((resolve) => setTimeout(resolve, 10000)); const poll = await fetch(`${BASE_URL}/monitors/${monitor.id}`, { headers }); if (poll.status === 429 || poll.status === 503) { const waitSeconds = Number(poll.headers.get("Retry-After") ?? 10); await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000)); continue; } if (!poll.ok) { throw new Error(`Failed to read monitor: ${await poll.text()}`); } monitor = await poll.json(); } if (monitor.status === "failed") { throw new Error(monitor.disabled_reason); } console.log(monitor.status, monitor.row_count); ``` ### Create from rows Create a monitor, then read the changes for one of its rows. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/monitors" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Portfolio leadership", "key_columns": ["company_name", "domain"], "rows": [ {"company_name": "Acme Robotics", "domain": "acmerobotics.com", "ceo_name": "Dana Lee"} ], "struct": {"ceo_name": "Full name of the current CEO"}, "frequency": "1d", "subject_type": "company" }' curl "https://api.sixtyfour.ai/monitors/MONITOR_ID/rows" \ -H "x-api-key: YOUR_API_KEY" curl "https://api.sixtyfour.ai/monitors/MONITOR_ID/rows/ROW_ID/events?field=ceo_name" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.sixtyfour.ai" headers = {"x-api-key": API_KEY, "Content-Type": "application/json"} response = requests.post( f"{BASE_URL}/monitors", headers=headers, json={ "name": "Portfolio leadership", "key_columns": ["company_name", "domain"], "rows": [ {"company_name": "Acme Robotics", "domain": "acmerobotics.com", "ceo_name": "Dana Lee"} ], "struct": {"ceo_name": "Full name of the current CEO"}, "frequency": "1d", "subject_type": "company", }, ) response.raise_for_status() monitor = response.json() rows = requests.get(f"{BASE_URL}/monitors/{monitor['id']}/rows", headers=headers) rows.raise_for_status() row_id = rows.json()["rows"][0]["id"] events = requests.get( f"{BASE_URL}/monitors/{monitor['id']}/rows/{row_id}/events", headers=headers, params={"field": "ceo_name"}, ) events.raise_for_status() for event in events.json(): print(event["event_type"], event["previous_value"], "->", event["current_value"]) ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": API_KEY, "Content-Type": "application/json" }; const response = await fetch(`${BASE_URL}/monitors`, { method: "POST", headers, body: JSON.stringify({ name: "Portfolio leadership", key_columns: ["company_name", "domain"], rows: [ { company_name: "Acme Robotics", domain: "acmerobotics.com", ceo_name: "Dana Lee" }, ], struct: { ceo_name: "Full name of the current CEO" }, frequency: "1d", subject_type: "company", }), }); if (!response.ok) { throw new Error(`Failed to create monitor: ${await response.text()}`); } const monitor = await response.json(); const rowsResponse = await fetch(`${BASE_URL}/monitors/${monitor.id}/rows`, { headers, }); if (!rowsResponse.ok) { throw new Error(`Failed to read rows: ${await rowsResponse.text()}`); } const rowId = (await rowsResponse.json()).rows[0].id; const eventsResponse = await fetch( `${BASE_URL}/monitors/${monitor.id}/rows/${rowId}/events?field=ceo_name`, { headers }, ); const events = await eventsResponse.json(); for (const event of events) { console.log(event.event_type, event.previous_value, "->", event.current_value); } ``` ## Create a LinkedIn monitor from multiple profiles `POST /monitors` accepts multiple profiles with `monitor_type: "linkedin"`, `subject_type: "lead"`, and `key_columns: ["linkedin_url"]`. Each row contains only a LinkedIn person URL. The `struct` keys are selected field keys from discovery. There is one monitor ID and one schedule for the table. ```json theme={null} { "name": "Profile changes", "monitor_type": "linkedin", "subject_type": "lead", "key_columns": ["linkedin_url"], "rows": [ { "linkedin_url": "https://www.linkedin.com/in/example-one" }, { "linkedin_url": "https://www.linkedin.com/in/example-two" } ], "struct": { "headline": "Headline", "skills": "Skills" }, "frequency": "1w", "first_check": "next_tick" } ``` Frequency, upload, and duplicate-profile rules for LinkedIn monitors are in [LinkedIn Monitors](/api-reference/monitors/linkedin-monitors#csv-upload). ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for more information. ## Errors For error responses (400, 403, 404, 409, etc.), see [Handling Errors](/api-reference/errors). # Monitors Overview Source: https://docs.sixtyfour.ai/api-reference/monitors/monitors-overview How monitors keep watched fields on a table of people or companies current, and how checks, events, and schedules work. A monitor watches a table of people or companies and re-checks chosen fields on a schedule. When a value changes, it records the change with its sources and can notify your webhook. > [!NOTE] > > Monitors require access for your organization. Contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai). Create and manage monitors through the [Monitor Endpoints](/api-reference/monitors/monitors-endpoints), the workflow [`monitor` block](/api-reference/workflows/workflow-blocks#monitor), or the Monitors page in the Sixtyfour web app. [LinkedIn monitors](/api-reference/monitors/linkedin-monitors) compare selected stored profile fields without research. They share the monitor APIs and notifications. The check flow, tiers, and frequency units below describe research monitors. ## Concepts | Term | Meaning | | - | - | | Monitor | One table with one schedule. Its settings (watched fields, frequency, tier, webhook) apply to every row. | | Row | One person or company in the table, with its own current values, event history, and on/off switch. | | Subject | The values that identify a row, taken from its `key_columns` (for example `company_name` and `domain`). A subject never changes. | | Watched fields | The fields the monitor keeps current, passed as `struct`. Same shape as the enrichment `struct`: a description string, or an object with `type`, `description`, and `subfields` for nested fields. | | Baseline | The values a row currently holds for its watched fields. Every reported change updates it. | | Subject type | `company` re-derives fields with Company Intelligence. `lead` uses People Intelligence. | | Tier | Depth of the full re-check: `low`, `medium`, or `high`. | | Snapshot | The whole table as it stood at a moment. A snapshot is recorded for each second in which any value is established or changes. | A column cannot be both a key column and a watched field. Two rows with the same subject values are rejected; the workflow `monitor` block watches such rows once. ## How a check works Every tick of the schedule checks each active row. ```mermaid theme={null} flowchart TD A[Schedule tick] --> B[Each active row] B --> C{Baseline setup still needed?} C -- Yes --> D[Enrich watched fields at the monitor tier] D --> E[Report uploaded values that are already stale] C -- No --> F[Probe: has anything changed since the last check?] F -- No --> G[completion event] F -- Yes --> H[Re-check watched fields at the monitor tier] H --> I[change event per field that moved] E --> J[Webhook notification] G --> J I --> J ``` * Baseline setup: a watched field whose name matches a column in the input row starts from that cell. A row missing any watched value is enriched at the monitor's tier: missing fields get their first value, and every supplied value that is already out of date is reported as a `change`. Rows that supply every watched value skip setup and start with the probe on the schedule. * Probe: later checks read the row's known sources and new search results to decide whether any watched field changed. A check with no likely change ends here with a `completion` event. * Re-check: when the probe finds a likely change, the monitor re-derives the watched fields at its tier and records one `change` event per field whose value moved, with sources, confidence, and a justification. A value equal to the field's last reported value is not reported again. * Failures: a check that cannot finish records an `error` event and leaves its time window open. The next check re-examines the same window, so no change is lost. ## Events Every event from one check shares an `event_group_id`. | `event_type` | Meaning | | - | - | | `change` | A watched field's value changed. Carries `previous_value`, `current_value`, and a `payload` with `sources`, `confidence` (0–100, or `null` when not scored), `justification`, and the monitor's `metadata`. A change found by a re-check also carries `hint`, what the probe saw change. `payload.first_run` is `true` when setup found a supplied value stale. | | `established` | A field received its first value, either supplied at creation or found by a check. Not a change. | | `completion` | The check ran and found no change. `payload` describes what was checked and the time window (`window_from`, `window_to`). Hidden from event listings unless `include_completions=true`. | | `error` | The check could not run or finish. `payload.reason` says why: `insufficient balance`, or `probe did not run`, `re-check did not run`, or `baseline re-check did not run` when that step failed; the next check retries the same window. | [LinkedIn monitors](/api-reference/monitors/linkedin-monitors#event-payloads) write the same event types with different payloads and their own error reasons. ## Values Research monitors store every watched value as text. A list answer is stored as its items joined by `; `, and an object as `key=value` pairs joined by `; `. A re-check reports a change only when the new value still differs from the baseline after these rules: * Case, repeated whitespace, and trailing punctuation are ignored. * Numbers compare by value: `1981.0` equals `1981`, and `1,200` equals `1200`. * A list answer compares without regard to order. * An empty answer, such as `unknown` or `not found`, keeps the previous value and reports nothing. * A reworded value judged to state the same fact, such as a school's full name in place of its short name, is not reported. Starting values from row cells are stored as text too. Send them as strings, with list items joined by `; `. A list or object cell is stored in a different text form, and the first re-check can report it as a change. LinkedIn monitors compare stored profile values exactly. Text values are returned as they are; lists, numbers, and booleans are returned as compact JSON, such as `["Python","SQL"]` or `true`. Any difference is a change, including a list in a new order. ## How to read changes | To read | Use | Notes | | - | - | - | | One row's check, as it finishes | A [row notification](/api-reference/monitors/monitor-notifications#row-notification-payload), then [List row events](/api-reference/monitors/monitors-endpoints#list-row-events) with its `event_group_id` | `monitor.field.changed` covers checks with changes. Add `monitor.execution.completed` and `monitor.execution.failed` to hear about every check. | | Every row one run changed | [`monitor.run.completed`](/api-reference/monitors/monitor-notifications#whole-monitor-completion) or [List monitor runs](/api-reference/monitors/monitors-endpoints#list-monitor-runs), then [Get a run's changed rows](/api-reference/monitors/monitors-endpoints#get-a-runs-changed-rows) | Only for runs whose `status` is `completed`. Poll the runs list as well: a `failed` run sends no notification. | | When each cell last changed | [List latest changes](/api-reference/monitors/monitors-endpoints#list-latest-changes) | Timestamps for the whole monitor in one request, without values. | | One field's history on one row | [List row events](/api-reference/monitors/monitors-endpoints#list-row-events) with `field` | At most the newest 500 events. | | The whole table at one moment | [Download snapshot](/api-reference/monitors/monitors-endpoints#download-snapshot) with an `as_of` from [List snapshots](/api-reference/monitors/monitors-endpoints#list-snapshots), or `latest` | One CSV, one row per subject. | | The whole table across a period | [Export history](/api-reference/monitors/monitors-endpoints#export-history) | A ZIP with one CSV per snapshot. | ## Frequency On a research monitor, `frequency` is a whole number followed by a unit: `m` (minutes), `h` (hours), `d` (days), or `w` (weeks), for example `90m`, `6h`, `1d`, or `2w`. The default is `1d`. LinkedIn monitors read `m` as 30 days, not minutes, and have their own default and range. See [Create a LinkedIn monitor](/api-reference/monitors/linkedin-monitors#create-a-linkedin-monitor). * The supported range is `1h` to `365d`, so `1m` (one minute) is rejected on a research monitor. * Schedules align to UTC. An hourly monitor fires at the top of each hour, a daily monitor at 00:00 UTC, and a weekly monitor (`1w` or `7d`) on Thursdays at 00:00 UTC. Longer intervals do not follow the calendar: a `30d` monitor fires every 30 days, not on the same date each month. `next_run_times` on every monitor response lists the next three fire times. * `first_check: "now"` (default) runs setup immediately for rows missing watched values. Rows that supply every watched value wait for the schedule. `first_check: "next_tick"` defers every row to the first scheduled fire. * Checks never overlap. If a tick is still running when the next one is due, the next one is skipped. ## Billing For setup, probe, and re-check charges, see the [Credits & Pricing Guide](/guides/credits-and-pricing#monitors). Creating a research monitor, or changing its `tier`, checks tier access and the team's spend limit but not your balance. Each check is paid for when it runs. A research check your balance cannot cover is skipped and records an `error` event. For LinkedIn monitors, see [Check behavior](/api-reference/monitors/linkedin-monitors#check-behavior). ## Access and ownership * A monitor belongs to the team of the credential that created it. Only that team can see or change it. * A monitor runs on behalf of the API key or user that created it. If that key is revoked or expires, the user leaves the organization or team, or the team is archived, its rows pause; see [Disabled reasons](/api-reference/monitors/monitors-endpoints#disabled-reasons). * When the team's spend limit is reached, checks are skipped until the limit clears. ## Errors For error responses (400, 403, 404, 409, etc.), see [Handling Errors](/api-reference/errors). # Retrieve Monitor Source: https://docs.sixtyfour.ai/api-reference/monitors/retrieve-monitor https://api.sixtyfour.ai/openapi.json get /monitors/{monitor_id} The monitor: its settings, status and row counts. Its rows are read from `GET /monitors/{monitor_id}/rows`. # Retrieve One Row Source: https://docs.sixtyfour.ai/api-reference/monitors/retrieve-one-row https://api.sixtyfour.ai/openapi.json get /monitors/{monitor_id}/rows/{row_id} One subject of the table, with everything its fields currently hold. # Switch A Row On Or Off Source: https://docs.sixtyfour.ai/api-reference/monitors/switch-a-row-on-or-off https://api.sixtyfour.ai/openapi.json post /monitors/{monitor_id}/rows/{row_id} Include or exclude one row from the monitor's checks. A switched-off row is skipped when the monitor runs, so it costs nothing, and its baseline and history are kept. # Trigger Monitor Run Source: https://docs.sixtyfour.ai/api-reference/monitors/trigger-monitor-run https://api.sixtyfour.ai/openapi.json post /monitors/{monitor_id}/trigger Run every active row now, without waiting for the next interval. # Update Monitor Source: https://docs.sixtyfour.ai/api-reference/monitors/update-monitor https://api.sixtyfour.ai/openapi.json post /monitors/{monitor_id}/update Change monitor settings; changes apply to every row. Watched fields are fixed at creation. Setting `is_active` does not turn back on rows you switched off. # Rate Limits Source: https://docs.sixtyfour.ai/api-reference/rate-limits Every Sixtyfour API rate and concurrency limit, the headers a 429 carries, and how to retry. Limits apply per organization. Every API key and OAuth token in an organization, including keys used through the [MCP server](/developer-tools/mcp-setup), draws on the same budgets. A request over a rate limit returns `429` with a `Retry-After` header, and nothing runs. ## How limits are counted | Kind | How it counts | A `429` carries | | - | - | - | | Per-minute limit | Requests in any 60-second window | `Retry-After` | | Burst budget | Up to the burst at once, then a steady refill. 300 per minute with a burst of 15 allows 15 requests at once, then 5 a second | `Retry-After` and the `X-RateLimit-*` headers | A request counts once Sixtyfour accepts it, even if it fails afterwards. Each endpoint has its own budget unless a table below says it shares one. ## Status polling | Endpoint | Limit | | - | - | | [`GET /job-status/{job_id}`](/api-reference/enrichment/job-status) and `GET /workflows/runs/{run_id}/live_status` | 300 per minute, burst 15, one budget shared by both | At one poll every 10 seconds, the budget covers 50 jobs or runs in flight, with the polls spread out rather than sent together. For more work than that, receive results by webhook and poll only as a fallback: pass `webhook_url` to async jobs and add an `outgoing_webhook` block to workflows. See [Outgoing Webhooks](/api-reference/webhooks/outgoing). A `429` on a status read does not affect the job; read again after `Retry-After`. [Async Jobs](/api-reference/async-jobs#polling) has a polling helper. ## Limits by endpoint ### Intelligence and contact | Endpoint | Limit | Notes | | - | - | - | | `POST /bulk-intelligence/people`, `POST /bulk-intelligence/company` | 10 per minute each | A replay with the same `Idempotency-Key` does not count | | `POST /verify-email` | 120 per minute | | | `GET /check-balance` | 60 per minute | | ### Workflows | Endpoint | Limit | Notes | | - | - | - | | `POST /workflows/run` | 120 per minute | A retry with the same `Idempotency-Key` that returns the run it already started does not count | | `GET /workflows/runs/{run_id}/results/download-links` | 5 per second, burst 15 | Burst budget. A refused link request does not affect the run or its results; retry it instead of starting a new run. Each `download_url` lasts 15 minutes and draws on the file-download budget, not this one. See [Workflow result download retries](/api-reference/errors#workflow-result-download-retries) | | Downloading a file from a `download_url` | 60 per minute, burst 5 | Burst budget, shared by workflow result links, webhook result links, and `GET /search/download` links | | `GET /templates`, `GET /templates/{template_key}` | 120 per minute | One budget for both. Requests without credentials count per IP address | | `GET /templates/available` | 120 per minute | | ### Search | Endpoint | Limit | Notes | | - | - | - | | `POST /search/start-deep-search` | 200 per minute | Also 5 running at a time. See [Concurrency limits](#concurrency-limits) | | `POST /search/query` | 200 per minute | | | `POST /search/start-filter-search` | 200 per minute | Deprecated; use `POST /search/query` | | `POST /search/filter-field-values` | 200 per minute | | | `GET /search/filter-capabilities` | 300 per minute | | | `POST /search/lookalike` | 200 per minute | `GET /search/lookalike/{task_id}` is not limited | | `POST /search/export` | 10 per minute | | ### Monitors | Endpoint | Limit | Notes | | - | - | - | | `POST /monitors/upload` | 10 per minute | Replays count | | `POST /monitors/{monitor_id}/history-export` | 300 per minute, burst 15 | Burst budget. Also 5 builds at a time. See [Concurrency limits](#concurrency-limits) | ### Atlas | Endpoint | Limit | Notes | | - | - | - | | `/v1/atlas/*` | 120 per minute | One budget across every Atlas endpoint. Each connection and reconnection to the event stream counts | ### Webhooks | Endpoint | Limit | Notes | | - | - | - | | `POST /webhooks/test-events` | 10 per minute | | ### Skills | Endpoint | Limit | Notes | | - | - | - | | `/skills` and `/skills/{name}` | 60 per minute per API key | One budget across the Skills endpoints | ### Media | Endpoint | Limit | Notes | | - | - | - | | `GET /v1/media/{token}` | 600 per minute per IP address | | | `GET /v1/media/profiles/{key}` | 600 per minute per IP address | Separate budget | ### OAuth tokens `POST /oauth/token` issues up to 60 tokens per minute per client. Requests that fail client authentication are also limited to 25 per minute per client and to 120 per minute shared with other callers. Reuse each token until shortly before its `expires_in`. See [Using OAuth Clients](/guides/oauth-clients). ## Concurrency limits ### Deep search Each organization runs up to 5 deep searches at a time, people and company combined. A start above that is never refused with `429`. It returns `200` with `status: "running"`, and the search then ends `failed` with this `error`: ```text theme={null} Concurrency limit reached: max 5 concurrent deep_search requests. Please wait for an existing request to complete. ``` Start a new deep search only after one of yours reaches a final status. ### History exports Each organization runs up to 5 history-export builds at a time. A sixth returns `429` with `Retry-After: 5` and the `detail` `Five history exports are already building for this organization. Retry later.` ## Response headers | Header | Sent on | Meaning | | - | - | - | | `Retry-After` | Every `429`, and `503` responses you can retry | Seconds to wait before retrying | | `X-RateLimit-Requests` | `429` from a burst budget | Requests added per period | | `X-RateLimit-Period` | `429` from a burst budget | The period, in seconds | | `X-RateLimit-Burst` | `429` from a burst budget | Requests available at once after idling | Successful responses carry no `X-RateLimit-*` headers. A `429` from the status polling budget: ```http theme={null} HTTP/1.1 429 Too Many Requests Retry-After: 1 X-RateLimit-Requests: 300 X-RateLimit-Period: 60 X-RateLimit-Burst: 15 Content-Type: application/json {"detail": "Rate limit exceeded: 300 requests per minute per organization, with bursts of up to 15. Retry after the delay in Retry-After. Do not restart the job.", "code": "rate_limited", "correlation_id": "enrich_3f8b2c1e-9d4a-4c1b-8e2f-6a7b5c4d3e21"} ``` A `429` from a per-minute limit: ```json theme={null} { "detail": "Rate limit exceeded: 120 requests per minute. Retry after 12 seconds.", "code": "rate_limited", "correlation_id": "enrich_3f8b2c1e-9d4a-4c1b-8e2f-6a7b5c4d3e21" } ``` ## Retry after a 429 * Wait at least `Retry-After` seconds, then send the same request again. * Workers that share an organization share its budgets. Coordinate them, and add a small random delay after the server's minimum. * Stop after a bounded number of attempts. * For many records, use async endpoints, [Bulk Intelligence](/api-reference/endpoint/bulk-intelligence), or the [Workflows API](/api-reference/workflows/workflows-overview) instead of many synchronous calls. This helper retries `429` and `503` responses after `Retry-After`: ```python theme={null} import time import requests def post_with_retry(url, headers, data, max_retries=5): for attempt in range(max_retries): response = requests.post(url, headers=headers, json=data) if response.status_code in (429, 503): time.sleep(int(response.headers.get("Retry-After", 2 ** attempt))) continue response.raise_for_status() return response.json() raise Exception("Max retries exceeded") ``` Contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai) if a limit blocks your integration. For other status codes, see [Handling Errors](/api-reference/errors). # Create Exclusion List Source: https://docs.sixtyfour.ai/api-reference/search/create-exclusion-list https://api.sixtyfour.ai/openapi.json post /search/exclusion-lists Create an exclusion list. It builds in the background: poll `GET /search/exclusion-lists/{list_id}` until `status` is `ready`. A list cannot be changed; create a new one and delete the old. Up to 20 active lists per organization (40 with extended exclusion limits). # Delete Exclusion List Source: https://docs.sixtyfour.ai/api-reference/search/delete-exclusion-list https://api.sixtyfour.ai/openapi.json delete /search/exclusion-lists/{list_id} Delete an exclusion list. # Download Search Results Source: https://docs.sixtyfour.ai/api-reference/search/download-search-results https://api.sixtyfour.ai/openapi.json get /search/download Generate a 15-minute signed download URL for a search result. # Filter Field Values Source: https://docs.sixtyfour.ai/api-reference/search/filter-field-values https://api.sixtyfour.ai/openapi.json post /search/filter-field-values Get the top values by count for a single filter-search field. # Filter Search Source: https://docs.sixtyfour.ai/api-reference/search/filter-search Search people or companies with structured filters, discover fields and values, and page through results. Search people or companies by structured fields such as location, headcount, industry, or job title. > [card] [API Reference](/api-reference/search/search-query) > > See the full request/response schema and parameters in the API Reference. > [!WARNING] > > `POST /search/start-filter-search` is deprecated and returns a `Deprecation` header. Use `POST /search/query`, which takes the same filters and returns the same response. ## Core concepts | Term | Description | | - | - | | `simple_filters` | The primary query format. Mongo-style operators such as `$eq`, `$in`, `$gte`, `$match`, and `$and`. | | `filters` | A JSON query DSL for cases `simple_filters` can't express. Validated against the limits in `GET /search/filter-capabilities`. | | `query` | A natural language query, as an alternative to filters. The response echoes the structured `parsed_query` it was turned into. | | `exclude_entity_ids`, `exclude_list_ids` | Leave specific people or companies out of results. See [Search Exclusions](/api-reference/search/search-exclusions). | | `fields` | Field metadata from `GET /search/filter-capabilities`: which fields are queryable, sortable, rangeable, and support top values. | | `filter_snippet` | A ready-to-use exact-match `filters` clause returned with each top value. | | `cursor` | Pagination token returned as `next_cursor`. Send it as the only field to get the next page. See [cursor rules](/api-reference/search/search-results#paginate-with-cursors). | ## Recommended flow ```mermaid theme={null} flowchart TD A["GET
/search/filter-capabilities"] -->|"Discover valid fields & limits"| B["POST
/search/filter-field-values"] B -->|"Inspect top values for a field"| C["POST
/search/query"] C --> D{has_more?} D -->|"true — send {cursor} only"| C D -->|false| E[Done] ``` ## Simple filters ### Top-level shape ```json theme={null} { "simple_filters": { "hq_country_iso2": { "$eq": "US" }, "employees_count": { "$gte": 100, "$lte": 5000 } } } ``` ### Supported operators The capabilities response exposes supported simple-query operators in `simple_query_operators`. | Operator | Value | Description | | - | - | - | | `$eq`, `$ne` | Scalar | Exact match or exact exclusion. | | `$gt`, `$gte`, `$lt`, `$lte` | Number or date | Numeric or date range comparisons. The field must be `rangeable`. | | `$in`, `$nin` | Array of 1–25 scalars | Match or exclude multiple exact values. | | `$exists` | Boolean | Match records where a field is present or absent. | | `$match`, `$phrase` | String, up to 200 characters | Text matching for one text or keyword field. On a text field, `$match` needs any word of the value and `$phrase` needs every word. | | `$and`, `$or`, `$nor` | Array of 1–25 filter objects | Logical composition. | | `$not` | Filter object | Negate a condition. | | `$elemMatch` | Object keyed by full field paths | Match nested array elements. See [Nested array matching](#nested-array-matching). | | `$search` | Object with `query`, `fields`, and optional `type` and `operator` | Full-text search across up to 25 queryable fields. `query` is up to 200 characters. | | `$sort` | Object, or array of single-field objects, with up to 2 fields | Sort results in search requests. Each direction is `asc`, `desc`, `1`, `-1`, or `{"order": "desc", "missing": "_last"}`. | | `$limit` | Integer from 1 to 5,000 | Limit result count in search requests. | ### Nested array matching `$elemMatch` queries fields inside nested arrays. It is keyed on the field's `nested_path` from capabilities, which is not always the outermost array: `currentCompanies.company.companyId` has `nested_path: "currentCompanies"`, and `currentCompanies.positions.title` has `nested_path: "currentCompanies.positions"`, so `$elemMatch` on `currentCompanies` cannot reference a positions field. > [!WARNING] > > Inside `$elemMatch`, use the field's full dotted path. A relative path with a dot, such as `company.companyId`, returns a `400`, although its message suggests a relative path. In people mode, conditions inside one `$elemMatch` on `currentCompanies`, `previousCompanies`, `educations`, or a `positions` path hold on the same entry. On any other path, each condition can match a different entry. Find people currently at a specific company by numeric LinkedIn company ID, which company results expose as `source_id`: ```json theme={null} { "mode": "people", "simple_filters": { "currentCompanies": { "$elemMatch": { "currentCompanies.company.companyId": 1234567 } } } } ``` ### Excluding results There are two ways to exclude records: * Use `simple_filters` operators such as `$ne`, `$nin`, `$not`, and `$nor` to exclude records that match field conditions. * Use the top-level exclusion fields — `exclude_entity_ids` or `exclude_list_ids` — to suppress specific people or companies by ID. See [Search Exclusions](/api-reference/search/search-exclusions) for identifier formats, saved lists, and limits. ID-based exclusions apply after matching, so a page can hold fewer rows than `page_size`. ### Query examples Find a company by website domain: ```json theme={null} { "mode": "company", "simple_filters": { "website_domain": "acme.com" } } ``` Exclude companies in specific industries: ```json theme={null} { "mode": "company", "simple_filters": { "industry": { "$nin": ["Staffing", "Recruiting"] } } } ``` Exclude records that match any disallowed condition: ```json theme={null} { "mode": "company", "simple_filters": { "$nor": [ { "industry": { "$eq": "Staffing" } }, { "categories_and_keywords": { "$match": "recruiting" } } ] } } ``` Find US-based companies sorted by employee count descending, capped to 8 total results: ```json theme={null} { "mode": "company", "simple_filters": { "hq_country_iso2": { "$eq": "US" }, "$sort": [{ "employees_count": "desc" }], "$limit": 8 } } ``` Find Canadian companies that have raised any amount and mention technology in categories or keywords: ```json theme={null} { "mode": "company", "simple_filters": { "$and": [ { "hq_country_iso2": { "$eq": "CA" } }, { "last_funding_round_amount_raised": { "$gt": 0 } }, { "categories_and_keywords": { "$match": "technology" } } ] } } ``` Search several fields at once with `$search`: ```json theme={null} { "mode": "company", "simple_filters": { "$search": { "query": "cloud security", "fields": ["description", "categories_and_keywords"], "operator": "and" } } } ``` Find people by current job title: ```json theme={null} { "mode": "people", "simple_filters": { "currentCompanies.positions.title": { "$match": "engineering manager" } } } ``` Fields with a doubly nested path accept `$elemMatch` on the inner nested path: ```json theme={null} { "mode": "people", "simple_filters": { "currentCompanies.positions": { "$elemMatch": { "currentCompanies.positions.title": { "$match": "engineering manager" } } } } } ``` ## DSL filters ### Top-level shape ```json theme={null} { "filters": { "query": { ... }, "sort": [ ... ], "size": 100 } } ``` ### Supported query clauses | Clause | Description | | - | - | | `bool` | Combine conditions with `must`, `filter`, `should`, and `must_not`. | | `term` | Exact-match query for a single value. | | `terms` | Exact-match query for multiple values. | | `range` | Numeric or date range with `gt`, `gte`, `lt`, `lte`. | | `exists` | Match records where a field is present. | | `match` | Text match. On a text field, any word of the value, or every word with `"operator": "and"`. | | `match_phrase` | Text match. On a text field, every word of the value. | | `multi_match` | Search the same value across multiple fields. | | `nested` | Query nested arrays and nested objects. Conditions inside it match as they do in [`$elemMatch`](#nested-array-matching). | | `match_all` | Match all records in the selected mode. | ### Query examples Range filter: ```json theme={null} { "range": { "employees_count": { "gte": 100, "lte": 5000 } } } ``` Multi-field full-text search: ```json theme={null} { "multi_match": { "query": "cloud security", "fields": [ "description", "categories_and_keywords" ], "type": "best_fields", "operator": "and" } } ``` Nested query: ```json theme={null} { "nested": { "path": "technologies_used", "query": { "term": { "technologies_used.technology": "Kubernetes" } } } } ``` Exclude with `must_not`: ```json theme={null} { "bool": { "must": [ { "term": { "hq_country_iso2": "US" } } ], "must_not": [ { "term": { "industry": "Staffing" } } ] } } ``` ## Filter capabilities ```http theme={null} GET https://api.sixtyfour.ai/search/filter-capabilities ``` Returns every field you can query, sort, range, or get top values for, plus the `simple_filters` operators and DSL limits. > [!TIP] > > Call this first to discover what is filterable. Only fields in `fields`, and their `.keyword` and `.raw` forms, can be queried. ### Query parameters | Parameter | Type | Description | | - | - | - | | `mode` | string | `people` or `company`. Defaults to `company`. | ### Example request ```bash theme={null} curl -X GET "https://api.sixtyfour.ai/search/filter-capabilities?mode=company" \ -H "x-api-key: YOUR_API_KEY" ``` ### Response shape | Field | Type | Description | | - | - | - | | `mode` | string | The mode these capabilities describe. | | `fields` | array | Every queryable field in the selected mode. See [Field entry](#field-entry). | | `nested_paths` | string\[] | Top-level paths that hold nested arrays or objects. | | `simple_query_operators` | string\[] | Operators allowed in `simple_filters` (`$eq`, `$in`, `$gte`, `$match`, etc.). | | `limits` | object | Hard limits applied to `filters` queries. See [Limits](#limits). | | `price_per_result_credits` | number | Credits per result that `POST /search/query` charges this credential, or `0` when your searches are not charged. See [Credits & Pricing](/guides/credits-and-pricing#search). | | `cache_ttl_seconds` | integer | Seconds you can cache this response client-side. | | `mapping_hash` | string | Hash of the field list. Changes when fields are added or removed. | ### Limits | Limit | Description | | - | - | | `max_query_depth` | Maximum nesting depth of `filters.query`. | | `max_clause_count` | Maximum total clauses across a `filters` query. | | `max_terms_per_clause` | Maximum values in a single `terms` or `$in` clause. | | `max_sort_clauses` | Maximum sort fields. | | `max_string_length` | Maximum length of any string value in a query. | ### Field entry Each item in `fields` describes one queryable field. | Property | Type | Description | | - | - | - | | `field` | string | The field path to use in `simple_filters` and `filters`. | | `field_type` | string | The field's data type: `keyword`, `text`, `long`, `double`, `date`, or `boolean`. | | `value_type` | string | The type to send. Matches `field_type`, except for text fields with an exact-match form, which expose `keyword`. | | `nested_path` | string \| null | The field's nested path, or `null`. It is the key of `$elemMatch` and the `path` of a `nested` clause. | | `queryable` | boolean | Whether the field is valid in `simple_filters` and `filters`. | | `sortable` | boolean | Whether the field can be used in `$sort` or DSL `sort`. | | `rangeable` | boolean | Whether the field accepts `$gt`, `$gte`, `$lt`, `$lte`, or DSL `range`. | | `sort_field` | string \| null | The path to sort on when it differs from `field`, such as `company_name.keyword`. | | `aggregation_field` | string \| null | Path used for top-value aggregation in `POST /search/filter-field-values`. | | `supports_top_values` | boolean | Whether the field can be passed to `POST /search/filter-field-values`. | | `supports_exact_filter_snippet` | boolean | Whether a `filter_snippet` is returned alongside top values for this field. | | `preferred_for_exact_match` | boolean | Whether the field suits exact-match queries. `false` on text fields. | To list fields by property, filter the capabilities response, for example with `jq '.fields[] | select(.supports_top_values) | .field'`. ### Example response Trimmed to two representative fields — a top-level keyword and a nested numeric. ```json theme={null} { "mode": "company", "fields": [ { "field": "website_domain", "field_type": "keyword", "value_type": "keyword", "nested_path": null, "queryable": true, "sortable": false, "rangeable": false, "sort_field": null, "aggregation_field": "website_domain", "supports_top_values": true, "supports_exact_filter_snippet": true, "preferred_for_exact_match": true }, { "field": "last_funding_round.amount_raised", "field_type": "long", "value_type": "long", "nested_path": "last_funding_round", "queryable": true, "sortable": true, "rangeable": true, "sort_field": null, "aggregation_field": "last_funding_round.amount_raised", "supports_top_values": true, "supports_exact_filter_snippet": true, "preferred_for_exact_match": true } ], "nested_paths": [ "company_locations_full", "funding_rounds", "last_funding_round", "technologies_used" ], "simple_query_operators": [ "$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in", "$nin", "$exists", "$match", "$phrase", "$and", "$or", "$not", "$nor", "$elemMatch", "$search", "$sort", "$limit" ], "limits": { "max_query_depth": 12, "max_clause_count": 200, "max_terms_per_clause": 25, "max_sort_clauses": 2, "max_string_length": 200 }, "cache_ttl_seconds": 600, "mapping_hash": "ea1a21b4ccf1fef56bc102f80a7a59bb4b7ff5777d70d267236c357205e2eb99" } ``` ## Field values ```http theme={null} POST https://api.sixtyfour.ai/search/filter-field-values ``` Returns the most common values of one field, ranked by record count, optionally scoped by a query. | Parameter | Type | Default | Description | | - | - | - | - | | `field` | string | required | Field path. Must have `supports_top_values: true` in capabilities. | | `mode` | string | `company` | `people` or `company`. | | `top_k` | integer | `25` | Number of values to return, from 1 to 100. | | `simple_filters` or `filters` | object | — | Optional scope. Send at most one. | ### Scoped query A scope takes no `filters.sort`, `filters.size`, `simple_filters.$sort`, or `simple_filters.$limit`. > [!TIP] > > A high `top_k` lists the valid values of an enum-like field before filtering. ### Example request Global: ```bash theme={null} curl -X POST "https://api.sixtyfour.ai/search/filter-field-values" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"mode": "company", "field": "hq_country_iso2", "top_k": 4}' ``` Scoped: ```json theme={null} { "mode": "company", "field": "industry", "top_k": 4, "simple_filters": { "hq_country_iso2": { "$eq": "US" }, "employees_count": { "$gte": 200, "$lte": 5000 }, "ownership_status": { "$eq": "private" } } } ``` ### Response fields | Field | Description | | - | - | | `field`, `canonical_field`, `aggregation_field` | The requested field and the paths used to count it. | | `nested_path` | Set for fields inside nested arrays. | | `value_type` | Type of the returned values (`keyword`, `long`, etc.). | | `total_scoped_documents` | Records matched by the scope. | | `values[]` | Top values, each with `value`, `count`, `percent_of_scope` (0–1), and `filter_snippet`. | | `supports_top_values`, `supports_exact_filter_snippet`, `preferred_for_exact_match` | Same meaning as in [Field entry](#field-entry). | ### Nested fields For nested fields such as `technologies_used.technology`, counts are record counts, and each `filter_snippet` is wrapped in a `nested` clause, ready to use in `filters`. ## Search query ```http theme={null} POST https://api.sixtyfour.ai/search/query ``` Runs a paginated search and returns the first page. ### Request | Parameter | Type | Default | Description | | - | - | - | - | | `simple_filters` | object | — | Mongo-style filters. See [Simple filters](#simple-filters). | | `filters` | object | — | DSL filters. See [DSL filters](#dsl-filters). | | `query` | string | — | Natural language query, for people or companies. | | `parsed_query` | object | — | Structured query echoed by a previous response. | | `search_id` | string | — | Re-run a previous search, including deep and lookalike searches. See [Replay a saved search](/api-reference/search/search-results#replay-a-saved-search). | | `mode` | string | `company` | `people` or `company`. Inferred from `search_id` and `query` when omitted. | | `page_size` | integer | `10` | Rows per page, from 1 to 100. | | `max_results` | integer | `1000` | Total rows across all pages, up to your ceiling from [`GET /search/limits`](/api-reference/search/search-results#search-limits). | | `output_shape` | string | `raw` | `raw` returns each record as stored. `workflow` returns the flattened lead or company rows used by workflows and [CSV exports](/api-reference/search/search-results#csv-columns). | | `from_offset` | integer | — | Company mode only: start at this row offset (0–10,000) for page-numbered navigation. A non-zero offset returns no cursor and is not saved, so its `search_id` cannot be replayed. | | `exclude_entity_ids` | string\[] | — | IDs to leave out (max 1,000). | | `exclude_list_ids` | string\[] | — | Saved exclusion lists to apply. | | `exclude_public_ids` | string\[] | — | Deprecated. Use `exclude_entity_ids`. | | `location_expansion_enabled` | boolean | `true` | Widen the location in a `query` to related place names. | | `query_label` | string | — | Label shown for this search in the web app's search history. Does not affect results. | | `persist_history` | boolean | `true` | `false` does not save the search, so its `search_id` cannot be replayed or exported. | | `cursor` | string | — | `next_cursor` from the previous page. Send it alone. | ### Request flow 1. First request: send exactly one of `simple_filters`, `filters`, `query`, `parsed_query`, or `search_id`, plus any options above. 2. Next page: send only `{"cursor": "..."}`. Mode, page size, output shape, and exclusions carry over. See [Paginate with cursors](/api-reference/search/search-results#paginate-with-cursors) for every cursor rule. ### Effective result cap The smallest of `max_results`, `simple_filters.$limit`, and `filters.size` applies. The response reports it as `max_results`, with `remaining_results` left after each page. ### Response fields For every response field, see [Search Results → Query response](/api-reference/search/search-results#query-response). ### Example request Fresh search: ```bash theme={null} curl -X POST "https://api.sixtyfour.ai/search/query" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "mode": "company", "simple_filters": { "hq_country_iso2": { "$eq": "US" }, "employees_count": { "$gte": 100, "$lte": 5000 } } }' ``` Exclude specific people with `exclude_entity_ids`: ```json theme={null} { "mode": "people", "parsed_query": { "jobTitleExperience": { "titles": ["VP Engineering", "Head of Engineering"], "searchBoth": false } }, "exclude_entity_ids": ["john-doe-123", "https://linkedin.com/in/jane-smith-456"] } ``` For identifier formats, saved lists, and limits, see [Search Exclusions](/api-reference/search/search-exclusions). Re-run a previous search: ```json theme={null} { "search_id": "e3b0c442-98fc-1c14-9afb-f4c8996fb924" } ``` Next page: ```json theme={null} { "cursor": "eyJpZCI6I..." } ``` ### Photo and logo URLs People rows carry image URLs that need no credentials, so they can be embedded directly in an `` tag, CRM record, or spreadsheet cell. | Field | Contains | | - | - | | `logoUrl` | The person's profile photo URL. Use it exactly as returned, including its query string. Empty or `null` when the person has no photo. | | `currentCompanies[].company.logo` in `raw_source`, `currentCompanyLogo` in workflow rows and CSV exports | The employer's LinkedIn logo. Empty when no company logo is available. | When `logoUrl` holds a bare 32-character key instead of a URL, the photo is at `https://api.sixtyfour.ai/v1/media/profiles/{key}`. These URLs do not expire. See [Rate Limits](/api-reference/rate-limits#media) and [Handling Errors](/api-reference/errors). ## Export search results Export results from any search as a CSV. See [Export Search Results](/api-reference/search/search-results#export-search-results) for the full endpoint reference. ## Example usage The following examples walk through the full recommended flow: discover valid fields, inspect top values for one field, then paginate through results. ```bash cURL theme={null} API_KEY="YOUR_API_KEY" BASE_URL="https://api.sixtyfour.ai" # 1. Get filter capabilities to discover valid fields curl -s "$BASE_URL/search/filter-capabilities" \ -H "x-api-key: $API_KEY" \ | jq '[.fields[] | select(.queryable) | .field] | .[0:5]' # 2. Inspect top values for a field to build a simple filter TOP_COUNTRY=$(curl -s -X POST "$BASE_URL/search/filter-field-values" \ -H "x-api-key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{"mode": "company", "field": "hq_country_iso2", "top_k": 4}' \ | jq -r '.values[0].value') echo "Top country: $TOP_COUNTRY" # 3. Run the paginated filter search, following cursors until has_more is false CURSOR="" while true; do if [ -z "$CURSOR" ]; then PAYLOAD=$(jq -nc --arg c "$TOP_COUNTRY" \ '{mode:"company", simple_filters:{hq_country_iso2:{"$eq":$c}, employees_count:{"$gte":100}}}') else PAYLOAD=$(jq -nc --arg cur "$CURSOR" '{cursor:$cur}') fi RESP=$(curl -s -X POST "$BASE_URL/search/query" \ -H "x-api-key: $API_KEY" \ -H "Content-Type: application/json" \ -d "$PAYLOAD") echo "$RESP" | jq -r '"Page \(.page_number) — \(.page_count) rows"' HAS_MORE=$(echo "$RESP" | jq -r '.has_more') [ "$HAS_MORE" != "true" ] && break CURSOR=$(echo "$RESP" | jq -r '.next_cursor') done ``` ```python Python theme={null} import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.sixtyfour.ai" headers = {"x-api-key": API_KEY, "Content-Type": "application/json"} # 1. Get filter capabilities to discover valid fields caps = requests.get( f"{BASE_URL}/search/filter-capabilities", headers=headers, ).json() queryable_fields = [f["field"] for f in caps["fields"] if f["queryable"]][:5] print(f"Available queryable fields: {queryable_fields}") print(f"Supported simple operators: {caps['simple_query_operators']}") # 2. Inspect top values for a field to build a simple filter values_resp = requests.post( f"{BASE_URL}/search/filter-field-values", headers=headers, json={"mode": "company", "field": "hq_country_iso2", "top_k": 4}, ).json() top_country = values_resp["values"][0]["value"] print(f"Top country: {values_resp['values'][0]['value']}") # 3. Run the paginated filter search using simple_filters all_results = [] cursor = None while True: if cursor: payload = {"cursor": cursor} else: payload = { "mode": "company", "simple_filters": { "hq_country_iso2": {"$eq": top_country}, "employees_count": {"$gte": 100}, }, } resp = requests.post( f"{BASE_URL}/search/query", headers=headers, json=payload, ).json() all_results.extend(resp["results"]) print(f"Page {resp['page_number']} — {resp['page_count']} rows") if not resp["has_more"]: break cursor = resp["next_cursor"] print(f"Total rows fetched: {len(all_results)}") ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": API_KEY, "Content-Type": "application/json" }; // 1. Get filter capabilities to discover valid fields const capsResp = await fetch(`${BASE_URL}/search/filter-capabilities`, { headers }); const caps = await capsResp.json(); const queryableFields = caps.fields.filter(f => f.queryable).slice(0, 5).map(f => f.field); console.log("Available queryable fields:", queryableFields); console.log("Supported simple operators:", caps.simple_query_operators); // 2. Inspect top values for a field to build a simple filter const valuesResp = await fetch(`${BASE_URL}/search/filter-field-values`, { method: "POST", headers, body: JSON.stringify({ mode: "company", field: "hq_country_iso2", top_k: 4 }), }); const valuesData = await valuesResp.json(); const topCountry = valuesData.values[0].value; console.log("Top country:", valuesData.values[0].value); // 3. Run the paginated filter search using simple_filters const allResults = []; let cursor = null; while (true) { const payload = cursor ? { cursor } : { mode: "company", simple_filters: { hq_country_iso2: { $eq: topCountry }, employees_count: { $gte: 100 }, }, }; const resp = await fetch(`${BASE_URL}/search/query`, { method: "POST", headers, body: JSON.stringify(payload), }); const data = await resp.json(); allResults.push(...data.results); console.log(`Page ${data.page_number} — ${data.page_count} rows`); if (!data.has_more) break; cursor = data.next_cursor; } console.log(`Total rows fetched: ${allResults.length}`); ``` ## Rate limits For per-endpoint limits, see [Rate Limits](/api-reference/rate-limits#search). ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs. ## Errors For error responses (400, 403, 404, 409, 503, etc.), see [Handling Errors](/api-reference/errors). # Filter Search Capabilities Source: https://docs.sixtyfour.ai/api-reference/search/filter-search-capabilities https://api.sixtyfour.ai/openapi.json get /search/filter-capabilities Get the fields, operators, and limits supported by filter search. # Get Exclusion List Source: https://docs.sixtyfour.ai/api-reference/search/get-exclusion-list https://api.sixtyfour.ai/openapi.json get /search/exclusion-lists/{list_id} Fetch one exclusion list, including its build status and size. # List Exclusion Lists Source: https://docs.sixtyfour.ai/api-reference/search/list-exclusion-lists https://api.sixtyfour.ai/openapi.json get /search/exclusion-lists List the organization's exclusion lists (most recent first). # Lookalike Search Source: https://docs.sixtyfour.ai/api-reference/search/lookalike-search Find people similar to a seed cohort using LinkedIn profiles or a CSV, then page the results by similarity. Lookalike Search finds people similar to a seed cohort across Sixtyfour's people index, ranks them by similarity, and leaves the seed people out. It finds people only. ## Endpoint ```http theme={null} POST https://api.sixtyfour.ai/search/lookalike GET https://api.sixtyfour.ai/search/lookalike/{task_id} ``` ```mermaid theme={null} flowchart LR Seeds["LinkedIn URLs or CSV"] --> Start["POST /search/lookalike"] Start --> Poll["GET /search/lookalike/{task_id}"] Poll --> Results["POST /search/query"] Results --> Cursor["Continue with next_cursor"] ``` ## Prepare the seed cohort Every request accepts exactly one seed source. | Seed source | Request field | Requirements | | - | - | - | | LinkedIn profiles | `linkedin_urls` | 1–1,000 LinkedIn profile URLs. Each URL must be 500 characters or fewer. | | Uploaded CSV | `csv_resource_handle_id` | Handle returned by `POST /storage/csv/upload`. The CSV can contain up to 1,000 data rows. | For CSV seeds, a LinkedIn profile URL column produces the most reliable identity matches. Recognized headers include `linkedin`, `linkedin url`, `linkedin profile`, and `profile url`. Rows without a LinkedIn URL need a full name plus supporting identity data such as company, company domain, role, location, or education. Sixtyfour drops weak or ambiguous identity matches instead of guessing. `guidance` adds requirements the seeds do not imply, such as `"Only people in the United States with 8+ years of experience"`. The seeds define the audience. ## Run a lookalike search Submit the seed cohort, poll the dedicated status endpoint until the search completes, then pass its `search_id` to `/search/query`. The start request resolves the seeds before it returns, which takes up to 90 seconds. Set your client timeout to at least 100 seconds. ```bash cURL theme={null} API_KEY="YOUR_API_KEY" BASE_URL="https://api.sixtyfour.ai" START_RESPONSE=$(curl -fsS --max-time 120 -X POST "$BASE_URL/search/lookalike" \ -H "x-api-key: $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "linkedin_urls": [ "https://www.linkedin.com/in/ada-lovelace", "https://www.linkedin.com/in/grace-hopper" ], "guidance": "Only people in the United States" }') TASK_ID=$(printf '%s' "$START_RESPONSE" | jq -r '.task_id') DEADLINE=$(( $(date +%s) + 30 * 60 )) while true; do if [ "$(date +%s)" -ge "$DEADLINE" ]; then printf '%s\n' "Search did not complete within 30 minutes" exit 1 fi HTTP_CODE=$(curl -sS --connect-timeout 10 --max-time 30 \ -o status.json -D headers.txt -w '%{http_code}' \ "$BASE_URL/search/lookalike/$TASK_ID?include_filters=false" \ -H "x-api-key: $API_KEY") if [ "$HTTP_CODE" = "429" ] || [ "$HTTP_CODE" = "503" ]; then RETRY_AFTER=$(awk 'tolower($1) == "retry-after:" {print $2}' headers.txt | tr -d '\r') sleep "${RETRY_AFTER:-10}" continue fi if [ "$HTTP_CODE" != "200" ]; then cat status.json exit 1 fi STATUS=$(jq -r '.status' status.json) if [ "$STATUS" = "completed" ]; then SEARCH_ID=$(jq -r '.search_id' status.json) break fi if [ "$STATUS" != "running" ]; then cat status.json exit 1 fi sleep 10 done curl -fsS -X POST "$BASE_URL/search/query" \ -H "x-api-key: $API_KEY" \ -H "Content-Type: application/json" \ -d "{\"search_id\":\"$SEARCH_ID\",\"page_size\":25}" ``` ```python Python theme={null} import time import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.sixtyfour.ai" HEADERS = { "x-api-key": API_KEY, "Content-Type": "application/json", } start_response = requests.post( f"{BASE_URL}/search/lookalike", headers=HEADERS, json={ "linkedin_urls": [ "https://www.linkedin.com/in/ada-lovelace", "https://www.linkedin.com/in/grace-hopper", ], "guidance": "Only people in the United States", }, timeout=(10, 120), ) start_response.raise_for_status() task_id = start_response.json()["task_id"] deadline = time.monotonic() + 30 * 60 while True: if time.monotonic() >= deadline: raise TimeoutError("Search did not complete within 30 minutes") status_response = requests.get( f"{BASE_URL}/search/lookalike/{task_id}", headers=HEADERS, params={"include_filters": "false"}, timeout=(10, 30), ) if status_response.status_code in (429, 503): time.sleep(int(status_response.headers.get("Retry-After", "10"))) continue status_response.raise_for_status() status_data = status_response.json() if status_data["status"] == "completed": search_id = status_data["search_id"] break if status_data["status"] != "running": raise RuntimeError( status_data.get("error") or f"Search {status_data['status']}" ) time.sleep(10) page_response = requests.post( f"{BASE_URL}/search/query", headers=HEADERS, json={"search_id": search_id, "page_size": 25}, ) page_response.raise_for_status() page = page_response.json() for person in page["results"]: print( person["raw_source"].get("fullName"), person["raw_source"].get("headline"), person["raw_score"], ) ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": API_KEY, "Content-Type": "application/json", }; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function requestJson(url, options = {}, timeoutMs = 30_000) { for (let attempt = 0; attempt < 5; attempt++) { const response = await fetch(url, { ...options, signal: AbortSignal.timeout(timeoutMs), }); if (response.status === 429 || response.status === 503) { await sleep(Number(response.headers.get("Retry-After") ?? 10) * 1000); continue; } if (!response.ok) { throw new Error(`${response.status}: ${await response.text()}`); } return response.json(); } throw new Error(`${url} kept returning 429 or 503`); } async function main() { const started = await requestJson( `${BASE_URL}/search/lookalike`, { method: "POST", headers, body: JSON.stringify({ linkedin_urls: [ "https://www.linkedin.com/in/ada-lovelace", "https://www.linkedin.com/in/grace-hopper", ], guidance: "Only people in the United States", }), }, 120_000, ); let searchId; const deadline = Date.now() + 30 * 60_000; while (!searchId) { if (Date.now() >= deadline) { throw new Error("Search did not complete within 30 minutes"); } const params = new URLSearchParams({ include_filters: "false", }); const status = await requestJson( `${BASE_URL}/search/lookalike/${started.task_id}?${params}`, { headers }, ); if (status.status === "completed") { searchId = status.search_id; break; } if (status.status !== "running") { throw new Error(status.error ?? `Search ${status.status}`); } await sleep(10_000); } const page = await requestJson( `${BASE_URL}/search/query`, { method: "POST", headers, body: JSON.stringify({ search_id: searchId, page_size: 25 }), }, 120_000, ); for (const person of page.results) { console.log( person.raw_source?.fullName, person.raw_source?.headline, person.raw_score, ); } } main().catch((error) => { console.error(error); process.exitCode = 1; }); ``` > [!WARNING] > > Poll lookalike tasks with `GET /search/lookalike/{task_id}`, not `GET /search/status/{task_id}`. If your client gives up on the start request before it returns, do not submit it again automatically—the search may already be running. ## Start response The start endpoint returns HTTP 202 after resolving the seeds and launching the search. ```json theme={null} { "task_id": "lookalike_search_11111111-1111-4111-8111-111111111111", "search_id": "11111111-1111-4111-8111-111111111111", "status": "running", "seed_diagnostics": { "seed_type": "linkedin_urls", "supplied": 2, "parsed": 2, "resolved": 2, "skipped": 0, "analyzed": 2, "errors": [], "warnings": [], "identity_resolution": { "exact_matches": 2, "similarity_matches": 0, "dropped_below_threshold": 0, "ambiguous_matches": 0, "duplicates_removed": 0, "average_confidence": 1.0 } } } ``` `seed_diagnostics` reports how many seeds Sixtyfour resolved. The search runs when at least one seed resolves. ## Completed status Poll every 10 seconds or slower, and stop on any status other than `running`: `completed`, `failed`, `cancelled`, `terminated`, or `timed_out`. ```json theme={null} { "task_id": "lookalike_search_11111111-1111-4111-8111-111111111111", "status": "completed", "start_time": "2026-08-12T18:00:00+00:00", "close_time": "2026-08-12T18:02:14+00:00", "search_id": "11111111-1111-4111-8111-111111111111", "total_results": 12480, "seed_diagnostics": { "seed_type": "linkedin_urls", "supplied": 2, "parsed": 2, "resolved": 2, "skipped": 0, "analyzed": 2, "errors": [], "warnings": [], "identity_resolution": { "exact_matches": 2, "similarity_matches": 0, "dropped_below_threshold": 0, "ambiguous_matches": 0, "duplicates_removed": 0, "average_confidence": 1.0 } }, "excluded_public_ids": ["ada-lovelace", "grace-hopper"] } ``` The status response also carries `filters`, which has no fixed shape. Set `include_filters=false` to omit it. `total_results` counts everyone who matches `guidance`, without the seeds; `POST /search/query` returns up to `max_results` of them. `excluded_public_ids` lists the seed people, who are left out of results, replays, and exports. While the search is `running`, the response carries `progress_message`. Fields without a value are omitted. ## Page ranked results Use the completed `search_id` for the first page: ```json theme={null} { "search_id": "11111111-1111-4111-8111-111111111111", "page_size": 25 } ``` Rows are standard people-search rows, ordered by similarity to the cohort, with the score in `raw_score`. The seed people are excluded, so `total_available` is omitted: ```json theme={null} { "search_id": "11111111-1111-4111-8111-111111111111", "next_cursor": "eyJ2IjoxLCJraWQiOiJr...", "cursor_expires_in_seconds": 1800, "has_more": true, "page_size": 25, "page_count": 25, "total_results": 25, "results": [ { "raw_document_id": "jane-doe", "raw_source": { "publicId": "jane-doe", "fullName": "Jane Doe", "headline": "Staff Data Engineer" }, "raw_score": 0.78 } ] } ``` Paging follows the shared [cursor rules](/api-reference/search/search-results#paginate-with-cursors), and the full response is described in [Query response](/api-reference/search/search-results#query-response). For every later page, send only the cursor returned by the previous response: ```json theme={null} { "cursor": "eyJ2IjoxLCJraWQiOiJr..." } ``` ## Use a CSV seed cohort A CSV seed file: ```csv theme={null} linkedin_url,name,current_company,current_role https://www.linkedin.com/in/ada-lovelace,Ada Lovelace,Analytical Engines,Mathematician https://www.linkedin.com/in/grace-hopper,Grace Hopper,United States Navy,Computer Scientist ``` Upload it, then pass the returned `handle_id` as `csv_resource_handle_id`. ```bash cURL theme={null} UPLOAD=$(curl -fsS -X POST "https://api.sixtyfour.ai/storage/csv/upload" \ -H "x-api-key: YOUR_API_KEY" \ -F "file=@lookalike-seeds.csv;type=text/csv") HANDLE_ID=$(printf '%s' "$UPLOAD" | jq -r '.handle_id') curl -fsS -X POST "https://api.sixtyfour.ai/search/lookalike" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"csv_resource_handle_id\":\"$HANDLE_ID\"}" ``` ```python Python theme={null} import requests headers = {"x-api-key": "YOUR_API_KEY"} with open("lookalike-seeds.csv", "rb") as csv_file: upload_response = requests.post( "https://api.sixtyfour.ai/storage/csv/upload", headers=headers, files={"file": ("lookalike-seeds.csv", csv_file, "text/csv")}, ) upload_response.raise_for_status() handle_id = upload_response.json()["handle_id"] start_response = requests.post( "https://api.sixtyfour.ai/search/lookalike", headers={**headers, "Content-Type": "application/json"}, json={"csv_resource_handle_id": handle_id}, ) start_response.raise_for_status() print(start_response.json()) ``` ```javascript JavaScript theme={null} import { readFile } from "node:fs/promises"; const headers = { "x-api-key": "YOUR_API_KEY" }; const csv = await readFile("lookalike-seeds.csv"); const form = new FormData(); form.append( "file", new Blob([csv], { type: "text/csv" }), "lookalike-seeds.csv", ); const uploadResponse = await fetch( "https://api.sixtyfour.ai/storage/csv/upload", { method: "POST", headers, body: form }, ); if (!uploadResponse.ok) throw new Error(await uploadResponse.text()); const { handle_id } = await uploadResponse.json(); const startResponse = await fetch( "https://api.sixtyfour.ai/search/lookalike", { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ csv_resource_handle_id: handle_id }), }, ); if (!startResponse.ok) throw new Error(await startResponse.text()); console.log(await startResponse.json()); ``` After the start call, use the same status and result-pagination flow shown above. ## Export results Pass the completed lookalike `search_id` to `POST /search/export`, poll the returned export task through `GET /search/status/{task_id}`, then download the generated CSV. See [Export Search Results](/api-reference/search/search-results#export-search-results). ## Rate limits For per-endpoint limits, see [Rate Limits](/api-reference/rate-limits#search). ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for more information. ## Errors For error responses (400, 402, 403, 404, 422, 429, 504, etc.), see [Handling Errors](/api-reference/errors). # Lookalike Search Status Source: https://docs.sixtyfour.ai/api-reference/search/lookalike-search-status https://api.sixtyfour.ai/openapi.json get /search/lookalike/{task_id} Poll a lookalike search, and read its generated filters once it completes. # Deep Search Source: https://docs.sixtyfour.ai/api-reference/search/search-endpoints Reference for starting a deep search from a natural language query. Turn a natural language query into a list of people or companies. Start the search here, then poll, download, and page through it with the [Search Results](/api-reference/search/search-results) endpoints. For the end-to-end flow, see the [Deep Search Overview](/api-reference/search/search-overview). > [card] [API Reference](/api-reference/search/start-deep-search-async) > > See the full request/response schema and parameters in the API Reference. ## Start deep search Submit a natural language query. Returns a `task_id` to poll with [`GET /search/status/{task_id}`](/api-reference/search/search-results#get-search-status). ```http theme={null} POST https://api.sixtyfour.ai/search/start-deep-search ``` | Parameter | Type | Default | Description | | - | - | - | - | | `query` | string | required | Natural language description of the people or companies to find, up to 10,000 characters. | | `mode` | string | `people` | `people` or `company`. | | `max_results` | integer | `1000` | Total result cap, from 1 to your [ceiling](/api-reference/search/search-results#search-limits). | | `output_mode` | string | `csv` | `csv` writes a downloadable CSV. `query_only` skips the CSV; page through results with [`POST /search/query`](/api-reference/search/search-results#query-search-results). | | `search_id` | string | generated | Optional UUID for the new search. It is not an idempotency key; see [Retry a start request](#retry-a-start-request). | | `exclude_entity_ids` | string\[] | — | People or companies to leave out (max 1,000). See [Search Exclusions](/api-reference/search/search-exclusions). | | `exclude_list_ids` | string\[] | — | Saved exclusion lists to apply. See [Search Exclusions](/api-reference/search/search-exclusions). | ```json theme={null} { "query": "VP of Engineering at Series B SaaS startups in New York", "mode": "people", "max_results": 500 } ``` Response: ```json theme={null} { "task_id": "agentic_search_e3b0c442-98fc-1c14-9afb-f4c8996fb924", "status": "running", "message": "Search started successfully" } ``` The task ID is `agentic_search_` followed by the search's `search_id`. ### Concurrency and run time Each organization runs up to 5 deep searches at a time; see [Concurrency limits](/api-reference/rate-limits#deep-search). A deep search runs for at most 15 minutes, then ends `timed_out`. ### Retry a start request A start request that sends its own `search_id` (a UUID) can be checked after a timeout at `GET /search/status/agentic_search_{search_id}`: * A status response means the search started. * A `404` two minutes after the start means it did not start. A new start needs a new `search_id`. Each `search_id` must be a new UUID; see [Search errors](/api-reference/errors#search). ## Get the results | Step | Endpoint | | - | - | | Poll the task until it stops | [`GET /search/status/{task_id}`](/api-reference/search/search-results#get-search-status) | | Download the CSV (`output_mode: "csv"`) | [`GET /search/download`](/api-reference/search/search-results#download-search-results) | | Page through rows as JSON | [`POST /search/query`](/api-reference/search/search-results#query-search-results) | | Build a CSV from a `query_only` search | [`POST /search/export`](/api-reference/search/search-results#export-search-results) | *** ## Example usage ```bash cURL theme={null} # 1. Start a deep search curl -X POST "https://api.sixtyfour.ai/search/start-deep-search" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "VP of Engineering at Series B SaaS startups in New York", "mode": "people", "max_results": 500 }' # 2. Poll for status (use task_id from step 1) curl -X GET "https://api.sixtyfour.ai/search/status/TASK_ID" \ -H "x-api-key: YOUR_API_KEY" # 3. Download results (use resource_handle_id from step 2) curl -X GET "https://api.sixtyfour.ai/search/download?resource_handle_id=RESOURCE_HANDLE_ID" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import requests import time API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.sixtyfour.ai" headers = { "x-api-key": API_KEY, "Content-Type": "application/json" } start_response = requests.post( f"{BASE_URL}/search/start-deep-search", headers=headers, json={ "query": "VP of Engineering at Series B SaaS startups in New York", "mode": "people", "max_results": 500 } ) start_response.raise_for_status() task_id = start_response.json()["task_id"] print(f"Search started: {task_id}") deadline = time.monotonic() + 20 * 60 while True: if time.monotonic() > deadline: raise TimeoutError(f"Search {task_id} did not finish within 20 minutes") status_response = requests.get( f"{BASE_URL}/search/status/{task_id}", headers=headers, timeout=30, ) if status_response.status_code in (429, 503): time.sleep(int(status_response.headers.get("Retry-After", "10"))) continue status_response.raise_for_status() status_data = status_response.json() print(f"Status: {status_data['status']} — {status_data.get('progress_message') or ''}") if status_data["status"] == "completed": resource_handle_id = status_data["resource_handle_id"] print(f"Total results: {status_data['total_results']}") break if status_data["status"] != "running": raise RuntimeError(f"Search {status_data['status']}: {status_data.get('error')}") time.sleep(10) download_response = requests.get( f"{BASE_URL}/search/download", headers=headers, params={"resource_handle_id": resource_handle_id} ) download_response.raise_for_status() download_url = download_response.json()["url"] print(f"Download URL: {download_url}") csv_response = requests.get( download_url, headers=headers, allow_redirects=False ) if csv_response.status_code != 200: raise RuntimeError(f"Download failed: {csv_response.status_code}") with open("search_results.csv", "wb") as f: f.write(csv_response.content) print("Results saved to search_results.csv") ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": API_KEY, "Content-Type": "application/json" }; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const startResponse = await fetch(`${BASE_URL}/search/start-deep-search`, { method: "POST", headers, body: JSON.stringify({ query: "VP of Engineering at Series B SaaS startups in New York", mode: "people", max_results: 500 }) }); if (!startResponse.ok) throw new Error(`Failed to start: ${await startResponse.text()}`); const { task_id } = await startResponse.json(); console.log(`Search started: ${task_id}`); let resourceHandleId; const deadline = Date.now() + 20 * 60_000; while (true) { if (Date.now() > deadline) throw new Error(`Search ${task_id} did not finish within 20 minutes`); const statusResponse = await fetch( `${BASE_URL}/search/status/${task_id}`, { headers } ); if (statusResponse.status === 429 || statusResponse.status === 503) { await sleep(Number(statusResponse.headers.get("Retry-After") ?? 10) * 1000); continue; } if (!statusResponse.ok) throw new Error(`Status check failed: ${statusResponse.status}`); const statusData = await statusResponse.json(); console.log(`Status: ${statusData.status} — ${statusData.progress_message ?? ""}`); if (statusData.status === "completed") { resourceHandleId = statusData.resource_handle_id; console.log(`Total results: ${statusData.total_results}`); break; } if (statusData.status !== "running") { throw new Error(`Search ${statusData.status}: ${statusData.error}`); } await sleep(10_000); } const downloadResponse = await fetch( `${BASE_URL}/search/download?resource_handle_id=${resourceHandleId}`, { headers } ); if (!downloadResponse.ok) throw new Error(`Download request failed: ${downloadResponse.status}`); const { url } = await downloadResponse.json(); console.log(`Download URL: ${url}`); const csvResponse = await fetch(url, { headers, redirect: "error" }); if (!csvResponse.ok) throw new Error(`Download failed: ${csvResponse.status}`); const buffer = Buffer.from(await csvResponse.arrayBuffer()); const fs = await import("fs"); fs.writeFileSync("search_results.csv", buffer); console.log("Results saved to search_results.csv"); ``` ## Rate limits For per-endpoint limits, see [Rate Limits](/api-reference/rate-limits#search). ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs. ## Errors For error responses (400, 403, 404, 409, 429, etc.), see [Handling Errors](/api-reference/errors). *** # Search Exclusions Source: https://docs.sixtyfour.ai/api-reference/search/search-exclusions Suppress known people or companies from search results with inline IDs or reusable saved exclusion lists. Exclusions keep known people or companies, such as existing customers or do-not-contact lists, out of search results. The API searches first, then removes excluded records before returning results. Matching behavior is unchanged. ## Choosing an approach | Need | Use | | - | - | | Exclude a few people or companies in one request | `exclude_entity_ids` | | Reuse a large suppression list | `exclude_list_ids` | | Build a saved list from IDs in the request | `POST /search/exclusion-lists` with `source.type = "entity_ids"` | | Build a saved list from a CSV upload | `POST /search/exclusion-lists` with `source.type = "resource_handle"` | | Build a saved list from a completed search | `POST /search/exclusion-lists` with `source.type = "search_id"` | ## Supported endpoints | Endpoint | Notes | | - | - | | `POST /search/query` | Send exclusions on the first request. | | `POST /search/start-deep-search` | See [Deep search](#deep-search). | | `POST /search/export` | Applies exclusions to the CSV. A `search_id` does not carry exclusions, so send them again. See [Export search results](/api-reference/search/search-results#export-search-results). | | `POST /search/exclusion-lists` | Creates reusable saved exclusion lists. See [Saved exclusion lists](#saved-exclusion-lists). | ## Inline exclusions Inline exclusions apply only to the request that sends them. ### People Use `exclude_entity_ids`. In people mode it accepts LinkedIn public IDs or LinkedIn profile URLs: ```json theme={null} { "mode": "people", "query": "VP Engineering in New York", "exclude_entity_ids": ["jane-doe", "https://linkedin.com/in/john-smith"] } ``` ### Companies Company searches use `exclude_entity_ids`: ```json theme={null} { "mode": "company", "simple_filters": { "hq_country_iso2": { "$eq": "US" } }, "exclude_entity_ids": [ "1441", "linkedin.com/company/acme-robotics", "acme.com" ] } ``` Company identifiers can be: * Numeric LinkedIn company IDs * LinkedIn company URLs * LinkedIn company slugs * Exact website domains Identifiers that match no company (unresolvable) or more than one (ambiguous) are skipped. ## Saved exclusion lists Saved lists are reusable, organization-scoped suppression lists. Each list has an `entity_type` of `person` or `company`, and `entity_type` defaults to `person`. A person list can only be used in people search, and a company list only in company search. Creating a list is asynchronous. A successful request returns `202 Accepted` with the list `id` and `status: "pending"`: ```json theme={null} { "id": "11111111-1111-1111-1111-111111111111", "status": "pending" } ``` Poll `GET /search/exclusion-lists/{list_id}` every 10 seconds or slower, and use the list only after `status` is `ready`. See [List status](#list-status). ### Create from inline IDs Use `source.type = "entity_ids"` for both people and companies. People: ```json theme={null} { "name": "Do not contact people", "entity_type": "person", "source": { "type": "entity_ids", "entity_ids": ["jane-doe", "https://linkedin.com/in/john-smith"] } } ``` Companies: ```json theme={null} { "name": "Customer accounts", "entity_type": "company", "source": { "type": "entity_ids", "entity_ids": ["1441", "linkedin.com/company/acme-robotics", "acme.com"] } } ``` ### Create from a CSV upload Upload the file with `POST /storage/csv/upload` (CSV, JSON, or NDJSON), then pass the `handle_id` it returns as `resource_handle_id`: ```json theme={null} { "name": "CSV suppression list", "entity_type": "company", "source": { "type": "resource_handle", "resource_handle_id": "8f14e45f-1111-2222-3333-abcdefabcdef", "id_column": "company_url" } } ``` `id_column` is optional if the API can detect the right column. Column names match without regard to case, spaces, or punctuation, so `public_id`, `publicId`, and `Public ID` are the same column. Without `id_column`, the API uses the column whose name comes first in this list: | `entity_type` | Recognized columns, in priority order | | - | - | | `person` | `public_id`, `public_ids`, `entity_id`, `linkedin_url`, `linkedin`, `url`, `profile_url` | | `company` | `company_id`, `company_ids`, `entity_id`, `linkedin_url`, `linkedin`, `url`, `website_domain`, `domain`, `website` | A file with a single column whose header is not recognized is read as a headerless list: its first line is treated as an ID. A file with several columns and none recognized needs `id_column`. Values are deduplicated, and a CSV must be UTF-8. ### Create from a completed search Use `source.type = "search_id"` to turn a completed search into a reusable list. The source search mode must match the list `entity_type`: a people search produces a `person` list, a company search a `company` list. `max_results` sets how many top results the list captures, from 1 to 250,000 (the default). `name` is optional for this source; it defaults to the search's query and the date. ```json theme={null} { "name": "Companies from last search", "entity_type": "company", "source": { "type": "search_id", "search_id": "22222222-2222-2222-2222-222222222222", "max_results": 5000 } } ``` The `source` can carry its own exclusions, so the list is built from the search results minus another list: ```json theme={null} { "name": "Search results except current customers", "entity_type": "company", "source": { "type": "search_id", "search_id": "22222222-2222-2222-2222-222222222222", "max_results": 5000, "exclude_list_ids": ["11111111-1111-1111-1111-111111111111"] } } ``` > [!WARNING] > > `search_id` stores only the query — not any exclusions you applied when browsing the results. If the list should match what you previewed, send the same exclusions in the `source`. ## Using a saved list in search Pass the list ID in `exclude_list_ids`: ```json theme={null} { "mode": "company", "simple_filters": { "hq_country_iso2": { "$eq": "US" } }, "exclude_list_ids": ["11111111-1111-1111-1111-111111111111"] } ``` Saved lists and inline exclusions combine in the same request: ```json theme={null} { "mode": "company", "simple_filters": { "hq_country_iso2": { "$eq": "US" } }, "exclude_list_ids": ["11111111-1111-1111-1111-111111111111"], "exclude_entity_ids": ["acme.com"] } ``` ## Manage saved lists `GET /search/exclusion-lists` returns every list in the organization, most recent first. `GET /search/exclusion-lists/{list_id}` returns one: ```json theme={null} { "id": "11111111-1111-1111-1111-111111111111", "name": "Customer accounts", "entity_type": "company", "status": "ready", "size": 2, "source_type": "entity_ids", "source_total_available": null, "id_column": null, "unresolved_count": 1, "error_message": null, "created_at": "2026-08-12T18:00:00+00:00", "updated_at": "2026-08-12T18:00:41+00:00" } ``` | Field | Description | | - | - | | `status` | See [List status](#list-status). | | `size` | Unique members, once the list is `ready`. | | `source_type` | `entity_ids`, `resource_handle`, or `search_id`. | | `source_total_available` | For a `search_id` source, the search's full match count. Larger than `size` when `max_results` capped the list. | | `id_column` | The column read from an uploaded file. | | `unresolved_count` | Company lists only: input values dropped because they matched no company or more than one. | | `error_message` | Why the list failed. | ### List status | Status | Meaning | | - | - | | `pending`, `materializing` | The list is being built. Keep polling. | | `ready` | The list can be used in `exclude_list_ids`. | | `failed` | The list could not be built. Read `error_message`. A list still building 60 minutes after its last update reads as `failed`. | Every list that is not deleted counts toward the active-list limit, including `pending` and `failed` lists. Delete failed lists to free their slots. ### Delete a list `DELETE /search/exclusion-lists/{list_id}` deletes a list and returns: ```json theme={null} { "success": true, "id": "11111111-1111-1111-1111-111111111111" } ``` Searches that use the list stop working: an open cursor that applies it fails, and so does any new request that names it. Start paging again without the list. ## Limits | Limit | Value | | - | - | | Saved lists per request (`exclude_list_ids`) | 5 (10 with extended exclusion limits) | | Active saved lists per organization | 20 (40 with extended exclusion limits) | | Inline IDs per request | 1,000 | | IDs per `entity_ids` source | 10,000 | | Members per saved list | 250,000 | | List name | 1–200 characters. Required unless `source.type` is `search_id` | Extended exclusion limits are enabled per organization; contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai) to request them. Lists are immutable: to change one, create a new list and delete the old one. ## Pagination Send exclusions on the first `/search/query` request. With exclusions, a page can hold fewer rows than `page_size`, even zero, while `has_more` is `true`. See [Paginate with cursors](/api-reference/search/search-results#paginate-with-cursors). ## Deep search ```json theme={null} { "query": "VP Engineering at Series B SaaS companies in New York", "mode": "people", "max_results": 500, "exclude_list_ids": ["11111111-1111-1111-1111-111111111111"], "exclude_entity_ids": ["jane-doe"] } ``` For `output_mode: "csv"`, Sixtyfour applies exclusions before producing the CSV. Rows removed by `exclude_list_ids` are not replaced, so the CSV can hold far fewer than `max_results` rows. To get up to `max_results` eligible rows, start the search with `output_mode: "query_only"` and page with exclusions through `POST /search/query`, or export it with `POST /search/export`. For `output_mode: "query_only"`, the `search_id` stores the query, not your exclusions. Send them again with the `search_id` on `POST /search/query`: ```json theme={null} { "search_id": "22222222-2222-2222-2222-222222222222", "exclude_list_ids": ["11111111-1111-1111-1111-111111111111"], "exclude_entity_ids": ["jane-doe"] } ``` ## Errors For error responses (400, 403, 422, etc.), see [Handling Errors](/api-reference/errors). # Search Export Source: https://docs.sixtyfour.ai/api-reference/search/search-export https://api.sixtyfour.ai/openapi.json post /search/export Export search results as a CSV file. Poll `GET /search/status/{task_id}` until it completes, then pass its `resource_handle_id` to `GET /search/download` for a download URL. # Search Limits Source: https://docs.sixtyfour.ai/api-reference/search/search-limits https://api.sixtyfour.ai/openapi.json get /search/limits The most rows one search may return or save for the credential you call with. `POST /search/query`, `POST /search/export` and deep search stop at `max_results`. # Deep Search Overview Source: https://docs.sixtyfour.ai/api-reference/search/search-overview Turn a natural language query into a list of people or companies, delivered as a CSV or as paginated JSON. Deep Search turns a natural language query such as "VP of Engineering at Series B SaaS startups in New York" into a list of matching people or companies. The search runs asynchronously: submit the query, poll for progress, then download a CSV or page through the results as JSON. Use it for prospecting and list building when you can describe the audience in words. To query structured fields directly, use [Filter Search](/api-reference/search/filter-search). To find people similar to an existing list, use [Lookalike Search](/api-reference/search/lookalike-search). ## Core concepts | Term | Description | | - | - | | `task_id` | Returned when a search or export starts. Poll `GET /search/status/{task_id}` with it. | | `search_id` | Identifier for the saved search. Pass it to `POST /search/query` to page through results or to `POST /search/export` to generate a CSV. | | `resource_handle_id` | Identifier for the generated CSV, returned when a CSV search or export completes. | | `mode` | `people` (default) or `company`. | | `max_results` | Total result cap. Defaults to 1,000. `GET /search/limits` returns your ceiling. | | `output_mode` | `csv` (default) writes a downloadable CSV. `query_only` skips the CSV so you can page through results with `POST /search/query`. | ## Execution flows ### CSV download ```mermaid theme={null} flowchart LR A["POST /search/start-deep-search"] --> B["GET /search/status/{task_id}"] B --> C["GET /search/download"] ``` 1. Start a search with [`POST /search/start-deep-search`](/api-reference/search/search-endpoints#start-deep-search) and save the `task_id`. 2. Poll [`GET /search/status/{task_id}`](/api-reference/search/search-results#get-search-status) until `status` is `completed`. The response includes `resource_handle_id`. Any status other than `running` or `completed` means the search stopped; see [Task statuses](/api-reference/search/search-results#task-statuses). 3. Request a signed URL with [`GET /search/download`](/api-reference/search/search-results#download-search-results) and fetch the CSV. The URL expires after 15 minutes. ### Paginated JSON ```mermaid theme={null} flowchart LR A["POST /search/start-deep-search
output_mode: query_only"] --> B["GET /search/status/{task_id}"] B --> C["POST /search/query"] C --> D["POST /search/export (optional)"] ``` 1. Start a search with `output_mode: "query_only"` and save the `task_id`. 2. Poll `GET /search/status/{task_id}` until `status` is `completed`, then read `search_id`. 3. Page through results with [`POST /search/query`](/api-reference/search/search-results#query-search-results), sending each `next_cursor` until `has_more` is `false`. Send `max_results` again on the first request; see [Replay a saved search](/api-reference/search/search-results#replay-a-saved-search). 4. Optionally generate a CSV from the same `search_id` with [`POST /search/export`](/api-reference/search/search-results#export-search-results). ## Quick start ```bash cURL theme={null} # 1. Start a deep search curl -X POST "https://api.sixtyfour.ai/search/start-deep-search" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "query": "VP of Engineering at Series B SaaS startups in New York", "mode": "people", "max_results": 500 }' # 2. Poll for status (use task_id from step 1) curl "https://api.sixtyfour.ai/search/status/TASK_ID" \ -H "x-api-key: YOUR_API_KEY" # 3. Get a signed download URL (use resource_handle_id from step 2) curl "https://api.sixtyfour.ai/search/download?resource_handle_id=RESOURCE_HANDLE_ID" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import time import requests BASE_URL = "https://api.sixtyfour.ai" headers = {"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"} start = requests.post( f"{BASE_URL}/search/start-deep-search", headers=headers, json={ "query": "VP of Engineering at Series B SaaS startups in New York", "mode": "people", "max_results": 500, }, ) start.raise_for_status() task_id = start.json()["task_id"] deadline = time.monotonic() + 20 * 60 while time.monotonic() < deadline: status = requests.get(f"{BASE_URL}/search/status/{task_id}", headers=headers, timeout=30) if status.status_code in (429, 503): time.sleep(int(status.headers.get("Retry-After", "10"))) continue status.raise_for_status() data = status.json() print(data["status"], data.get("progress_message") or "") if data["status"] == "completed": resource_handle_id = data["resource_handle_id"] break if data["status"] != "running": raise RuntimeError(data.get("error") or f"Search {data['status']}") time.sleep(10) else: raise TimeoutError(f"Search {task_id} did not finish within 20 minutes") download = requests.get( f"{BASE_URL}/search/download", headers=headers, params={"resource_handle_id": resource_handle_id}, ) download.raise_for_status() print(download.json()["url"]) ``` ```javascript JavaScript theme={null} const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const startResponse = await fetch(`${BASE_URL}/search/start-deep-search`, { method: "POST", headers, body: JSON.stringify({ query: "VP of Engineering at Series B SaaS startups in New York", mode: "people", max_results: 500, }), }); if (!startResponse.ok) throw new Error(await startResponse.text()); const { task_id } = await startResponse.json(); let resourceHandleId; const deadline = Date.now() + 20 * 60_000; while (resourceHandleId === undefined) { if (Date.now() > deadline) throw new Error(`Search ${task_id} did not finish within 20 minutes`); const response = await fetch(`${BASE_URL}/search/status/${task_id}`, { headers }); if (response.status === 429 || response.status === 503) { await sleep(Number(response.headers.get("Retry-After") ?? 10) * 1000); continue; } if (!response.ok) throw new Error(await response.text()); const data = await response.json(); console.log(data.status, data.progress_message ?? ""); if (data.status === "completed") resourceHandleId = data.resource_handle_id; else if (data.status !== "running") throw new Error(data.error ?? `Search ${data.status}`); else await sleep(10_000); } const { url } = await fetch( `${BASE_URL}/search/download?resource_handle_id=${resourceHandleId}`, { headers }, ).then((r) => r.json()); console.log(url); ``` # Search Query Source: https://docs.sixtyfour.ai/api-reference/search/search-query https://api.sixtyfour.ai/openapi.json post /search/query Run a search query. Provide exactly one query source: query, simple_filters, filters, parsed_query, or search_id. For pagination, pass cursor alone. # Search Results Source: https://docs.sixtyfour.ai/api-reference/search/search-results Reference for the endpoints every search shares: task status, paginated results, CSV export and download, and result limits. Every deep, filter, and lookalike search produces a `search_id`. These endpoints read its results: poll a task with `GET /search/status/{task_id}`, page through rows with `POST /search/query`, build a CSV with `POST /search/export`, download it with `GET /search/download`, and read your result ceiling with `GET /search/limits`. > [card] [API Reference](/api-reference/search/search-query) > > See the full request/response schema and parameters in the API Reference. ## Get search status Poll the progress and result of a deep search or an export. ```http theme={null} GET https://api.sixtyfour.ai/search/status/{task_id} ``` `task_id` is the value returned by [Start Deep Search](/api-reference/search/search-endpoints#start-deep-search) (`agentic_search_{search_id}`) or by [Export](#export-search-results) (`search_export_{search_id}`). Lookalike tasks use [`GET /search/lookalike/{task_id}`](/api-reference/search/lookalike-search#completed-status); this endpoint refuses them. > [!WARNING] > > `GET /search/deep-search-status/{task_id}` is deprecated and returns a `Deprecation` header. Use `GET /search/status/{task_id}`, which returns the same response. ### Task statuses | Status | Description | | - | - | | `running` | The task is in progress. `progress_message` describes the current step. | | `completed` | The task finished. `search_id` and `total_results` are set; `resource_handle_id` is set when a CSV was produced. | | `failed` | The task stopped. Read `error`. | | `cancelled`, `terminated`, `timed_out` | The task stopped before finishing: it was cancelled, stopped by Sixtyfour, or passed the 15-minute run limit. | `running` is the only in-progress status. Poll every 10 seconds or slower, and stop on any other status, including values not listed here. A search that finds nothing does not always end `completed`: * A `query_only` deep search with no matches ends `completed` with `total_results: 0`. * A CSV deep search or an export with no rows to write ends `failed`, with a message in `error`. This includes searches whose every row was excluded. A deep search started above your organization's concurrency limit also ends `failed`. See [Concurrency limits](/api-reference/rate-limits#deep-search). ### Status response | Field | Description | | - | - | | `task_id` | The task being polled. | | `status` | One of the [task statuses](#task-statuses). | | `search_id` | The search this task belongs to, in every status. Pass it to `POST /search/query` or `POST /search/export` once the task is `completed`. | | `start_time`, `close_time` | ISO 8601 timestamps. `close_time` is set once the task stops. | | `progress_message` | Human-readable progress while `running`. | | `iterations` | While a deep search is `running`, its latest progress: `iteration`, `results`, `elapsed_ms`, and `message`. | | `total_results` | Matches found. For an export, rows exported. | | `exported_count` | Rows written to the CSV. For a `query_only` deep search, rows the search collected, up to `max_results`. | | `resource_handle_id` | The CSV's handle. Pass it to `GET /search/download`. `null` for a `query_only` deep search. | | `error` | Failure reason. | Fields without a value are returned as `null`. ```json theme={null} { "task_id": "agentic_search_e3b0c442-98fc-1c14-9afb-f4c8996fb924", "status": "completed", "start_time": "2026-01-15T18:00:00+00:00", "progress_message": null, "close_time": "2026-01-15T18:04:12+00:00", "resource_handle_id": "8f14e45f-1111-2222-3333-abcdefabcdef", "total_results": 2140, "exported_count": 500, "error": null, "search_id": "e3b0c442-98fc-1c14-9afb-f4c8996fb924", "iterations": null } ``` For a complete polling loop that honors `Retry-After` and stops at a deadline, see [Deep Search → Example usage](/api-reference/search/search-endpoints#example-usage). *** ## Query search results Page through the results of a completed search as JSON. ```http theme={null} POST https://api.sixtyfour.ai/search/query ``` Send the `search_id` on the first request, then send only `{"cursor": "..."}` for each next page: ```json theme={null} { "search_id": "e3b0c442-98fc-1c14-9afb-f4c8996fb924", "page_size": 100, "max_results": 5000 } ``` ```json theme={null} { "cursor": "eyJpZCI6I..." } ``` | Parameter | Type | Default | Description | | - | - | - | - | | `search_id` | string | — | A completed deep, filter, or lookalike search. See [Replay a saved search](#replay-a-saved-search). | | `page_size` | integer | `10` | Rows per page, from 1 to 100. | | `max_results` | integer | `1000` | Total rows across all pages, up to your [ceiling](#search-limits). | | `output_shape` | string | `raw` | `raw` returns each record as stored. `workflow` returns the flattened rows used by workflows and [CSV exports](#csv-columns). | | `exclude_entity_ids`, `exclude_list_ids` | string\[] | — | People or companies to leave out. See [Search Exclusions](/api-reference/search/search-exclusions). | | `cursor` | string | — | `next_cursor` from the previous page. Send it alone. See [Paginate with cursors](#paginate-with-cursors). | `POST /search/query` also runs new searches from filters or a natural language query. For those inputs, see [Filter Search → Search query](/api-reference/search/filter-search#search-query). ### Replay a saved search A request with `search_id` runs the saved search's query again and starts a new set of pages under the same `search_id`. A replay carries: * The saved query and its mode. * For a lookalike search, its ranking and its seed people, who stay excluded from results. A replay does not carry: * `max_results`. It defaults to 1,000, whatever the original search used. Send it again. * `page_size`, `output_shape`, and exclusions. Send them on the first request. The saved search must be `completed`. Replaying a search that is still running, or one that failed, returns an error. A replay does not save a new search. ### Paginate with cursors Each page returns `has_more` and, while more rows remain, a `next_cursor`. Send `{"cursor": "..."}` alone to get the next page. Mode, page size, output shape, and exclusions carry over. * Stop when `has_more` is `false`. The last page omits `next_cursor` and `cursor_expires_in_seconds`; they are not returned as `null`. * A page can hold fewer rows than `page_size`, even zero, while `has_more` is `true`. Keep paging. * Each cursor works once. * Send each cursor only after the previous response arrives. * A cursor expires `cursor_expires_in_seconds` after it is issued (30 minutes), or when an exclusion list it applies is deleted. When a cursor fails, start again from the `search_id`. * A `429` or `503` does not use up a cursor: send it again after `Retry-After`, or after a few seconds if the header is absent. * A cursor request accepts only `cursor`. Sending a query, paging, or exclusion field with it fails the request. `mode`, `persist_history`, and `location_expansion_enabled` are ignored, as are fields sent as `null` or `false`. * A cursor works only for the organization that created it. ### Query response | Field | Description | | - | - | | `search_id` | Saved search ID. Reuse it with `POST /search/query` or `POST /search/export`. | | `results` | Rows for this page, in the selected `output_shape`. A `raw` row holds `raw_document_id`, `raw_source` (the record), and `raw_score` (its relevance score, or `null`). | | `has_more` | `true` while another page is available. | | `next_cursor`, `cursor_expires_in_seconds` | Token for the next page and its lifetime in seconds. Omitted on the last page. | | `page_size`, `page_count`, `page_number`, `total_pages`, `max_pages` | Pagination details. `page_count` is the number of rows on this page. `total_pages` is omitted while `has_more` is `true`. | | `total_results` | Rows returned so far. | | `total_available` | Rows the search can return: the exact match count for filters, or the ranked list, at most `max_results`, for a natural language query. Omitted when unknown or when exclusions apply. | | `total_available_lower_bound` | Minimum total matches when counting stopped at 50,000. | | `max_results`, `remaining_results` | The effective cap and the rows still available under it. | | `charge_credits` | Credits charged for this page. | | `parsed_query`, `natural_language_query` | The structured query, round-trippable into a new request, and the original `query` text. | | `output_shape` | Shape of `results`. | | `unmet_constraints` | Present when results do not cover the whole query. See [Constraints the search did not apply](#constraints-the-search-did-not-apply). | Fields without a value are omitted from the response, not returned as `null`. Inside `results`, rows keep their `null` values. ### Constraints the search did not apply `unmet_constraints` lists the parts of the query the search did not apply, usually as field names: Sixtyfour has no data for the field, or dropped the constraint because the full query matched too few rows. Results can include rows outside these constraints. [`GET /search/filter-capabilities`](/api-reference/search/filter-search#filter-capabilities) lists every field you can filter on. ### Example: page through a search ```bash cURL theme={null} API_KEY="YOUR_API_KEY" BASE_URL="https://api.sixtyfour.ai" BODY='{"search_id": "e3b0c442-98fc-1c14-9afb-f4c8996fb924", "page_size": 100, "max_results": 5000}' RETRIES=0 while true; do HTTP_CODE=$(curl -sS -o page.json -D headers.txt -w '%{http_code}' \ -X POST "$BASE_URL/search/query" \ -H "x-api-key: $API_KEY" \ -H "Content-Type: application/json" \ -d "$BODY") if [ "$HTTP_CODE" = "429" ] || [ "$HTTP_CODE" = "503" ]; then RETRIES=$((RETRIES + 1)) [ "$RETRIES" -le 5 ] || { echo "Search query kept returning $HTTP_CODE"; exit 1; } RETRY_AFTER=$(awk 'tolower($1) == "retry-after:" {print $2}' headers.txt | tr -d '\r') sleep "${RETRY_AFTER:-10}" continue fi if [ "$HTTP_CODE" != "200" ]; then cat page.json exit 1 fi RETRIES=0 jq -c '.results[]' page.json >> rows.jsonl [ "$(jq -r '.has_more' page.json)" = "true" ] || break BODY=$(jq -c '{cursor: .next_cursor}' page.json) done ``` ```python Python theme={null} import time import requests BASE_URL = "https://api.sixtyfour.ai" HEADERS = {"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"} def post_query(body): for _ in range(5): response = requests.post( f"{BASE_URL}/search/query", headers=HEADERS, json=body, timeout=120 ) if response.status_code in (429, 503): time.sleep(int(response.headers.get("Retry-After", "10"))) continue response.raise_for_status() return response.json() raise RuntimeError("Search query kept returning 429 or 503") rows = [] page = post_query({ "search_id": "e3b0c442-98fc-1c14-9afb-f4c8996fb924", "page_size": 100, "max_results": 5000, }) while True: rows.extend(page["results"]) if page.get("unmet_constraints"): print("Not applied:", page["unmet_constraints"]) if not page["has_more"]: break page = post_query({"cursor": page["next_cursor"]}) print(f"Fetched {len(rows)} rows") ``` ```javascript JavaScript theme={null} const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function postQuery(body) { for (let attempt = 0; attempt < 5; attempt++) { const response = await fetch(`${BASE_URL}/search/query`, { method: "POST", headers, body: JSON.stringify(body), signal: AbortSignal.timeout(120_000), }); if (response.status === 429 || response.status === 503) { await sleep(Number(response.headers.get("Retry-After") ?? 10) * 1000); continue; } if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`); return response.json(); } throw new Error("Search query kept returning 429 or 503"); } const rows = []; let page = await postQuery({ search_id: "e3b0c442-98fc-1c14-9afb-f4c8996fb924", page_size: 100, max_results: 5000, }); while (true) { rows.push(...page.results); if (page.unmet_constraints) console.warn("Not applied:", page.unmet_constraints); if (!page.has_more) break; page = await postQuery({ cursor: page.next_cursor }); } console.log(`Fetched ${rows.length} rows`); ``` *** ## Export search results Generate a CSV from any completed search: deep, filter, or lookalike. ```http theme={null} POST https://api.sixtyfour.ai/search/export ``` | Parameter | Type | Default | Description | | - | - | - | - | | `search_id` | string | — | The search to export. It must be `completed`. | | `parsed_query` | object | — | A structured query from a previous response, as an alternative to `search_id`. | | `mode` | string | `company` | `people` or `company`. Taken from the saved search when you send `search_id`. Send it with `parsed_query`: a people query sent without `mode` runs as a company search. | | `max_results` | integer | `1000` | Rows to export, up to your [ceiling](#search-limits). | | `exclude_entity_ids`, `exclude_list_ids` | string\[] | — | Exclusions to apply to the CSV. A `search_id` does not carry exclusions from earlier requests. | An export runs the saved query again, as a [replay](#replay-a-saved-search) does, so its rows can differ from the CSV a deep search wrote. ```json theme={null} { "task_id": "search_export_e3b0c442-98fc-1c14-9afb-f4c8996fb924", "status": "running", "message": "Export started. Poll /search/status/{task_id} for progress." } ``` 1. Call `POST /search/export` and save the `task_id`. 2. Poll [`GET /search/status/{task_id}`](#get-search-status) until it is `completed`, then read `resource_handle_id`. 3. Download the CSV with [`GET /search/download`](#download-search-results). One export per `search_id` runs at a time; while it runs, poll `search_export_{search_id}` instead of starting another. An export with `parsed_query` gets a new ID each time. ### CSV columns A CSV has one row per result, with the same columns as `output_shape: "workflow"` rows. | Mode | Columns | | - | - | | `people` | `linkedinUrl`, `fullName`, `headline`, `summary`, `logoUrl`, `currentCompany`, `currentPosition`, `currentCompanyLogo`, `currentCompanyStartDate`, `currentPositionStartDate`, `previousCompany`, `previousPosition`, `locationName`, `locationCountry`, `industry`, `totalExperienceYears`, `education`, `degree`, `fieldOfStudy`, `graduationYear`, `skills`, `certifications`, `languages`, `connectionsCount`, `followersCount` | | `company` | `linkedinUrl`, `name`, `industry`, `overview`, `tagline`, `website`, `domain`, `logoUrl`, `employeeCount`, `employeesOnLinkedIn`, `revenue`, `foundedAt`, `headquartersCity`, `headquartersCountry`, `headquartersRegion`, `locationCount`, `specialties`, `tags`, `fundingAmount`, `fundingType`, `fundingYear`, `fundingRoundsCount` | *** ## Download search results Get a signed URL for the CSV of a completed CSV deep search or export. ```http theme={null} GET https://api.sixtyfour.ai/search/download?resource_handle_id={resource_handle_id} ``` The response contains `url`. > [!NOTE] > > Fetch the returned `url` with your `x-api-key` header; the signed token does not replace authentication. Disable redirects when sending your API key. > [!WARNING] > > Signed URLs expire after 15 minutes. Download the file right away or request a new URL. ### Spreadsheet-safe CSV By default, the file is escaped so that opening it in a spreadsheet never runs a formula: * Every field is quoted. * A cell that starts with `=`, `+`, `-`, `@`, a tab, or a line break (or a full-width `=`, `+`, `-`, or `@`) gets a leading `'`. A phone number `+14155550123` arrives as `'+14155550123`. Negative numbers such as `-12.5` are left as they are. * The response carries the header `X-Spreadsheet-Safe: 1`. To read the file as data, append `?spreadsheet_safe=false` to the `url`. The file then arrives exactly as stored, without the header. A file that cannot be escaped (not valid CSV, or too large) downloads only with `?spreadsheet_safe=false`. ```bash cURL theme={null} # DOWNLOAD_URL is the url from GET /search/download curl -sS "DOWNLOAD_URL?spreadsheet_safe=false" \ -H "x-api-key: YOUR_API_KEY" \ -o search_results.csv ``` ```python Python theme={null} csv_response = requests.get( download_url, headers={"x-api-key": "YOUR_API_KEY"}, params={"spreadsheet_safe": "false"}, allow_redirects=False, ) ``` ```javascript JavaScript theme={null} const csvResponse = await fetch(`${downloadUrl}?spreadsheet_safe=false`, { headers: { "x-api-key": "YOUR_API_KEY" }, redirect: "error", }); ``` *** ## Search limits Return the maximum number of results your credential can request, retrieve, or export in one search. ```http theme={null} GET https://api.sixtyfour.ai/search/limits ``` ```json theme={null} { "max_results": 5000 } ``` API keys get 5,000 unless your organization has a custom ceiling. The ceiling applies to `max_results` on `POST /search/start-deep-search`, `POST /search/query`, and `POST /search/export`; a higher value fails the request. ## Rate limits For per-endpoint limits, see [Rate Limits](/api-reference/rate-limits#search). ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for more information. ## Errors For error responses (400, 403, 404, 409, 429, etc.), see [Handling Errors](/api-reference/errors). # Search Status Source: https://docs.sixtyfour.ai/api-reference/search/search-status https://api.sixtyfour.ai/openapi.json get /search/status/{task_id} Query the status of any async search task (deep search or export). # Start Deep Search (Async) Source: https://docs.sixtyfour.ai/api-reference/search/start-deep-search-async https://api.sixtyfour.ai/openapi.json post /search/start-deep-search Start a deep, agentic search from a natural language query. Returns a task ID to poll. # Start Lookalike Search (Async) Source: https://docs.sixtyfour.ai/api-reference/search/start-lookalike-search-async https://api.sixtyfour.ai/openapi.json post /search/lookalike Resolve the seed profiles and start a lookalike search. Poll `GET /search/lookalike/{task_id}`. # Delete Team Skill Source: https://docs.sixtyfour.ai/api-reference/skills/delete-team-skill https://api.sixtyfour.ai/openapi.json delete /skills/{name} Delete one of your team's skills at the `version` a read returned. Who may is as for a save. # List Team Skills Source: https://docs.sixtyfour.ai/api-reference/skills/list-team-skills https://api.sixtyfour.ai/openapi.json get /skills List your team's skills. Every member of the team can read them; with an API key, the key's team. # Put Team Skill Source: https://docs.sixtyfour.ai/api-reference/skills/put-team-skill https://api.sixtyfour.ai/openapi.json put /skills/{name} Create one of your team's skills (no `version`), or replace it at the `version` a read returned. A team owner or an organization admin can; an API key can when the person who created it can, as they stand now. # Read Team Skill Source: https://docs.sixtyfour.ai/api-reference/skills/read-team-skill https://api.sixtyfour.ai/openapi.json get /skills/{name} Read one of your team's skills, with the version a replace or delete names. # Get Workflow Template Source: https://docs.sixtyfour.ai/api-reference/templates/get-workflow-template https://api.sixtyfour.ai/openapi.json get /templates/{template_key} Return one catalog template, e.g. ``/templates/personal-email-enrichment``. Accepts the template's `slug` or its `id`. A key of one of your organization's own templates (``:``) returns 404 here; those are listed by `GET /templates/available`. # List Available Templates Source: https://docs.sixtyfour.ai/api-reference/templates/list-available-templates https://api.sixtyfour.ai/openapi.json get /templates/available The catalog plus your organization's own templates, in one list. # List Workflow Templates Source: https://docs.sixtyfour.ai/api-reference/templates/list-workflow-templates https://api.sixtyfour.ai/openapi.json get /templates Return the curated catalog, newest first. No authentication required. # List Test Webhook Events Source: https://docs.sixtyfour.ai/api-reference/webhook-testing/list-test-webhook-events https://api.sixtyfour.ai/openapi.json get /webhooks/test-events Discover synthetic samples. Does not run jobs, send callbacks or charge credits. # Send Test Webhook Event Source: https://docs.sixtyfour.ai/api-reference/webhook-testing/send-test-webhook-event https://api.sixtyfour.ai/openapi.json post /webhooks/test-events Send a sample event to your HTTPS receiver, signed and retried as real deliveries are. A call can take up to 100 seconds. `delivered` is true only when the receiver returned 2xx. Each call sends a new event, so don't retry it automatically. # Incoming Webhooks Source: https://docs.sixtyfour.ai/api-reference/webhooks/incoming Trigger workflows by POSTing data to Sixtyfour. Start a workflow run by POSTing data from an external system, such as a CRM, a scheduler or your own tools. > [!TIP] > > An incoming webhook is a workflow whose first block is the `webhook` source block. POST a payload to `/workflows/run`, and that payload becomes the workflow's input dataset. ## Headers | Name | Type | Required | Description | | - | - | - | - | | x-api-key | string | Yes, unless you send `Authorization` | Your Sixtyfour API key | | Authorization | string | No | `Bearer ACCESS_TOKEN` for an [OAuth client](/guides/oauth-clients) | | Content-Type | string | Yes | `application/json` | ## Configure the webhook block The `webhook` block is configured inside the workflow editor. It defines the shape of the data your workflow expects. | Spec field | Type | Required | Description | | - | - | - | - | | `input_schema` | object | Recommended | Map of column name → field schema. Declares the columns the workflow expects so they can be referenced by downstream blocks. | | `webhook_input` | string (JSON) | No | Default sample payload used when the workflow runs without `webhook_payload`: test runs in the editor, scheduled runs, and API runs that send no payload. | | `dataframe_type` | string | Yes | `LEAD` or `COMPANY`. Determines which downstream blocks can consume the output. | Each row in your POSTed payload becomes one row in the workflow's input dataset. Columns not declared in `input_schema` still pass through to downstream blocks. See the [Workflow Blocks reference](/api-reference/workflows/workflow-blocks) for where this fits in the block catalog. ## Trigger a workflow run POST your payload to `/workflows/run` with the workflow ID as a query parameter. ```http theme={null} POST https://api.sixtyfour.ai/workflows/run?workflow_id={workflow_id} ``` ### Request Body | Field | Type | Description | | - | - | - | | `webhook_payload` | array or object | Input data for the workflow. Can be a list of records (multi-row) or a single object (one row). | | `specs_override` | object | Optional. Override specs of the source block. Open **View Workflow Definition** in the workflow editor's More options menu (⋮) to see the block's specs. | > [!WARNING] > > Unknown body keys are ignored. If `webhook_payload` is missing or misspelled, for example `webhookPayload`, the run processes the block's saved `webhook_input` instead of your rows, and it is billed. ### Example Request ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/workflows/run?workflow_id=WORKFLOW_ID" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_payload": [ { "company_name": "Acme Corp", "website": "acme.com", "industry": "Technology" }, { "company_name": "TechStart Inc", "website": "techstart.io", "industry": "Software" } ] }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/workflows/run", headers={ "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json", }, params={"workflow_id": "WORKFLOW_ID"}, json={ "webhook_payload": [ {"company_name": "Acme Corp", "website": "acme.com"}, {"company_name": "TechStart Inc", "website": "techstart.io"}, ] }, ) job_id = response.json()["job_id"] ``` ```javascript JavaScript theme={null} const response = await fetch( "https://api.sixtyfour.ai/workflows/run?workflow_id=WORKFLOW_ID", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ webhook_payload: [ { company_name: "Acme Corp", website: "acme.com" }, { company_name: "TechStart Inc", website: "techstart.io" }, ], }), } ); const { job_id } = await response.json(); ``` ### Response (200) ```json theme={null} { "status": "RUNNING", "workflow_id": "3f8b2c1e-6d4a-4f9b-8e2c-1a5b7c9d0e3f", "job_id": "0b9d7c1e-2f4a-4e8b-9c3d-5a6f7e8d9c0b" } ``` Use the returned `job_id` to track execution. See [Workflow Execution](/api-reference/workflows/workflows-execution) for status, results, and cancellation endpoints. ## Rules and constraints * Top-level keys that start with `_` are reserved; a row with one is rejected. * `read_csv` source blocks are triggered with `specs_override.resource_handle_id` (from `/storage/csv/upload`), not `webhook_payload`. See [Workflow Execution](/api-reference/workflows/workflows-execution). * Payloads missing required columns from `input_schema`, or malformed JSON, are rejected with a validation error. * Workflows must have exactly one source block. ## Receiving results Incoming webhooks only handle the trigger. To receive workflow output back at your URL, add an `outgoing_webhook` block to the workflow — see [Outgoing Webhooks](/api-reference/webhooks/outgoing). Or poll for completion and download results with the [Workflow Execution](/api-reference/workflows/workflows-execution) endpoints. ## Errors For error response shapes (400, 422, etc.), see [Handling Errors](/api-reference/errors). # Outgoing Webhooks Source: https://docs.sixtyfour.ai/api-reference/webhooks/outgoing Receive HTTP notifications from Sixtyfour when async jobs and workflows complete. Sixtyfour POSTs to your URL when work completes, so you react in real time instead of polling. Outgoing webhooks come from two sources: | Source | Trigger | Payload | | - | - | - | | Async-job webhook | `webhook_url` in the body of an async endpoint | Job result envelope | | Workflow `outgoing_webhook` block | The block in a workflow | Workflow run envelope with results | Headers, signing, retries and destination checks are the same for both sources. The payload and the accepted URL schemes differ. ## Async-job webhooks Pass an optional `webhook_url` in the request body. When the job completes, Sixtyfour POSTs the result to that URL. Synchronous `/people-intelligence`, `/company-intelligence`, and `/qa-agent` requests that include `webhook_url` also send this callback. ### Supported endpoints | Endpoint | Callback | | - | - | | `/find-email-async`, `/find-email-bulk-async` | Job result envelope | | `/find-phone-async`, `/find-phone-bulk-async` | Job result envelope | | `/reverse-email-async`, `/reverse-email-bulk-async` | Job result envelope | | `/reverse-phone-async`, `/reverse-phone-bulk-async` | Job result envelope | | `/people-intelligence-async` | Job result envelope | | `/company-intelligence-async` | Job result envelope | | `/qa-agent-async` | Job result envelope | | `/bulk-intelligence/people`, `/bulk-intelligence/company` | [Workflow run envelope](#workflow-outgoing_webhook-block), with `run_id` set to the job's `task_id`. Set `webhook_url` in the job config; the result file gains `webhook_status_code`, `webhook_response` and `webhook_success` columns. See [Bulk Intelligence](/api-reference/endpoint/bulk-intelligence#webhook-callback). | | `POST /monitors`, `POST /monitors/linkedin`, `POST /monitors/upload` (`config.webhook_url`), `POST /monitors/{monitor_id}/update`, and the workflow [`monitor` block](/api-reference/workflows/workflow-blocks#monitor) | Monitor notifications. See [Monitor Notifications](/api-reference/monitors/monitor-notifications). | Async-job endpoints and Bulk Intelligence accept only `https://` URLs. Monitors and the workflow `outgoing_webhook` block accept `http://` and `https://`. Every destination must pass the [URL rules](#url-rules). For rejected URLs, see [Handling Errors](/api-reference/errors). ### Request ```json theme={null} POST /find-email-async { "lead": { "name": "Jane Smith", "company": "Northgate Labs", "linkedin_url": "https://linkedin.com/in/janesmith" }, "webhook_url": "https://your-server.com/webhooks/sixtyfour" } ``` The endpoint returns a `task_id` immediately: ```json theme={null} { "task_id": "550e8400-e29b-41d4-a716-446655440000", "status": "RUNNING", "total_leads": 1, "mode": "PROFESSIONAL" } ``` ### Payload ```json theme={null} { "task_id": "550e8400-e29b-41d4-a716-446655440000", "status": "completed", "task_type": "find_email", "result": { "leads": [ { "name": "Jane Smith", "company": "Northgate Labs", "email": [["jane.smith@northgatelabs.com", "OK", "COMPANY"]], "other_emails": [] } ], "total_leads": 1, "emails_found": 1, "cost_cents": 5, "cost_credits": 0.5, "mode": "PROFESSIONAL" }, "error": null, "event_id": "f7c2a4e0-9b5d-4a1f-8d2c-1e3b5f7a9c0d", "attempt": 1 } ``` | Field | Type | Description | | - | - | - | | `task_id` | string | The job's ID, as returned by the async endpoint. | | `status` | string | `completed`. | | `task_type` | string \| null | Job type. See the table below. | | `result` | object | The job result. See [Result fields by endpoint](#result-fields-by-endpoint). | | `error` | null | Always null. Failures are reported through `/job-status/{task_id}`. | | `event_id` | string (UUID) | Unique per delivery and the same across its retries. A job can send more than one delivery, each with its own `event_id`, so dedupe on `task_id`. | | `attempt` | integer | Delivery attempt, starting at `1`. | | `investigation` | object | People Intelligence `xhigh` only: `{ "id", "workspace_url" }` of the [Atlas investigation](/atlas/integrations#webhooks) the job created. | ### Result fields by endpoint | Endpoint family | `task_type` | `result` shape | | - | - | - | | Find Email | `find_email` | `leads`, `total_leads`, `emails_found`, `cost_cents`, `cost_credits`, `mode` | | Find Phone | `find_phone` | `leads`, `total_leads`, `phones_found`, `cost_cents`, `cost_credits` | | Reverse Email, Reverse Phone | `reverse_enrichment` | `leads`, `total_leads`, `successful_lookups`, `cost_cents`, `cost_credits` | | People Intelligence | `lead_enrichment` | Same as the job-status `result`. `xhigh` jobs also carry the research notes, graph and workspace files. | | Company Intelligence | `company_enrichment` | Same as the job-status `result` | | QA Agent | `null` | Same as the job-status `result` | Contact lookups wrap records in `result.leads`, even for one record; `GET /job-status/{task_id}` returns a single record as `result` ([details](/api-reference/async-jobs#contact-lookups-in-job-status-and-webhooks)). `cost_credits` is the amount charged; `cost_cents` is [deprecated](/guides/credits-and-pricing#costs-in-api-responses). A non-null `task_type` matches the `Sixtyfour-Event-Type` header and job status. For People Intelligence, Company Intelligence and QA Agent, the callback can arrive before `GET /job-status/{task_id}` reads `completed`. Read `charge_credits` once it does. See [Async jobs](/api-reference/async-jobs#webhooks). ### Job failures Sixtyfour sends callbacks for completed jobs only; failed, cancelled and timed-out jobs send none. If no callback arrives by your deadline, poll `GET /job-status/{task_id}`. ## Workflow `outgoing_webhook` block Add an `outgoing_webhook` block to POST the run's results to your URL. The block sends one request per run, not per row. When no rows reach the block, for example because a filter removed every row, it sends nothing. To catch that case, poll the run's [live status](/api-reference/workflows/workflows-execution#get-live-status) when no delivery arrives. ### Block specs | Field | Type | Default | Description | | - | - | - | - | | `url` | string | required | Destination URL. `http://` or `https://`, public host only. | | `headers` | object | none | Extra request headers. Invalid header names are dropped. | | `timeout_seconds` | float | `10` | Per-attempt timeout in seconds, up to 150. Values up to 300 are accepted and run as 150. | | `include_csv_download_link` | boolean | `false` | Include download URLs for the previous block's results. The delivery waits up to 120 seconds for them; a URL that isn't ready by then is `null`. | The block adds three columns to the workflow output: `webhook_status_code`, `webhook_response` (first 2,000 characters) and `webhook_success`. ### Payload ```json theme={null} { "event_id": "f7c2a4e0-9b5d-4a1f-8d2c-1e3b5f7a9c0d", "event": "outgoing_webhook.completed", "status": "success", "workflow_id": "3f8b2c1e-6d4a-4f9b-8e2c-1a5b7c9d0e3f", "run_id": "0b9d7c1e-2f4a-4e8b-9c3d-5a6f7e8d9c0b", "organization_id": "acme", "row_count": 95, "previous_block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b02", "previous_block_name": "lead_enrichment", "previous_block_number": 2, "completed_at": "2026-04-22T20:00:00Z", "duration_ms": 1234, "attempt": 1, "results_download_url": "https://api.sixtyfour.ai/storage/download/v1.eyJlIjoxNzkzNjQ5...", "ndjson_download_url": null, "results": [ { "name": "Jane Smith", "email": "jane.smith@northgatelabs.com" } ] } ``` | Field | Type | Description | | - | - | - | | `event_id` | string (UUID) | Unique per delivery and the same across its retries. Matches the `Sixtyfour-Event-Id` header. A block that runs again sends a new `event_id`; dedupe on `run_id` and `previous_block_id`. | | `event` | string | Always `outgoing_webhook.completed`. | | `status` | string | Always `success`. | | `workflow_id` | string | The workflow definition ID. | | `run_id` | string | The run ID. | | `organization_id` | string | Your organization ID. | | `row_count` | integer | Rows in the result, including rows that failed at an earlier step. | | `previous_block_id` | string | ID of the block before the webhook block. | | `previous_block_name` | string | Block type of the previous block, such as `lead_enrichment`. | | `previous_block_number` | integer | Sequence number of the previous block. | | `completed_at` | string (ISO 8601) | When this attempt was sent. | | `duration_ms` | integer | Time spent in the webhook block. | | `attempt` | integer | Delivery attempt, starting at `1`. | | `results_download_url` | string \| null | Link to the previous block's CSV result file when `include_csv_download_link` is `true`. | | `ndjson_download_url` | string \| null | Link to the previous block's NDJSON result file when `include_csv_download_link` is `true` and the run requested NDJSON results. | | `results` | array \| null | Inline rows when the input is at most 200 rows and 512 KB; otherwise `null`. Rows are the ones entering the webhook block, without keys that start with `_`. | Fetch both links with an `x-api-key` header from the run's organization within 15 minutes. For new links, call [Download results](/api-reference/workflows/workflows-execution#download-results) with the `run_id`. A CSV link returns [spreadsheet-safe](/api-reference/workflows/workflows-execution#spreadsheet-safe-csv) cells unless you add `?spreadsheet_safe=false`. ### Failed rows in the payload The payload includes rows that failed at an earlier step, with `Failed` in the columns their failing step would have filled. The payload has no per-row status, so identify failed rows from the final result: wait for the run to reach a terminal status and read its `Status` column. See [Failed rows](/api-reference/workflows/workflows-execution#failed-rows) and [Download results](/api-reference/workflows/workflows-execution#download-results). A run where every row fails ends as `failed`, and its results can still be downloaded. ## Delivery ### Headers | Header | Value | | - | - | | `Content-Type` | `application/json` | | `Sixtyfour-Signature` | `t=,v1=[,v1=]` | | `Sixtyfour-Event-Id` | UUID per delivery, same across retries. Monitor notifications send their `event_group_id` or `run_id` | | `Sixtyfour-Event-Type` | `task_type` for async jobs (omitted when it is null), `outgoing_webhook.completed` for the block, the `type` for monitor notifications | | `Sixtyfour-Delivery-Attempt` | Attempt counter starting at `1` | Sixtyfour sends the `Sixtyfour-*` headers only when your organization has a [signing secret](/api-reference/webhooks/signing-secrets). [Test deliveries](/guides/using-webhooks#test-deliveries) always add `Sixtyfour-Test` headers. ### Retries | Setting | Value | | - | - | | Attempts | 5 (1 initial attempt and 4 retries) | | Backoff | 1s, 2s, 4s, 8s between attempts | | Timeout | 10 seconds per attempt for async jobs; `timeout_seconds` for the block | | Success | Any `2xx` response | | Redirects | Not followed. A `3xx` response counts as a failed attempt. | If every attempt fails, the delivery is dropped. The job or run itself is unaffected, and its results stay available by polling. ### URL rules Sixtyfour checks the destination before every attempt. URLs with credentials, `localhost`, `*.local` hosts, and hosts that resolve to private, loopback, link-local or other non-public IPs are rejected and not retried. A DNS lookup that fails counts as a failed attempt and is retried. ### Receiver checklist * Return `2xx` within the timeout and process the payload asynchronously. * Return `2xx` for events you ignore. Any other status triggers retries. * Dedupe on the job or run, not only `event_id`: `task_id` for async jobs, `run_id` with `previous_block_id` for the workflow block, and `type` with `event_group_id` or `run_id` for monitor notifications. `event_id` covers only retries of one delivery, and monitor payloads have none. See the [receiver example](/guides/using-webhooks#handle-deliveries). * Once signing is enabled, reject deliveries without a valid `Sixtyfour-Signature`. ## Polling fallback Results stay available whether or not a webhook is delivered: * Async jobs: `GET /job-status/{task_id}` * Workflow runs: the status and download endpoints in [Workflow Execution](/api-reference/workflows/workflows-execution) ## Errors For error responses (400, 403, etc.), see [Handling Errors](/api-reference/errors). # Signing Secrets & Verification Source: https://docs.sixtyfour.ai/api-reference/webhooks/signing-secrets Verify that webhook deliveries come from Sixtyfour with HMAC-SHA256 signatures. A signing secret lets your server prove each webhook delivery came from Sixtyfour and was not altered or replayed. Each organization has at most one active signing secret. Once it exists, every [outgoing webhook](/api-reference/webhooks/outgoing) (async-job callbacks, workflow `outgoing_webhook` blocks, and [monitor notifications](/api-reference/monitors/monitor-notifications)) carries a `Sixtyfour-Signature` header. Without a secret, deliveries are unsigned. ## Manage the secret Manage the secret in **Settings → Webhooks**, on the **Signing Secret** card. Every member can see whether a secret exists and when it was created; only organization admins and owners can create, roll or revoke it. | Action | Effect | | - | - | | **Create secret** | Generates a secret and shows it once. Copy it into your secret manager; Sixtyfour cannot show it again. | | **Roll signing secret** → **24-hour overlap** | Issues a new secret. For 24 hours every delivery carries two signatures, one per secret, so you can deploy the new secret without dropping deliveries. The old secret then stops signing. | | **Roll signing secret** → **Immediate rollover** | Issues a new secret and stops signing with the old one immediately. Use this when a secret leaks. | | **Revoke secret** | Stops signing. Deliveries continue unsigned and without `Sixtyfour-*` headers. | ## Signature format ```text theme={null} Sixtyfour-Signature: t=1745337600,v1=3b8f2c... ``` | Part | Meaning | | - | - | | `t` | Unix timestamp (seconds) when the attempt was signed. Each retry is signed again with a new timestamp. | | `v1` | Hex HMAC-SHA256 of `"{t}." + raw_body`, keyed with the secret. During a rotation overlap there are two `v1` values: the new secret's first, then the old one's. | The body is compact JSON. Compute the HMAC over the exact bytes you received, before any JSON parsing or re-serialization. ## Verify signatures 1. Read the raw request body. 2. Parse `Sixtyfour-Signature` into `t` and every `v1`. 3. Reject the request if `t` is more than 5 minutes from your clock. 4. Compute `HMAC-SHA256(secret, f"{t}." + raw_body)` as hex. 5. Accept if it matches any `v1` with a constant-time comparison. ```python Python theme={null} import hashlib import hmac import time TOLERANCE_SECONDS = 300 def verify(raw_body: bytes, header: str, secret: str) -> bool: timestamp, signatures = None, [] for part in header.split(","): key, _, value = part.strip().partition("=") if key == "t": timestamp = value elif key == "v1": signatures.append(value) if not timestamp or not timestamp.isdigit() or not signatures: return False if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: return False expected = hmac.new( secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256 ).hexdigest() return any(hmac.compare_digest(expected, sig) for sig in signatures) ``` ```javascript JavaScript theme={null} import crypto from "node:crypto"; const TOLERANCE_SECONDS = 300; export function verify(rawBody, header, secret) { let timestamp = null; const signatures = []; for (const part of header.split(",")) { const [key, value] = part.trim().split("=", 2); if (key === "t") timestamp = value; else if (key === "v1") signatures.push(value); } if (!timestamp || !/^\d+$/.test(timestamp) || signatures.length === 0) return false; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) return false; const expected = crypto .createHmac("sha256", secret) .update(`${timestamp}.`) .update(rawBody) .digest("hex"); return signatures.some( (sig) => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)), ); } ``` `rawBody` must be the unparsed request body, for example from `express.raw({ type: "application/json" })`. ## Behavior * While a signing secret is active, Sixtyfour never sends an unsigned delivery; a delivery it cannot sign fails. * The timestamp is inside the signed payload, so an old delivery cannot be replayed with a fresh `t`. Dedupe deliveries as the [receiver checklist](/api-reference/webhooks/outgoing#receiver-checklist) describes. ## Errors For error responses (401, 403, etc.), see [Handling Errors](/api-reference/errors). # Create Schedule Source: https://docs.sixtyfour.ai/api-reference/workflow-schedules/create-schedule https://api.sixtyfour.ai/openapi.json post /schedules Create a schedule that runs a workflow on a cron expression. The schedule belongs to the workflow's team, and its runs are attributed to that team. It keeps running if the person who created it leaves. # Delete Schedule Source: https://docs.sixtyfour.ai/api-reference/workflow-schedules/delete-schedule https://api.sixtyfour.ai/openapi.json delete /schedules/{schedule_id} Delete a schedule. # Get Schedule Source: https://docs.sixtyfour.ai/api-reference/workflow-schedules/get-schedule https://api.sixtyfour.ai/openapi.json get /schedules/{schedule_id} Get a schedule. # List Schedules Source: https://docs.sixtyfour.ai/api-reference/workflow-schedules/list-schedules https://api.sixtyfour.ai/openapi.json get /schedules List schedules in the caller's active team, optionally for one workflow. # Update Schedule Source: https://docs.sixtyfour.ai/api-reference/workflow-schedules/update-schedule https://api.sixtyfour.ai/openapi.json patch /schedules/{schedule_id} Update a schedule. Send only the fields to change. Setting `is_active` to true makes you, or the API key you call with, the schedule's owner and clears `disabled_reason`. The schedule stays in its team; if you cannot run the workflow there, it pauses again when it next fires. # Cancel Workflow Run Source: https://docs.sixtyfour.ai/api-reference/workflow/cancel-workflow-run https://api.sixtyfour.ai/openapi.json post /workflows/cancel Request cancellation of a run. When `cancellation_requested` is `true`, keep polling until the run reaches a final status. Results and charges for completed work remain; a run that already ended keeps its outcome. # Create Workflow Source: https://docs.sixtyfour.ai/api-reference/workflow/create-workflow https://api.sixtyfour.ai/openapi.json post /workflows/create_workflow Create a new workflow from a block graph definition. # Delete Workflow Source: https://docs.sixtyfour.ai/api-reference/workflow/delete-workflow https://api.sixtyfour.ai/openapi.json post /workflows/delete_workflow Delete a workflow and its run history. # Get Workflow Source: https://docs.sixtyfour.ai/api-reference/workflow/get-workflow https://api.sixtyfour.ai/openapi.json get /workflows/{workflow_id} Retrieve a workflow's full details, including its block graph definition. # Get Workflow Run (Live Status) Source: https://docs.sixtyfour.ai/api-reference/workflow/get-workflow-run-live-status https://api.sixtyfour.ai/openapi.json get /workflows/runs/{run_id}/live_status A run's status and per-block progress. It shares the job status polling budget; see [Rate Limits](/api-reference/rate-limits#status-polling). # List Available Blocks Source: https://docs.sixtyfour.ai/api-reference/workflow/list-available-blocks https://api.sixtyfour.ai/openapi.json get /workflows/blocks List all workflow block types with their input specs schemas. # List Run Result Downloads Source: https://docs.sixtyfour.ai/api-reference/workflow/list-run-result-downloads https://api.sixtyfour.ai/openapi.json get /workflows/runs/{run_id}/results/download-links Signed download links for a run's result files. Each link works for 15 minutes. # List Workflow Runs Source: https://docs.sixtyfour.ai/api-reference/workflow/list-workflow-runs https://api.sixtyfour.ai/openapi.json get /workflows/runs List recent workflow runs for the organization, sorted by most recent first. Optionally filter by `status` (`running`, `completed`, `cancelled`, or `active` for queued+running) and `workflow_id`. # List Workflows Source: https://docs.sixtyfour.ai/api-reference/workflow/list-workflows https://api.sixtyfour.ai/openapi.json get /workflows List the workflows in the organization the caller can access. Returns lightweight metadata without block definitions; use `GET /workflows/{workflow_id}` for the full definition. # Run Workflow Source: https://docs.sixtyfour.ai/api-reference/workflow/run-workflow https://api.sixtyfour.ai/openapi.json post /workflows/run Start a workflow run and return a job ID for tracking. Supports per-run spec overrides via `specs_override` and webhook input via `webhook_payload`. API-triggered `read_csv` workflows must provide `specs_override.resource_handle_id` from `/storage/csv/upload`. # Update Workflow Source: https://docs.sixtyfour.ai/api-reference/workflow/update-workflow https://api.sixtyfour.ai/openapi.json post /workflows/update_workflow Update a workflow's metadata or block definition. Creates the workflow if it doesn't exist. # Workflow Blocks Source: https://docs.sixtyfour.ai/api-reference/workflows/workflow-blocks Reference for all workflow block types supported by the Workflows API. Block types for workflow definitions. `GET /workflows/blocks` returns every block with its specs schema. > [card] [API Reference](/api-reference/workflow/list-available-blocks) > > See the full request/response schema and parameters in the API Reference. ## Get available blocks Retrieve the list of all workflow block types supported by the API, along with their specs schemas. ```http theme={null} GET https://api.sixtyfour.ai/workflows/blocks ``` Returns an array of block descriptors. Each entry includes the block's name (used as `block_name` when building a workflow) and a `specs_schema` object describing its configurable fields. ## Available blocks | Block name | Workflow Builder name | Purpose | | - | - | - | | `append_to_notebook` | Append to Notebook | Append or upsert rows into a notebook tab, with column mapping and a duplicate action of `skip`, `update`, or `merge` | | `backlinks_referring_domains` | Backlinks Referring Domains | List the domains that link to a target domain or URL | | `build_graph` | Knowledge Graph | Add each row to the [Lattice](/lattice/getting-started) graph named by `subgraph_id`. A workflow can have one, and it must be the last block. In beta and enabled on request | | `company_enrichment` | Enrich Company | Enrich company rows with your `return_fields`; optionally find people at each company | | `company_to_leads` | Extract Leads | Expand company rows into one row per lead | | `deduplicate` | Deduplicate Rows | Remove duplicate rows by one or more columns. Set `keep` to `"first"` (default), `"last"`, or `"false"` to drop every row that has a duplicate | | `deduplicate_wrt_notebook` | Deduplicate with Notebook | Drop rows that already exist in a notebook tab, or set `keep_duplicates` to keep only those rows | | `filter` | Filter | Keep rows that match a query string or field conditions (`include`, `exclude`, `contains`, `not_contains`) | | `find_email` | Find Email | Find a professional or personal email for each lead. See [Find Email](#find-email) | | `find_phone` | Find Phone | Find a phone number for each lead | | `find_subdomains` | Find Subdomains | Discover subdomains of a domain, with optional first-seen and last-seen date filters | | `github_find_people` | GitHub Contributors | Get the contributors, stargazers, or watchers of GitHub repositories; requires a GitHub integration | | `github_search` | GitHub Search | Search GitHub repositories, users, or code; requires a GitHub integration | | `google_maps_search` | Google Maps Search | Find places by query, coordinates, Place ID, or CID, with business details and phone numbers | | `group_by` | Group By | Group rows by one or more columns and aggregate the rest, or nest grouped rows | | `hubspot_import` | HubSpot Import | Import HubSpot `contacts` or `companies`; requires a HubSpot integration | | `kaggle_leaderboard` | Kaggle Leaderboard | Get the participants of Kaggle competition leaderboards | | `lead_enrichment` | Enrich People | Enrich lead rows with your `return_fields` | | `leads_to_company` | Leads to Company | Group lead rows into company rows with a nested list of leads; the inverse of `company_to_leads` | | `linkedin_enrichment` | Enrich Profile | Enrich rows from the professional profile URL in `linkedin_url_column` (default `linkedin_url`) | | `llm_openai` | LLM Output | Generate text for each row from a prompt template; use `{column_name}` to insert column values and choose the model in the block | | `monitor` | Monitor | Start a monitor that keeps watched fields current for the rows that reach this block. See [Monitor](#monitor) | | `outgoing_webhook` | Outgoing Webhooks | Send the block's input dataset to your URL in one request. See [Outgoing Webhooks](/api-reference/webhooks/outgoing) | | `qa_agent` | — | Answer a question for each row with an agent that can use tools, up to `max_tool_calls` (default 10) | | `read_notebook` | Read Notebook | Read a notebook tab as `LEAD` or `COMPANY` rows; requires `notebook_id`, `tab_id`, and `dataframe_type` | | `remove_columns` | Remove Columns | Drop the listed columns | | `research_agent` | Web Research Agent | Research each row on the web and return the fields in `struct` | | `reverse_email` | Reverse Email | Identify the person or company behind an email address | | `reverse_phone` | Reverse Phone | Identify the person or company behind a phone number | | `scrape_web` | Scrape Web URL | Fetch the page in `url_column` as `markdown` (default) or `html`, with optional JavaScript rendering and location | | `search` | Search | Load the results of a saved search as workflow rows; requires `dataframe_type` | | `send_slack_message` | Slack | Post to Slack channels or DMs; use `{column}` to insert values and `{@column}` to tag a user by email | | `tiktok_get_following` | TikTok Following | List the accounts a TikTok profile follows | | `transform_data` | Transform Data | Reshape each row into your `output_schema`, with optional instructions and a `low` or `medium` tier | | `verify_email` | Verify Email | Check whether each email is deliverable (default column `email`) | | `verify_phone` | Verify Phone | Check whether each phone number is valid and belongs to the lead. See [Verify Phone](#verify-phone) | | `webhook` | Incoming Webhooks | Start a workflow from data you send; requires `dataframe_type`. See [Incoming Webhooks](/api-reference/webhooks/incoming) | | `x_search` | X Search | Find X profiles that match a search query | | `yc_batches` | — | List Y Combinator batches with their company counts | | `yc_companies` | Y Combinator Companies | List Y Combinator companies, filtered by batch, industry, tags, or status | A dash means the block is not in the Workflow Builder. ### Find Email `find_email` finds a work email (`mode: "PROFESSIONAL"`) or a personal email (`mode: "PERSONAL"`) for each row. **Inputs** | Field | Default | Description | | - | - | - | | `mode` | — | Required. `PROFESSIONAL` finds a work email; `PERSONAL` finds a personal email. | | `verify_emails` | `true` | Verify each email found. | | `skip_rows_with_existing_input` | `false` | When `true`, rows that already have the email skip the lookup and get status `PROVIDED`. | **Skipping rows that already have the email** With `skip_rows_with_existing_input: true`, put the email you already have in one of these columns: | Mode | Column | Skipped when | | - | - | - | | `PROFESSIONAL` | `work_email`, `email` or `email_address` | The value is an email on a company domain. A personal address such as gmail.com does not count, so the row is still looked up. | | `PERSONAL` | `personal_email` | The value is an email. | Blank, `N/A` and malformed values count as missing, so the row is looked up. Skipped rows keep their position and input values and get `email_status` (`personal_email_status` in `PERSONAL` mode) set to `PROVIDED`. In `PROFESSIONAL` mode the matched work email is also written to `email` when that column is blank or holds a personal address. **Output columns** | Mode | Columns | | - | - | | `PROFESSIONAL` | `email`, `email_status`, `email_type`, `other_emails` | | `PERSONAL` | `personal_email`, `personal_email_status`, `personal_email_type`, `personal_other_emails` | ### Verify Phone `verify_phone` checks each row's phone number against the person's full name. Every row gets all twelve output columns, `null` when not found. Rows skipped for missing input keep any existing verification values; see [Handling Errors](/api-reference/errors#verify-phone-column-mappings). **Inputs** | Field | Default | Description | | - | - | - | | `phone_column` | `phone` | Column containing the phone number. | | `name_column` | `name` | Column containing the person's full name, required for verification. Must match the actual input column, such as `fullName` or `full_name`. | | `email_column` | — | Optional column containing an email address. Populates the `email_*` output columns. | | `ip_address_column` | — | Optional column containing an IP address, used as an additional verification signal. | | `address_street_line_1_column`, `address_city_column`, `address_state_code_column`, `address_postal_code_column`, `address_country_code_column` | — | Optional columns containing postal address parts. Populate the `address_*` output columns. | | `country_hint` | — | Optional ISO 3166-1 alpha-2 region code used to parse numbers written in national format. | **Output columns** | Column | Description | | - | - | | `phone_is_valid` | Whether the number is valid. | | `phone_activity_score` | Activity score for the number. | | `phone_line_type` | Line type of the number. | | `phone_name_match` | Whether the name matches the number's owner. | | `phone_contact_grade` | Contact grade for the phone. | | `email_is_valid` | Whether the email is valid. | | `email_name_match` | Whether the name matches the email's owner. | | `email_contact_grade` | Contact grade for the email. | | `address_is_valid` | Whether the address is valid. | | `address_name_match` | Whether the name matches the address. | | `phone_warnings` | Warnings for the row, such as `Missing Name`. | | `phone_errors` | Error details when the row could not be verified. | ### Monitor `monitor` starts one [monitor](/api-reference/monitors/monitors-overview) over the rows that reach it. It runs once all rows have arrived and returns no rows, so place it last in the workflow. Each workflow run starts its own monitor, which keeps checking on its own schedule after the run ends. **Specs** | Field | Default | Description | | - | - | - | | `key_columns` | — | Columns that identify each row. Required. Rows with no value in any of them are skipped. Rows with the same values are watched once. | | `watched` | — | Watched fields: field name → description, or an object with `type`, `description`, and `subfields`. Required. A column with the same name as a watched field sets that field's starting value. | | `frequency` | `1d` | How often the monitor checks, `1h` to `365d`. Independent of how often the workflow runs. | | `first_check` | `now` | `now` runs setup immediately only for rows missing watched values; complete rows wait for the schedule. `next_tick` defers every row to the first scheduled fire. | | `tier` | `low` | Depth of the full re-check: `low`, `medium`, or `high`. | | `subject_type` | `company` | `company` or `lead`. | | `research_plan` | — | Instructions for the enrichment that re-derives the fields. | | `name` | watched field names | Name of the monitor the run starts. | | `webhook_url` | — | URL that receives the monitor's [notifications](/api-reference/monitors/monitor-notifications). `http://` or `https://`, public host only. Checked when the block runs; an invalid URL fails this step. | | `webhook_event_types` | `["monitor.field.changed"]` | Notifications to send: any of `monitor.field.changed`, `monitor.execution.completed`, `monitor.execution.failed`, and `monitor.run.completed`. | Retrieve the monitor a run started with `GET /monitors/for-run/{workflow_run_id}/{block_id}`. See [Monitor Endpoints](/api-reference/monitors/monitors-endpoints#get-the-monitor-a-workflow-run-started). ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs. ## Errors For error responses (400, 403, 404, etc.), see [Handling Errors](/api-reference/errors). # Workflow Schedules Source: https://docs.sixtyfour.ai/api-reference/workflows/workflow-schedules Create, update, and delete cron-based schedules that trigger workflow runs automatically. Run a workflow automatically on a cron schedule, with no cron infrastructure of your own. > [card] [API Reference](/api-reference/workflow-schedules/create-schedule) > > See the full request/response schema and parameters in the API Reference. ## Schedule definition A schedule binds a `workflow_id` to a `cron_expression` and `timezone`: * `workflow_id` — the workflow to run. It must belong to your team. * `name` — required, up to 200 characters. * `cron_expression` — standard 5-field cron (`minute hour day month weekday`), up to 1,000 characters. * `timezone` — IANA timezone, e.g. `America/Los_Angeles`. Defaults to `UTC`. * `description` — optional free-form text, up to 2,000 characters. * `is_active` — defaults to `true`. Set to `false` to save a schedule without activating it. Each scheduled run uses the workflow's last valid saved definition, so edits apply from the next run. > [!WARNING] > > A scheduled run of a workflow that starts with a `webhook` block processes that block's saved sample input every time, and each run is billed. Without a saved sample input, every scheduled run fails. Send new rows with [Run Workflow](/api-reference/workflows/workflows-execution#run-workflow) instead. Schedules belong to their team. A schedule pauses, with `disabled_reason` set, when the API key it runs as is revoked or expires, when its team is archived, or when its creator is removed and the admin chooses to pause their schedules. ## Overlapping and missed runs * Every scheduled time starts a run, even when the schedule's previous run is still going, so runs can overlap. Keep the interval longer than a typical run. * A run that cannot start at its scheduled time, such as during an outage, starts late if it can start within 1 hour; otherwise it is skipped. * There is no minimum interval. `* * * * *` starts a run every minute, and each run is billed. ## Create schedule Create a new schedule for a workflow. ```http theme={null} POST https://api.sixtyfour.ai/schedules ``` Required body fields: `workflow_id`, `name`, and `cron_expression`. Returns `201` with the schedule, including up to three upcoming `next_run_times`. ### Example request ```json theme={null} { "workflow_id": "WORKFLOW_ID", "name": "Daily lead refresh", "cron_expression": "0 9 * * *", "timezone": "America/New_York", "description": "Re-enrich the prospect list every morning" } ``` ### Example response ```json theme={null} { "id": "a1b2c3d4-5e6f-4789-a123-b4c5d6e7f890", "workflow_id": "3f8b2c1e-6d4a-4f9b-8e2c-1a5b7c9d0e3f", "team_id": "3c4d5e6f-7a8b-4c9d-8e0f-1a2b3c4d5e6f", "name": "Daily lead refresh", "description": "Re-enrich the prospect list every morning", "cron_expression": "0 9 * * *", "timezone": "America/New_York", "is_active": true, "disabled_reason": null, "created_by": "f9e8d7c6-b5a4-4321-9876-543210fedcba", "api_key_id": null, "next_run_times": [ "2026-07-17T13:00:00Z", "2026-07-18T13:00:00Z", "2026-07-19T13:00:00Z" ], "created_at": "2026-07-16T18:00:00Z", "updated_at": "2026-07-16T18:00:00Z" } ``` *** ## List schedules Retrieve your team's schedules. ```http theme={null} GET https://api.sixtyfour.ai/schedules ``` Optional query parameter: `workflow_id` — filter to schedules for a single workflow. *** ## Get schedule Retrieve a single schedule by ID. ```http theme={null} GET https://api.sixtyfour.ai/schedules/{schedule_id} ``` *** ## Update schedule Update a schedule's name, cadence, timezone, description, or active state. ```http theme={null} PATCH https://api.sixtyfour.ai/schedules/{schedule_id} ``` All body fields (`name`, `cron_expression`, `timezone`, `description`, `is_active`) are optional — only include what you want to change. > [!NOTE] > > Setting `is_active: true` makes the caller, user or API key, the schedule's owner (`created_by`, `api_key_id`) and clears `disabled_reason`. The schedule stays in its team. ### Example request ```json theme={null} { "is_active": false } ``` *** ## Delete schedule Permanently delete a schedule. Returns `{"success": true, "id": "SCHEDULE_ID"}`. ```http theme={null} DELETE https://api.sixtyfour.ai/schedules/{schedule_id} ``` > [!NOTE] > > Deleting a schedule does not affect the underlying workflow or its run history. *** ## Example usage ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/schedules" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "workflow_id": "WORKFLOW_ID", "name": "Daily lead refresh", "cron_expression": "0 9 * * *", "timezone": "America/New_York" }' ``` ```python Python theme={null} import requests API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.sixtyfour.ai" headers = {"x-api-key": API_KEY, "Content-Type": "application/json"} response = requests.post( f"{BASE_URL}/schedules", headers=headers, json={ "workflow_id": "WORKFLOW_ID", "name": "Daily lead refresh", "cron_expression": "0 9 * * *", "timezone": "America/New_York", }, ) response.raise_for_status() schedule = response.json() next_run = schedule["next_run_times"][0] if schedule["next_run_times"] else "not yet computed" print(f"Created schedule {schedule['id']}, next run at {next_run}") ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": API_KEY, "Content-Type": "application/json" }; const response = await fetch(`${BASE_URL}/schedules`, { method: "POST", headers, body: JSON.stringify({ workflow_id: "WORKFLOW_ID", name: "Daily lead refresh", cron_expression: "0 9 * * *", timezone: "America/New_York", }), }); if (!response.ok) { throw new Error(`Failed to create schedule: ${await response.text()}`); } const schedule = await response.json(); const nextRun = schedule.next_run_times[0] ?? "not yet computed"; console.log(`Created schedule ${schedule.id}, next run at ${nextRun}`); ``` ## Pricing Scheduled runs are billed the same as manual runs. See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs. ## Errors For error responses (400, 401, 403, 422, etc.), see [Handling Errors](/api-reference/errors). # Workflow Execution Source: https://docs.sixtyfour.ai/api-reference/workflows/workflows-execution Run workflows, poll status, cancel runs, and download results. Run a saved workflow on your input, track it to completion, and download the results. > [card] [API Reference](/api-reference/workflow/run-workflow) > > See the full request/response schema and parameters in the API Reference. ## Run workflow Execute a workflow and receive a job ID for tracking. ```http theme={null} POST https://api.sixtyfour.ai/workflows/run ``` Runs use the workflow's last valid saved definition. | Query parameter | Required | Description | | - | - | - | | `workflow_id` | Yes | The workflow to run. | | `result_formats` | No | Extra result formats to save. Accepts `csv` and `ndjson`; repeat the parameter for more than one. CSV is always saved. | | `save_json` | No | Default `false`. Also save results as JSON, readable with `download-links?format=json`. | > [!NOTE] > > To be notified when a run finishes, add an [`outgoing_webhook`](/api-reference/webhooks/outgoing) block to the workflow. ### Idempotency-Key header (optional) Send an `Idempotency-Key` header to make retries safe. It is scoped to your organization and the workflow, and kept for 24 hours. A repeat request with the same key starts no new run and returns the original `job_id` with `already_started: true` and the run's current `status`, such as `running` or `completed`. The body is not compared, so a repeat with different rows still returns the original run. ```bash theme={null} curl -X POST "https://api.sixtyfour.ai/workflows/run?workflow_id=WORKFLOW_ID" \ -H "x-api-key: YOUR_API_KEY" \ -H "Idempotency-Key: your-job-id-123" \ -H "Content-Type: application/json" \ -d '{ "webhook_payload": [{ "name": "Jane Doe" }] }' ``` ### Request body (optional) | Field | Type | Description | | - | - | - | | `expected_revision` | integer | `edit_revision` from [Get workflow](/api-reference/workflows/workflows-management#get-workflow). The run starts only if the workflow is still at that revision and has no draft that failed validation. | | `specs_override` | object | Overrides the first block's specs for this run. For workflows that start from an uploaded file, pass `resource_handle_id`. | | `webhook_payload` | array or object | Input rows for workflows that start with a `webhook` block: a list of records or a single object. See [Incoming Webhooks](/api-reference/webhooks/incoming). | > [!WARNING] > > Unknown body keys are ignored. If `webhook_payload` is missing or misspelled, for example `webhookPayload`, the run processes the `webhook` block's saved sample input instead of your rows, and it is billed. ### Example request ```json theme={null} { "webhook_payload": [ { "company_name": "Acme Corp", "website": "acme.com", "industry": "Technology" }, { "company_name": "TechStart Inc", "website": "techstart.io", "industry": "Software" } ] } ``` For workflows that start from an uploaded file, pass the `handle_id` from [Uploading an input file](/api-reference/workflows/workflows-overview#uploading-an-input-file): ```json theme={null} { "specs_override": { "resource_handle_id": "22222222-2222-2222-2222-222222222222" } } ``` ### Response ```json theme={null} { "status": "RUNNING", "workflow_id": "3f8b2c1e-6d4a-4f9b-8e2c-1a5b7c9d0e3f", "job_id": "0b9d7c1e-2f4a-4e8b-9c3d-5a6f7e8d9c0b" } ``` `status` is uppercase `RUNNING` once the run has started. [Live status](#status-values) reports lowercase statuses. `already_started: true` is added when an `Idempotency-Key` matched an earlier request. Use `job_id` to track the run. `status` can also be `queued`: the run is accepted but has not started yet. Track it with `job_id` as usual; repeating the request with the same `Idempotency-Key` returns this run instead of starting another. If `webhook_payload` cannot be stored in time, the request returns `503` and starts no run; see [503 Service Unavailable](/api-reference/errors#503-service-unavailable). > [!NOTE] > > To run a workflow on a recurring cadence instead of calling Run Workflow manually, see [Workflow Schedules](/api-reference/workflows/workflow-schedules). *** ## List workflow runs Get execution history for your workflows. ```http theme={null} GET https://api.sixtyfour.ai/workflows/runs ``` | Query parameter | Description | | - | - | | `status` | Any [status value](#status-values) except `finalizing`, or `active` for queued and running runs. A finalizing run is listed as `running`. | | `workflow_id` | Only runs of this workflow. | | `limit` | 1–500, default 100. | Runs are sorted most recent first (`queued_at` descending). Failed runs carry `error_message` and `status_reason` (`null` when no reason was recorded). *** ## Get live status Monitor real-time progress of a running workflow. ```http theme={null} GET https://api.sixtyfour.ai/workflows/runs/{run_id}/live_status ``` `run_id` is the same as the `job_id` returned by Run Workflow. ### Status values | Status | Description | | - | - | | queued | Waiting to start | | running | Currently executing | | finalizing | Every block has finished and the result files are being saved. Results are ready at `completed` | | completed | Finished. Can include failed rows; see [Failed rows](#failed-rows) | | failed | Encountered an error, or every row failed before the end of the workflow | | cancelled | Cancelled through the API or the web app | | terminated | Stopped by Sixtyfour before it finished | | timeout | Stopped by a time limit | `queued`, `running`, and `finalizing` are in progress. Every other status is final, including any status not listed here. ### Polling recommendations * Poll every 10 seconds or slower. * Stop when `overall_status` is anything other than `queued`, `running`, or `finalizing`. * On `429` or `503`, wait for `Retry-After`, then poll again; see [Status polling](/api-reference/rate-limits#status-polling). * Use `overall_progress_percentage` for progress bars. *** ## Cancel run Stop an in-progress workflow execution. ```http theme={null} POST https://api.sixtyfour.ai/workflows/cancel ``` Pass `job_id` from the Run Workflow response as a query parameter. The response returns `job_id`, the run's current `status`, and `cancellation_requested`. Until the run stops, the response can show `queued` or `running` with `cancellation_requested: true`: ```json theme={null} { "job_id": "0b9d7c1e-2f4a-4e8b-9c3d-5a6f7e8d9c0b", "status": "running", "cancellation_requested": true } ``` Keep polling [live status](#get-live-status) until `overall_status` is no longer `queued`, `running`, or `finalizing`. A terminal cancellation response is: ```json theme={null} { "job_id": "0b9d7c1e-2f4a-4e8b-9c3d-5a6f7e8d9c0b", "status": "cancelled", "cancellation_requested": false } ``` > [!NOTE] > > If the run had already finished, the API returns its final status without changes. Results produced before cancellation stay available. *** ## Download results Get download links for a run's result files. ```http theme={null} GET https://api.sixtyfour.ai/workflows/runs/{run_id}/results/download-links ``` Request links once the run's status is final. Failed and cancelled runs keep the files saved before they stopped. The response is a JSON array with one entry for each block that saved results: * The final result file comes from the workflow's last block and is named `{run_id}.csv` (`{run_id}.json` or `{run_id}.ndjson` for those formats). It is the only file with the `Status` column described in [Failed rows](#failed-rows). * Every other entry is one block's output, named `{run_id}_{block_number}.csv` (or `.json` or `.ndjson`). ```json theme={null} [ { "filename": "4fe6a411-ecdc-4524-9724-be968488c503_1.csv", "file_size": 106, "row_count": 2, "block_number": 1, "created_at": "2026-11-02T19:38:41.287018+00:00", "message": null, "content_type": "text/csv", "download_url": "https://api.sixtyfour.ai/storage/download/v1.eyJlIjoxNzkzNjQ5...", "download_expires_in_seconds": 900, "results": [ { "_row_id": "1", "email": "jane@northgatelabs.com", "phone": "+14155550123" }, { "_row_id": "2", "email": "sam@pacificviewstudios.com", "phone": "+12065550188" } ] }, { "filename": "4fe6a411-ecdc-4524-9724-be968488c503.csv", "file_size": 320, "row_count": 2, "block_number": 2, "created_at": "2026-11-02T19:38:48.624741+00:00", "message": null, "content_type": "text/csv", "download_url": "https://api.sixtyfour.ai/storage/download/v1.eyJlIjoxNzkzNjQ5...", "download_expires_in_seconds": 900, "results": [ { "_row_id": "1", "email": "jane@northgatelabs.com", "phone": "+14155550123", "email_state": "deliverable", "Status": "Succeeded" }, { "_row_id": "2", "email": "sam@pacificviewstudios.com", "phone": "+12065550188", "email_state": "undeliverable", "Status": "Succeeded" } ] } ] ``` | Field | Description | | - | - | | `filename` | `{run_id}.csv` for the final file, `{run_id}_{block_number}.csv` for one block's output | | `block_number` | Sequence number of the block that wrote the file | | `row_count` | Rows in the file | | `file_size` | Size in bytes, or `null` | | `content_type` | `text/csv`, `application/json`, or `application/x-ndjson` | | `download_url` | The file's download link, or `null` when no link could be created | | `download_expires_in_seconds` | Seconds until `download_url` expires: `900` | | `results` | The file's rows inline when it has at most 200 rows and 512 KB, otherwise `null`. CSV values are the stored cell text and are never spreadsheet-escaped | | `message` | A note about the file, such as that it is still being saved, or `null` | Every result file starts with a `_row_id` column that numbers its rows from `1`. Pass `format=csv` (default), `format=json`, or `format=ndjson`. JSON links exist only for runs started with `save_json=true`, and NDJSON links only for runs started with `result_formats=ndjson`. In workflow, search and Atlas CSV exports, list and object cells contain JSON; booleans are `true` or `false`. Parse list and object cells with `json.loads()` or `JSON.parse()`. Python's `ast.literal_eval()` does not accept JSON `null`, `true` or `false`. Previously saved files keep their original text. Each `download_url` needs an `x-api-key` header from the run's organization. Reuse it until it expires instead of requesting new links. While a step's result files are still being saved, its CSV entry carries a `message` saying so, and its `download_url` and inline `results` already serve its rows. JSON and NDJSON link requests return `503` with a `Retry-After` header until the files are saved; see [workflow result download retries](/api-reference/errors#workflow-result-download-retries). A run with a large result file can also finish later than usual while that file saves. Do not rerun the workflow. Link requests and file downloads each have an organization-wide budget; see [Rate limits](/api-reference/rate-limits#workflows). For rate-limit responses and retry timing, see [workflow result download retries](/api-reference/errors#workflow-result-download-retries). ### Spreadsheet-safe CSV CSV downloads are escaped for spreadsheets by default. A cell that starts with `=`, `+`, `-`, `@`, a tab, or a line break gets a leading `'`, so a spreadsheet shows it as text instead of running it as a formula. For example, `+14155550123` downloads as `'+14155550123` and `@northgate` as `'@northgate`. Negative numbers such as `-12.5` are unchanged. Every field is quoted, and the response carries an `X-Spreadsheet-Safe: 1` header. To get the file exactly as stored, add `?spreadsheet_safe=false` to the `download_url`. JSON and NDJSON files are never escaped. When code reads the results, request NDJSON with `result_formats=ndjson`. ```bash theme={null} curl "DOWNLOAD_URL?spreadsheet_safe=false" \ -H "x-api-key: YOUR_API_KEY" \ -o results.csv ``` *** ## Failed rows A row fails at a step when that step still cannot process it after its retries. A failed row: * skips every later step and is not charged for those steps * stays in the output, with `Failed` in the columns the failing step would have filled * carries a `_block_failed` column in result files, set to the `block_name` of the step it failed at, such as `lead_enrichment` * is still sent to an outgoing webhook step The final result file ends with a `Status` column set to `Succeeded` or `Failed` for each row. If your data already has a column named `Status`, it is kept as is and the added column is named `Workflow status`. If that name is also taken, the added column uses the first available numbered name, starting with `Workflow status 2`. A run with some failed rows finishes as `completed`. It finishes as `failed` only when no row made it through the workflow. List workflow runs returns the counts in each run's `metrics`: | Field | Description | | - | - | | `failed_row_count` | Total rows that failed, across all steps | | `failed_rows` | Per step: `block_id`, `block_name`, `sequence_number`, and `count` of rows that failed at that step | | `failed_steps` | Block IDs of steps that every row had already failed before reaching | These fields are omitted when no row failed. *** ## Example usage ```python Python theme={null} import requests import time API_KEY = "YOUR_API_KEY" WORKFLOW_ID = "YOUR_WORKFLOW_ID" BASE_URL = "https://api.sixtyfour.ai" headers = {"x-api-key": API_KEY, "Content-Type": "application/json"} run_response = requests.post( f"{BASE_URL}/workflows/run?workflow_id={WORKFLOW_ID}", headers=headers, json={ "webhook_payload": [ {"company_name": "Acme Corp", "website": "acme.com"}, {"company_name": "TechStart", "website": "techstart.io"} ] } ) run_response.raise_for_status() job_id = run_response.json()["job_id"] IN_PROGRESS = {"queued", "running", "finalizing"} while True: status_response = requests.get( f"{BASE_URL}/workflows/runs/{job_id}/live_status", headers=headers ) if status_response.status_code in (429, 503): time.sleep(int(status_response.headers.get("Retry-After", 10))) continue status_response.raise_for_status() status = status_response.json() if status["overall_status"] not in IN_PROGRESS: break time.sleep(10) if status["overall_status"] == "completed": links = requests.get( f"{BASE_URL}/workflows/runs/{job_id}/results/download-links", headers=headers ).json() for r in links: print(f"{r['filename']}: {r['download_url']}") ``` ```javascript JavaScript theme={null} const API_KEY = "YOUR_API_KEY"; const WORKFLOW_ID = "YOUR_WORKFLOW_ID"; const BASE_URL = "https://api.sixtyfour.ai"; const headers = { "x-api-key": API_KEY, "Content-Type": "application/json" }; const runResponse = await fetch( `${BASE_URL}/workflows/run?workflow_id=${WORKFLOW_ID}`, { method: "POST", headers, body: JSON.stringify({ webhook_payload: [ { company_name: "Acme Corp", website: "acme.com" }, { company_name: "TechStart", website: "techstart.io" } ] }) } ); if (!runResponse.ok) { const err = await runResponse.json(); throw new Error(`Failed to start workflow: ${JSON.stringify(err)}`); } const { job_id } = await runResponse.json(); const IN_PROGRESS = ["queued", "running", "finalizing"]; let status; while (true) { const statusResponse = await fetch( `${BASE_URL}/workflows/runs/${job_id}/live_status`, { headers } ); if (statusResponse.status === 429 || statusResponse.status === 503) { const retryAfter = Number(statusResponse.headers.get("Retry-After") ?? 10); await new Promise((r) => setTimeout(r, retryAfter * 1000)); continue; } if (!statusResponse.ok) throw new Error(`Live status failed: ${statusResponse.status}`); status = await statusResponse.json(); if (!IN_PROGRESS.includes(status.overall_status)) { break; } await new Promise((r) => setTimeout(r, 10000)); } if (status.overall_status === "completed") { const linksResponse = await fetch( `${BASE_URL}/workflows/runs/${job_id}/results/download-links`, { headers } ); const links = await linksResponse.json(); links.forEach((r) => console.log(`${r.filename}: ${r.download_url}`)); } ``` ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs. ## Errors For error responses (400, 403, 404, 409, etc.), see [Handling Errors](/api-reference/errors). Cancellation retry behavior is covered under [503 Service Unavailable](/api-reference/errors#503-service-unavailable). *** # Workflow Management Source: https://docs.sixtyfour.ai/api-reference/workflows/workflows-management List, create, update, and delete workflows via the Workflows API. Create, update, and delete workflows from code, or start from a ready-made template. > [card] [API Reference](/api-reference/workflow/list-workflows) > > See the full request/response schema and parameters in the API Reference. ## Workflow definition A workflow definition is a directed graph of `blocks` connected by `edges`: * `blocks`: each block has a `block_name` (see [Workflow Blocks](/api-reference/workflows/workflow-blocks)), an optional `block_id`, and a `specs` object. * `edges`: each edge has a `from_block_id` and a `to_block_id`. This shape is used in both Create Workflow and Update Workflow requests. A `block_id` that is missing or is not a UUID is replaced with a new UUID on every save, and the edges are rewritten to match. ### Limits | Value | Limit | | - | - | | `workflow_name` | 200 characters | | `workflow_description` | 2,000 characters | | Blocks | 100 per workflow | | Whole definition | 1 MiB of compact JSON, not counting a `search` block's `exclude_public_ids` | | `block_id`, `from_block_id`, `to_block_id` | 200 characters | | `exclude_public_ids` in a `search` block | 50,000 IDs, each up to 200 characters. Deprecated. | | Any other string or map key in the definition | 100,000 characters | | Nesting depth | 64 levels | A save over a limit stores nothing. For the error responses, see [Handling Errors](/api-reference/errors). *** ## List workflows Retrieve the workflows in your organization. ```http theme={null} GET https://api.sixtyfour.ai/workflows ``` Returns the workflows without their block definitions. *** ## Get workflow Retrieve a specific workflow including its complete block graph. ```http theme={null} GET https://api.sixtyfour.ai/workflows/{workflow_id} ``` | Name | Required | Description | | - | - | - | | prefer\_draft | No | Default `true`: returns the saved draft, including edits that failed validation, when one exists. `false` returns the definition runs use. | Runs use the last valid saved definition; a draft that failed validation never runs. Any valid update takes effect from the next run. The workflow's `status` and `updated_at` change as runs progress, so track each run with its [live status](/api-reference/workflows/workflows-execution#get-live-status). `edit_revision` changes only when the name, description, or graph changes, including edits made in the web app. *** ## Create workflow Create a new workflow with a block graph definition. ```http theme={null} POST https://api.sixtyfour.ai/workflows/create_workflow ``` Required body fields: `workflow_name`, `workflow_description`, and `workflow_definition`. Pass an optional `id` containing a valid UUID to use a custom workflow ID; otherwise one is auto-generated. ### Example request ```json theme={null} { "workflow_name": "Contact Validation", "workflow_description": "Verifies contact email addresses", "workflow_definition": { "blocks": [ { "block_name": "webhook", "block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b01", "specs": { "dataframe_type": "LEAD", "input_schema": { "email": { "type": "string" } } } }, { "block_name": "verify_email", "block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b02", "specs": { "email_column": "email" } } ], "edges": [ { "from_block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b01", "to_block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b02" } ] } } ``` Definitions are validated before saving. With an API key, an invalid definition is rejected and nothing is stored; see [Validation errors](#validation-errors). With an OAuth access token, it is saved as a draft, which never runs. The response is the saved workflow. Its `status` is `created`, or `draft` when an OAuth request saved a draft. *** ## Update workflow Update an existing workflow's name, description, or block definition. ```http theme={null} POST https://api.sixtyfour.ai/workflows/update_workflow ``` Pass `workflow_id` as a query parameter. Include only the fields to change. Content updates to an existing workflow require `expected_revision`, copied from the latest `GET /workflows/{workflow_id}` response. Lock updates do not require it. | Field | Description | | - | - | | `expected_revision` | `edit_revision` from the latest workflow read. Required when changing name, description, or graph. | | `workflow_name` | New name. | | `workflow_description` | New description. | | `workflow_definition` | New block graph, in the same shape as Create Workflow. | | `locked` | `true` locks the graph: a request that changes `workflow_definition` is refused until it is unlocked. Name and description can still change. | > [!NOTE] > > If the workflow does not exist and no `expected_revision` is sent, the request creates it, and `workflow_definition` is required. An invalid definition is handled as in [Create workflow](#create-workflow), and the last valid saved definition is kept. *** ## Validation errors Definitions are validated before saving. For the checks and the `validation_errors` response, see [Handling Errors](/api-reference/errors#400-bad-request). One check runs only when a run starts: a source block with no data yet, such as a search that has not run or a file that was never uploaded (`MISSING_DATA_SOURCE`), saves but rejects the run with the same `validation_errors` shape. *** ## Delete workflow Permanently delete a workflow. ```http theme={null} POST https://api.sixtyfour.ai/workflows/delete_workflow ``` Pass `workflow_id` as a query parameter. The response is the deleted workflow. Locked workflows are deleted too. > [!WARNING] > > Deleting a workflow also deletes its [schedules](/api-reference/workflows/workflow-schedules) and its run history. Its runs, their live status, and their results can no longer be read. [Download the results](/api-reference/workflows/workflows-execution#download-results) you need before deleting. Use a new ID for a new workflow. *** ## Templates Templates are ready-made workflow definitions from the Sixtyfour catalog. Each template carries its full `blocks` and `edges`, so you can pass them to Create Workflow as your `workflow_definition`. | Endpoint | Auth | Returns | | - | - | - | | `GET /templates` | None | The catalog, newest first | | `GET /templates/available` | API key | The catalog plus your organization's own templates | | `GET /templates/{template_key}` | None | One catalog template, by `slug` or `id` | Template endpoints are rate limited; see [Rate limits](/api-reference/rate-limits#workflows). Both list endpoints accept these optional query parameters: | Query parameter | Description | | - | - | | `category` | Exact category match, such as `Intelligence`. | | `featured` | `true` to return only featured templates. | | `tags` | Repeatable. Returns templates with any of these tags. | | `q` | Free-text search. | Each template includes `key`, `scope` (`global` for the catalog, `org` for your organization's templates), `id`, `slug`, `name`, `description`, `category`, `tags`, `featured`, `difficulty`, `estimated_time`, `use_case`, `preview_blocks`, `blocks`, `edges`, `author_name`, `created_at`, and `updated_at`. ```bash theme={null} curl "https://api.sixtyfour.ai/templates?featured=true" ``` ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing) for credit costs. ## Errors For error responses (400, 403, 404, etc.), see [Handling Errors](/api-reference/errors). # Workflows Overview Source: https://docs.sixtyfour.ai/api-reference/workflows/workflows-overview Create, manage, and execute automated data enrichment pipelines with the Workflows API. Run many records through a saved multi-step pipeline, such as verify emails, enrich companies, and post the results to your webhook, with one API call. ## Core Concepts The Workflows API models pipelines as directed acyclic graphs (DAGs): | Term | Description | | - | - | | Workflow | A reusable pipeline definition containing blocks and edges | | Block | A processing step, such as webhook input, enrichment, filtering, or delivery. See [Workflow Blocks](/api-reference/workflows/workflow-blocks) | | Edge | A connection between blocks that defines data flow | | Run | One execution of a workflow, identified by its `job_id` | | Template | A ready-made workflow definition from the Sixtyfour catalog. See [Templates](/api-reference/workflows/workflows-management#templates) | | Schedule | A cron rule that starts runs automatically. See [Workflow Schedules](/api-reference/workflows/workflow-schedules) | ## Execution Flow ```mermaid theme={null} flowchart LR A[List Workflows] --> B[Run Workflow] B --> C[Poll Live Status] C --> D[Download Results] ``` 1. List workflows to find the `workflow_id` you want to run. 2. Run the workflow with input data and receive a `job_id`. 3. Poll live status every 10 seconds or slower until `overall_status` is no longer `queued`, `running`, or `finalizing`. 4. Download the result files (CSV, plus JSON or NDJSON when requested). > [!NOTE] > > For workflows that start with a `webhook` block, see [Incoming Webhooks](/api-reference/webhooks/incoming). For recurring runs, see [Workflow Schedules](/api-reference/workflows/workflow-schedules). ## Quick start > [!TIP] > > To build workflows without code, use the [Workflow Builder](https://app.sixtyfour.ai/workflows) in the Sixtyfour web app. ```bash cURL theme={null} # 1. List available workflows curl -X GET "https://api.sixtyfour.ai/workflows" \ -H "x-api-key: YOUR_API_KEY" # 2. Run a workflow with data curl -X POST "https://api.sixtyfour.ai/workflows/run?workflow_id=WORKFLOW_ID" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_payload": [ {"company_name": "Acme Corp", "website": "acme.com"} ] }' # 3. Check status (use job_id from step 2) curl -X GET "https://api.sixtyfour.ai/workflows/runs/JOB_ID/live_status" \ -H "x-api-key: YOUR_API_KEY" # 4. Download results curl -X GET "https://api.sixtyfour.ai/workflows/runs/JOB_ID/results/download-links" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import requests import time API_KEY = "YOUR_API_KEY" BASE_URL = "https://api.sixtyfour.ai" headers = { "x-api-key": API_KEY, "Content-Type": "application/json" } # 1. Run workflow run_response = requests.post( f"{BASE_URL}/workflows/run?workflow_id=WORKFLOW_ID", headers=headers, json={ "webhook_payload": [ {"company_name": "Acme Corp", "website": "acme.com"}, {"company_name": "TechStart", "website": "techstart.io"} ] } ) run_response.raise_for_status() job_id = run_response.json()["job_id"] print(f"Workflow started: {job_id}") # 2. Poll for completion IN_PROGRESS = {"queued", "running", "finalizing"} while True: status_response = requests.get( f"{BASE_URL}/workflows/runs/{job_id}/live_status", headers=headers ) if status_response.status_code in (429, 503): time.sleep(int(status_response.headers.get("Retry-After", 10))) continue status_response.raise_for_status() status = status_response.json() print(f"Status: {status['overall_status']} ({status['overall_progress_percentage']:.1f}%)") if status["overall_status"] not in IN_PROGRESS: break time.sleep(10) # 3. Download results if status["overall_status"] == "completed": results_response = requests.get( f"{BASE_URL}/workflows/runs/{job_id}/results/download-links", headers=headers ) for result in results_response.json(): print(f"Download: {result['filename']} ({result['row_count']} rows)") ``` ```javascript JavaScript theme={null} const API_KEY = 'YOUR_API_KEY'; const BASE_URL = 'https://api.sixtyfour.ai'; const headers = { 'x-api-key': API_KEY, 'Content-Type': 'application/json' }; // 1. Run workflow const runResponse = await fetch( `${BASE_URL}/workflows/run?workflow_id=WORKFLOW_ID`, { method: 'POST', headers, body: JSON.stringify({ webhook_payload: [ { company_name: 'Acme Corp', website: 'acme.com' }, { company_name: 'TechStart', website: 'techstart.io' } ] }) } ); if (!runResponse.ok) throw new Error(`Failed to start: ${await runResponse.text()}`); const { job_id } = await runResponse.json(); console.log(`Workflow started: ${job_id}`); // 2. Poll for completion const IN_PROGRESS = ['queued', 'running', 'finalizing']; let status; while (true) { const statusResponse = await fetch( `${BASE_URL}/workflows/runs/${job_id}/live_status`, { headers } ); if (statusResponse.status === 429 || statusResponse.status === 503) { const retryAfter = Number(statusResponse.headers.get('Retry-After') ?? 10); await new Promise((r) => setTimeout(r, retryAfter * 1000)); continue; } if (!statusResponse.ok) throw new Error(`Live status failed: ${statusResponse.status}`); status = await statusResponse.json(); console.log(`Status: ${status.overall_status} (${status.overall_progress_percentage}%)`); if (!IN_PROGRESS.includes(status.overall_status)) break; await new Promise((r) => setTimeout(r, 10000)); } // 3. Download results if (status.overall_status === 'completed') { const resultsResponse = await fetch( `${BASE_URL}/workflows/runs/${job_id}/results/download-links`, { headers } ); const results = await resultsResponse.json(); results.forEach((r) => console.log(`Download: ${r.filename} (${r.row_count} rows)`)); } ``` ## Uploading an input file Workflows that start with a `read_csv` block read their rows from a file you upload first. > [!TIP] > > Enriching a list of people or companies? Use [Bulk Intelligence](/api-reference/endpoint/bulk-intelligence) — one call with file and config, no workflow or handle needed. ```http theme={null} POST https://api.sixtyfour.ai/storage/csv/upload ``` Send the file as `multipart/form-data` with a single `file` part. Accepts `.csv`, `.json`, `.jsonl`, and `.ndjson` files up to 45 MB. Column names cannot start with `_`, except `_row_id`. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/storage/csv/upload" \ -H "x-api-key: YOUR_API_KEY" \ -F "file=@leads.csv" ``` The response returns a `handle_id`, valid only for your organization: ```json theme={null} { "handle_id": "9b1f9d2e-4c3a-4f61-9a7e-2d8b5c1e0f43" } ``` Pass `handle_id` as `specs_override.resource_handle_id` when you call `/workflows/run`. Runs of a `read_csv` workflow require it: ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/workflows/run?workflow_id=WORKFLOW_ID" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "specs_override": { "resource_handle_id": "9b1f9d2e-4c3a-4f61-9a7e-2d8b5c1e0f43" } }' ``` ## NDJSON input & output Upload NDJSON or JSONL input the same way as CSV (see [Uploading an input file](#uploading-an-input-file)). For NDJSON output, add `result_formats=ndjson` when running. CSV is always saved; NDJSON is added alongside it. NDJSON keeps numbers, booleans, and nested objects typed, where CSV writes every value as text (lists and objects as JSON). ```bash cURL theme={null} # Request NDJSON output curl -X POST "https://api.sixtyfour.ai/workflows/run?workflow_id=WORKFLOW_ID&result_formats=ndjson" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "specs_override": { "resource_handle_id": "9b1f9d2e-4c3a-4f61-9a7e-2d8b5c1e0f43" } }' # Get the NDJSON download links: a JSON array of result files, each with a download_url curl -X GET "https://api.sixtyfour.ai/workflows/runs/JOB_ID/results/download-links?format=ndjson" \ -H "x-api-key: YOUR_API_KEY" # Fetch the file from the returned download_url curl -X GET "DOWNLOAD_URL" \ -H "x-api-key: YOUR_API_KEY" \ -o results.ndjson ``` # Atlas API Source: https://docs.sixtyfour.ai/atlas/api Start and continue Atlas investigations, and read their findings, attribution graph, files, agent activity, reports and exports. Start and continue [Atlas](/atlas/overview) investigations, and read everything they produce. [Atlas API or People Intelligence](/atlas/integrations#atlas-api-or-people-intelligence) compares this API with the `xhigh` tier. ## Endpoint All paths are under `https://api.sixtyfour.ai/v1/atlas`. | Method | Path | Purpose | | - | - | - | | `GET` | `/access` | Check whether your org has Atlas, and the engines it can run with their prices. | | `POST` | `/investigations` | Start an investigation. | | `POST` | `/investigations/{investigation_id}/runs` | Continue an investigation with another run. | | `GET` | `/investigations` | List investigations. | | `GET` | `/investigations/{investigation_id}` | Get one investigation. | | `GET` | `/investigations/{investigation_id}/findings` | List findings (graph nodes), paginated. | | `GET` | `/investigations/{investigation_id}/links` | List links between findings (graph edges), paginated. | | `GET` | `/investigations/{investigation_id}/graph` | The whole attribution graph. | | `GET` | `/investigations/{investigation_id}/files` | List workspace files. | | `GET` | `/investigations/{investigation_id}/file-entries` | List workspace files, paginated. | | `GET` | `/investigations/{investigation_id}/files/{path}` | Read one workspace file or attachment. | | `GET` | `/investigations/{investigation_id}/events` | The agents' sessions and events, paginated. | | `GET` | `/investigations/{investigation_id}/events/stream` | Follow the agents' turn summaries live over Server-Sent Events. | | `GET` | `/investigations/{investigation_id}/events/{event_id}/payload` | Read one event's full arguments, result and reasoning. Requires activity access. | | `POST` | `/investigations/{investigation_id}/report` | Generate the intelligence report. | | `GET` | `/investigations/{investigation_id}/report` | Report generation status. | | `GET` | `/investigations/{investigation_id}/report.pdf` | Download the report PDF. | | `GET` | `/investigations/{investigation_id}/export.zip` | Export the whole workspace as a ZIP. | Authenticate with your `x-api-key` header, as on every other endpoint. ## Checking access Atlas is enabled on request; [talk to sales](https://cal.com/team/sixtyfour/discovery) to turn it on. `GET /access` works for every organization. It returns whether yours has Atlas and the engines your API key's team can run, default first, with each engine's price. ```bash cURL theme={null} curl "https://api.sixtyfour.ai/v1/atlas/access" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import requests response = requests.get( "https://api.sixtyfour.ai/v1/atlas/access", headers={"x-api-key": "YOUR_API_KEY"} ) response.raise_for_status() access = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/v1/atlas/access", { headers: { "x-api-key": "YOUR_API_KEY" } }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const access = await response.json(); ``` ```json theme={null} { "has_atlas": true, "engines": [ {"engine": "xhigh", "label": "XHigh", "credits": 250.0}, {"engine": "scout", "label": "Scout", "credits": 50.0} ] } ``` | Field | Type | Description | | - | - | - | | `has_atlas` | boolean | Whether your org can use Atlas. | | `engines` | array | The engines your org can run, the default first. Empty when `has_atlas` is `false`. | | `engines[].engine` | string | The value to pass as `engine` when starting or continuing a run. | | `engines[].label` | string | Display name for the engine. | | `engines[].credits` | number or null | Price of one unit of research on this engine, in credits. `null` when the price is unavailable; retry the request. | ## Starting an investigation `POST /investigations` returns 202 with the run's `task_id` and the new investigation: | Field | Type | Default | Description | | - | - | - | - | | `lead_info` | object | required | Who to investigate, for example `{"name": "Jane Doe", "company": "Acme"}`. | | `name` | string | derived from `lead_info` | Title for the investigation. | | `engine` | string | `xhigh` | `xhigh` for the deepest research, or `scout` for faster, lower-cost research (needs Scout access). [`GET /access`](#checking-access) lists the engines your org can run. | | `research_plan` | string | — | Plain-language guidance on what to look into. | | `struct` | object | — | Optional target fields, in the same format as [People Intelligence](/api-reference/endpoint/people-intelligence#using-the-struct-field). | | `relentless` | boolean | `true` | Research until the budget runs out. Set `false` with a `struct` to let the run stop once every field is answered. | | `max_credits` | integer | one unit of the engine price | How much research to buy, from one to ten units of the engine price. | | `max_minutes` | integer (10–1440) | no limit | Working-time limit. When it is reached the run writes up what it found and completes. See [Credits & Pricing Guide](/guides/credits-and-pricing#atlas-investigations) for what it is charged. | | `dispatch_id` | string (8–128 chars) | — | Idempotency key, unique within your organization and shared with `POST /people-intelligence-async`. Resending a key returns the run it first started and starts nothing, even for another investigation. | | `private` | boolean | `false` | Make the investigation visible only to you, not your team. Needs private runs enabled for your organization and allowed by your team. With an API key, the run is private to the key's creator, and that person's keys on the team can read it. | Other fields are ignored without an error. [Atlas API or People Intelligence](/atlas/integrations#atlas-api-or-people-intelligence) lists the fields only People Intelligence takes. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/v1/atlas/investigations" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lead_info": {"name": "Jane Doe", "company": "Acme Corp"}, "research_plan": "Professional background, current role and public board seats", "max_minutes": 60 }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/v1/atlas/investigations", headers={"x-api-key": "YOUR_API_KEY"}, json={ "lead_info": {"name": "Jane Doe", "company": "Acme Corp"}, "research_plan": "Professional background, current role and public board seats", "max_minutes": 60 } ) response.raise_for_status() run = response.json() investigation_id = run["investigation"]["id"] ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/v1/atlas/investigations", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ lead_info: { name: "Jane Doe", company: "Acme Corp" }, research_plan: "Professional background, current role and public board seats", max_minutes: 60 }) }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const run = await response.json(); const investigationId = run.investigation.id; ``` ```json theme={null} { "task_id": "enrich_3f8b2c1e-9d4a-4f6b-8c2e-1a7b5d9e0f34", "status": "RUNNING", "investigation": { "id": "29beab70-42e4-5d19-b3b1-9a28279ca19f", "workspace_url": "https://app.sixtyfour.ai/atlas/29beab70-42e4-5d19-b3b1-9a28279ca19f/graph" } } ``` Poll the run with [`GET /job-status/{task_id}`](/api-reference/async-jobs), or open `workspace_url` to watch it in the Atlas section of the Sixtyfour app. A completed run's `result` has the fields of an [Atlas job result](/atlas/integrations#response), including `structured_data` and `field_confidence` when you send a `struct`. A run the agents decline also reports `completed`; see [Declined runs](/atlas/integrations#declined-runs). ## Continuing an investigation `POST /investigations/{investigation_id}/runs` starts another run on the same workspace and graph. It takes the same body as starting one, without `lead_info`, `name` and `private`, and returns the same 202 response. Continuing needs editor access to the investigation. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID/runs" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"research_plan": "Follow up on the earlier role at the previous employer"}' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID/runs", headers={"x-api-key": "YOUR_API_KEY"}, json={"research_plan": "Follow up on the earlier role at the previous employer"} ) response.raise_for_status() run = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID/runs", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ research_plan: "Follow up on the earlier role at the previous employer" }) }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const run = await response.json(); ``` Continuing while a run is going starts a second run, billed separately. To refuse a second run, continue through [`POST /people-intelligence-async`](/atlas/integrations#request-parameters) with `investigation_id` and `exclusive_run: true`. The investigation's `status` covers all of its runs. After you continue a finished investigation it still reads `completed` until the new run's agents start, so follow the new run with `GET /job-status/{task_id}`. ## Listing and reading investigations `GET /investigations` returns `investigations`, newest first, the page `limit` and a `next_cursor`. An API key lists and opens its team's investigations and the ones shared with that team. | Query parameter | Default | Description | | - | - | - | | `limit` | `50` | Page size, 1–200. | | `after` | — | `next_cursor` from the previous page. | | `shared` | `all` | `owned` for your team's investigations, `shared` for ones other teams shared with yours, or `all`. | | `status` | — | Only investigations in this [state](#investigation-status): `created`, `running`, `stalled`, `completed` or `failed`. A filtered page can come back short or empty, so follow `next_cursor` until it is `null`. | | `created_after`, `created_before` | — | ISO 8601 timestamps. | Each investigation, also returned by `GET /investigations/{investigation_id}`, looks like this: ```json theme={null} { "id": "29beab70-42e4-5d19-b3b1-9a28279ca19f", "name": "Jane Doe — Acme Corp", "status": "completed", "visibility": "private", "lead_info": {"name": "Jane Doe", "company": "Acme Corp"}, "created_at": "2026-01-15T10:00:00Z", "updated_at": "2026-01-15T10:42:00Z", "completed_at": "2026-01-15T10:42:00Z", "cost_cents": 25000, "cost_credits": 2500, "shared": null, "workspace_url": "https://app.sixtyfour.ai/atlas/29beab70-42e4-5d19-b3b1-9a28279ca19f/graph", "private": false } ``` `shared` is `null` when your team owns the investigation. Otherwise it names the team that shared it and your `tier` on it: a `viewer` can read and export, an `editor` can also run and edit. | Field | Description | | - | - | | `status` | The investigation's [status](#investigation-status), across all of its runs. | | `visibility` | `public` while a public link to the investigation is on (set in the web app), otherwise `private`. | | `private` | `true` for a private run, visible only to the person who started it. Separate from `visibility`. | | `completed_at` | When the investigation reached `completed` or `failed`. `null` while a run is going. | | `cost_credits` | Total spend across every run, in credits. `null` if a run's cost has not been recorded yet. `cost_cents` is deprecated. | ### Investigation status | Status | Meaning | | - | - | | `created` | No run's agents have started yet. | | `running` | A run's agents are working. | | `stalled` | An agent has made no progress for an hour. It returns to `running` when the agent resumes; after 24 hours without progress, its run fails. | | `completed` | A run has completed. The investigation stays `completed` between runs, and when a later run fails or is stopped. | | `failed` | The latest run failed or was stopped, and no run has completed. | ## Reading the graph and workspace | Endpoint | Returns | | - | - | | `GET .../graph` | The whole attribution graph: `nodes` (findings with their source URLs) and `edges` (confidence-weighted links between them). | | `GET .../findings`, `GET .../links` | The same nodes and edges one page at a time: `items` and `next_cursor`. `limit` is 1–50 (default 50); pass `next_cursor` as `after`. | | `GET .../files` | `files`: every workspace file's `path`, `content_type`, `size_bytes`, `updated_at`, `version`, and `attachment` metadata for saved images and documents. | | `GET .../file-entries` | The same file metadata one page at a time: `items` and `next_cursor`. `limit` is 1–50 (default 50). | | `GET .../files/{path}` | One file's content, or an attachment's bytes with its content type. | | `GET .../events` | The agents' `sessions` (type, task, status, turn count, cost) and their turn summaries as `events`, oldest first, up to 500 a page. `next_cursor` is `null` on the last page, which can be empty. | ### Findings, links and events Findings are the graph's `nodes` and the items of `GET .../findings`. Empty fields are left out. | Field | Description | | - | - | | `id` | The finding's id in this graph. The inputs the investigation started from are `input.`. | | `type` | `ground_truth` for those inputs, `discovered` for everything the agents found. | | `value` | The finding: a URL, handle, name or other value. | | `kind` | A free-text category, such as `account` or `email`. | | `status` | `active`, or `retired` once it is taken out of the graph. | | `ruled_out` | `true` when someone retired it as wrong in the web app. | | `field` | The `struct` field it answers, if any. | | `source_url`, `source_urls` | The pages it was read on. `source_url` is the first. | | `source_quote` | The text it was read from. | | `confidence` | A confidence recorded on the finding itself, when there is one. | | `evidence_receipts` | The evidence behind it: each receipt's `confidence`, `basis` and source `records`. | | `attachments` | Workspace paths of images saved as evidence for it. | | `depth`, `unexplored` | How far the agents explored this source, and what is left to check there. | Links are the graph's `edges` and the items of `GET .../links`: | Field | Description | | - | - | | `from`, `to` | The finding the link starts from, and the finding reached through it. | | `confidence` | How confident the link is, 0–100. | | `label` | Why the link holds, and where it came from. | Events are the agents' turn summaries: | Field | Description | | - | - | | `id`, `seq` | The event's id and its order in the investigation. | | `session_id`, `agent_id` | The agent session and the agent that wrote it. | | `turn` | The agent's turn when it was written. | | `kind` | `turn_summary`. | | `summary` | What the agent did, shortened for display. `detail.summary` holds the full text. | | `created_at` | When it was written. | With activity access, the event list also includes tool calls and their `payload`: `args`, `result` and `thinking`. A large payload has a non-null `payload.storage_ref` instead of inline values; `GET /investigations/{investigation_id}/events/{event_id}/payload` returns it in full. The request takes the event's `id`. `storage_ref` is opaque. ### Images and attachments Job results and image fields link attachments by permanent `https://api.sixtyfour.ai/v1/media/...` URLs that need no API key and can be embedded. For organizations with customer-managed encryption, media URLs need the `x-api-key` header, so fetch them from your server. ## Following a run live `GET .../events/stream` sends the agents' turn summaries over [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) as they are written, in order. Omit `after` to replay from the start. | Frame | Sent | Data | | - | - | - | | `sessions` | When the stream opens, and whenever an agent session starts, finishes or changes | Every agent session | | `event` | For each turn summary | The event. Its `id:` is the cursor to resume from | | `status` | When the stream opens, and whenever the investigation's status changes | `status` and `completed_at` | | `end` | When the investigation is `completed` or `failed` and every event has been sent | `status`. The stream closes | | `access_lost` | When you can no longer see the investigation | None. The stream closes | The `sessions` frame before `end` carries the run's cost, or `null` if the cost has not been recorded yet; read `cost_credits` from `GET /investigations/{investigation_id}` later. Comment lines keep the connection open; ignore them. `status` and `end` follow the investigation's [status](#investigation-status). After you continue a finished investigation, open the stream once it reads `running`. A stream closes after about five minutes. Reconnect with the last event's `id` as `after` or as a `Last-Event-ID` header; `after` also accepts `next_cursor` from `GET .../events`. Every new stream sends `status` and `sessions` again, so treat them as the current state, not as changes. Browser `EventSource` can't send the `x-api-key` header, so read the stream with an HTTP client, as below. Close it on `end` or `access_lost`: the stream sets `retry: 1000`, and each reconnect counts toward your [rate limit](/api-reference/rate-limits#atlas). ```bash cURL theme={null} curl -N "https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID/events/stream" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import json import time import requests STREAM = "https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID/events/stream" HEADERS = {"x-api-key": "YOUR_API_KEY"} after, done = None, False while not done: params = {"after": after} if after else None with requests.get(STREAM, headers=HEADERS, params=params, stream=True, timeout=(10, 60)) as response: if response.status_code in (429, 503): time.sleep(int(response.headers.get("Retry-After", "10"))) continue response.raise_for_status() frame = {} for raw in response.iter_lines(): line = raw.decode() if line: field, _, value = line.partition(": ") frame[field] = value continue if frame.get("event") == "event": after = frame["id"] print(json.loads(frame["data"])["summary"]) elif frame.get("event") in ("end", "access_lost"): done = True break frame = {} ``` ```javascript JavaScript theme={null} const STREAM = "https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID/events/stream"; const headers = { "x-api-key": "YOUR_API_KEY" }; const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); let after = null; let done = false; while (!done) { const response = await fetch(after ? `${STREAM}?after=${encodeURIComponent(after)}` : STREAM, { headers }); if (response.status === 429 || response.status === 503) { await sleep(Number(response.headers.get("Retry-After") ?? 10)); continue; } if (!response.ok) throw new Error(`Request failed: ${response.status}`); const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); let buffer = ""; while (!done) { const { value, done: closed } = await reader.read(); if (closed) break; buffer += value; let boundary; while (!done && (boundary = buffer.indexOf("\n\n")) !== -1) { const frame = {}; for (const line of buffer.slice(0, boundary).split("\n")) { const split = line.indexOf(": "); if (split > 0) frame[line.slice(0, split)] = line.slice(split + 2); } buffer = buffer.slice(boundary + 2); if (frame.event === "event") { after = frame.id; console.log(JSON.parse(frame.data).summary); } else if (frame.event === "end" || frame.event === "access_lost") { done = true; } } } await reader.cancel(); } ``` ## Reports Atlas starts a report when a run completes. To get one at any other time, or to be sure it covers the newest findings: 1. `POST .../report` starts a PDF report from the current workspace and returns 202 with a report job. A request while a report is building joins that build; if the build predates newer findings, the job has `follow_up: true` and a new build starts after it. If the latest report is current, the job is `completed` with `cached: true`. Send `{"retry_failed": true}` to retry a failed build. An empty workspace returns 400. 2. Poll `GET .../report` until `status` is `completed` and `follow_up` is `false`. It returns the latest job: `job_id`, `status`, `follow_up`, `cached`, `requested_at`, `completed_at`, `error` and `byte_size`. 3. Download `GET .../report.pdf`. | `status` | Meaning | | - | - | | `pending`, `running` | The build is in progress. | | `completed` | The build finished, and `report.pdf` serves its PDF. | | `failed` | The build failed, and `error` says so. With `follow_up: true`, a build from the newest findings follows; otherwise `POST .../report` with `retry_failed: true` tries again. | | `superseded` | A revision in the web app replaced the build before it finished. | If `follow_up` stays `true` for 10 minutes after a build ends, send `POST .../report` again. `GET .../report.pdf` answers: | Response | When | | - | - | | `200` | The latest report. `X-Report-As-Of` is when its findings were read; `X-Report-Current: false` means the investigation has changed since. | | `202` | No report can be served yet: the first one is being written, or the last one cited something since removed or rewritten and its replacement is being written. The body is the job; retry after `Retry-After` seconds. | | `404` | No report exists. See [Handling Errors](/api-reference/errors). | When no run is going, you can also poll `report.pdf` after `POST .../report` until it returns `200` with `X-Report-Current: true`. An older report that only lacks newer findings returns `200` with `X-Report-Current: false`. ```bash cURL theme={null} # Request the report curl -X POST "https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID/report" \ -H "x-api-key: YOUR_API_KEY" # Repeat every 15 s until status is "completed" and follow_up is false curl "https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID/report" \ -H "x-api-key: YOUR_API_KEY" # Download it curl -o report.pdf "https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID/report.pdf" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import time import requests BASE = "https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID" HEADERS = {"x-api-key": "YOUR_API_KEY"} def call(method, path): while True: response = requests.request(method, f"{BASE}/{path}", headers=HEADERS) if response.status_code not in (429, 503): response.raise_for_status() return response time.sleep(int(response.headers.get("Retry-After", "10"))) call("POST", "report") while True: job = call("GET", "report").json() if job["status"] == "completed" and not job["follow_up"]: pdf = call("GET", "report.pdf") if pdf.status_code == 200: break elif job["status"] not in ("pending", "running") and not job["follow_up"]: raise RuntimeError(f"Report {job['status']}: {job['error']}") time.sleep(15) with open("report.pdf", "wb") as file: file.write(pdf.content) ``` ```javascript JavaScript theme={null} const BASE = "https://api.sixtyfour.ai/v1/atlas/investigations/INVESTIGATION_ID"; const headers = { "x-api-key": "YOUR_API_KEY" }; const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); async function call(path, init = {}) { while (true) { const response = await fetch(`${BASE}/${path}`, { ...init, headers }); if (response.status !== 429 && response.status !== 503) { if (!response.ok) throw new Error(`Request failed: ${response.status}`); return response; } await sleep(Number(response.headers.get("Retry-After") ?? 10)); } } await call("report", { method: "POST" }); let pdf; while (true) { const job = await (await call("report")).json(); if (job.status === "completed" && !job.follow_up) { const response = await call("report.pdf"); if (response.status === 200) { pdf = await response.arrayBuffer(); break; } } else if (!["pending", "running"].includes(job.status) && !job.follow_up) { throw new Error(`Report ${job.status}: ${job.error}`); } await sleep(15); } ``` ## Export `GET .../export.zip` downloads everything visible in the app in one archive: | Path | Contents | | - | - | | `investigation.json` | The investigation record. | | `attribution-graph.json`, `attribution-graph.svg` | The attribution graph as data and as an image, when it has findings. | | `agents/sessions.json`, `agents/events.jsonl` | The agents' sessions and turn summaries. | | Workspace paths | Every workspace file. Each attachment comes with a `.json` metadata sidecar. | ## Web app only These have no API route and are done in the [web app](/atlas/web-app): stopping a run (**Stop latest run**), steering agents, attaching files to a run, sharing with another team or by public link, renaming and deleting investigations, folders, editing notes and findings, revising a report and reading earlier versions, automations, and saved instructions. ## Pricing See [Credits & Pricing Guide](/guides/credits-and-pricing#atlas-investigations) for more information. ## Errors For error responses (400, 402, 403, 404, 409, 429, etc.), see [Handling Errors](/api-reference/errors). # Atlas Integrations Source: https://docs.sixtyfour.ai/atlas/integrations Run Atlas through People Intelligence, Bulk Intelligence, workflows, webhooks and MCP, and get structured fields back from an investigation. Atlas runs through more than its own API. Each surface here starts the same investigation, which you can open in the [web app](/atlas/web-app) or read through the [Atlas API](/atlas/api). ## People Intelligence Send a `struct` to [People Intelligence](/api-reference/endpoint/people-intelligence) with `tier: "xhigh"` and the run returns your fields in `structured_data`, alongside a persistent investigation. `scout` with `graph: true` runs the same investigation tooling on Scout's lighter model. Atlas runs take longer than other tiers, so start them with the async endpoint: ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/people-intelligence-async" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lead_info": {"name": "Jane Doe", "company": "Acme Corp"}, "struct": { "current_role": "Their current job title and employer", "board_seats": {"description": "Public board seats they hold", "type": "list[str]"} }, "tier": "xhigh", "max_minutes": 60 }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/people-intelligence-async", headers={"x-api-key": "YOUR_API_KEY"}, json={ "lead_info": {"name": "Jane Doe", "company": "Acme Corp"}, "struct": { "current_role": "Their current job title and employer", "board_seats": {"description": "Public board seats they hold", "type": "list[str]"} }, "tier": "xhigh", "max_minutes": 60 } ) response.raise_for_status() run = response.json() ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/people-intelligence-async", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ lead_info: { name: "Jane Doe", company: "Acme Corp" }, struct: { current_role: "Their current job title and employer", board_seats: { description: "Public board seats they hold", type: "list[str]" } }, tier: "xhigh", max_minutes: 60 }) }); if (!response.ok) throw new Error(`Request failed: ${response.status}`); const run = await response.json(); ``` The start response and every `/job-status` response carry the investigation: ```json theme={null} { "task_id": "enrich_3f8b2c1e-9d4a-4f6b-8c2e-1a7b5d9e0f34", "status": "RUNNING", "investigation": { "id": "29beab70-42e4-5d19-b3b1-9a28279ca19f", "workspace_url": "https://app.sixtyfour.ai/atlas/29beab70-42e4-5d19-b3b1-9a28279ca19f/graph" } } ``` Open `workspace_url` to watch the run in the Atlas section of the Sixtyfour app, or read it through the [Atlas API](/atlas/api) while it is still going. ### Request parameters Atlas runs take the [People Intelligence parameters](/api-reference/endpoint/people-intelligence#request-parameters), plus: | Parameter | Type | Default | Description | | - | - | - | - | | `graph` | boolean | `false` | With `tier: "scout"`, run Scout as an Atlas run. Any other tier rejects it. | | `struct` | object | — | Optional on Atlas runs. Without it, the run researches open-endedly into the workspace. | | `investigation_id` | string | — | Continue an existing investigation. The run reuses its graph and workspace and builds on the earlier research. | | `investigation_name` | string | derived from `lead_info` | Title for a new investigation. Ignored when `investigation_id` is set. | | `max_credits` | integer | one unit of the tier price | How much research to buy, from one to ten units of the tier price. | | `max_minutes` | integer (10–1440) | no limit | Working-time limit. When it is reached the run writes up what it found and completes. See [Credits & Pricing Guide](/guides/credits-and-pricing#atlas-investigations) for what it is charged. | | `relentless` | boolean | `false` | Research until the budget runs out. By default a run with `struct` may stop once every field is answered. | | `include_workspace` | boolean | `true` | Include the investigation's workspace files with the result. | | `exclusive_run` | boolean | `false` | Async only. With `investigation_id`, refuse to start a second run while one is running. See [Handling Errors](/api-reference/errors) for the response. | | `private` | boolean | `false` | Make the run visible only to you (with an API key, to the key's creator), not your team. Needs private runs enabled for your organization and allowed by your team, the async endpoint, and no `investigation_id`. | ### Atlas API or People Intelligence Both start the same investigation, and both take `lead_info`, `struct`, `research_plan`, `max_credits`, `max_minutes`, `dispatch_id` and `private`. Side by side: | | [Atlas API](/atlas/api#starting-an-investigation) | People Intelligence with `tier: "xhigh"` | | - | - | - | | Start | `POST /v1/atlas/investigations` | `POST /people-intelligence-async` | | Continue | `POST /v1/atlas/investigations/{investigation_id}/runs` | The same endpoint with `investigation_id` | | Engine | `engine`: `xhigh` or `scout` | `tier: "xhigh"`, or `tier: "scout"` with `graph: true` | | Title | `name` | `investigation_name` | | `relentless` default | `true`: research until the budget runs out | `false`: a run with `struct` may stop once every field is answered | | `webhook_url` | Not taken | Taken | | `field_confidence` | Returned automatically with a `struct`; the option is ignored | Returned automatically with a `struct`; the option isn't needed | | `include_workspace` | Not taken: results always include `workspace_files` | Taken, default `true` | | `exclusive_run` | Not taken | Taken, with `investigation_id` | | Unknown fields | Ignored without a warning | Ignored, and listed in `unrecognized_fields` in the start response | ### Response Poll `GET /job-status/{task_id}`; [Async Jobs](/api-reference/async-jobs) covers polling and statuses. A completed Atlas job's `result` has the [People Intelligence fields](/api-reference/endpoint/people-intelligence#response), plus: | Field | Type | Description | | - | - | - | | `research_notes` | string | The agents' research notes. | | `graph` | string | A text rendering of the attribution graph. For structured nodes and edges, use [`GET .../graph`](/atlas/api#reading-the-graph-and-workspace). | | `workspace_files` | object | Workspace file path → content. Omitted when `include_workspace` is `false`. | ```json theme={null} { "id": "enrich_3f8b2c1e-9d4a-4f6b-8c2e-1a7b5d9e0f34", "status": "completed", "investigation": { "id": "29beab70-42e4-5d19-b3b1-9a28279ca19f", "workspace_url": "https://app.sixtyfour.ai/atlas/29beab70-42e4-5d19-b3b1-9a28279ca19f/graph" }, "result": { "structured_data": { "current_role": "VP Engineering, Acme Corp", "board_seats": ["Example Foundation"] }, "notes": "Confirmed the current role from the company site and a recent filing.", "references": { "https://acme.example/team": "Leadership page with current title" }, "research_notes": "# Jane Doe\n\nCurrent role confirmed from...", "graph": "# Attribution graph\nnodes:\n...", "workspace_files": { "notes/background.md": "# Background\n\n..." } } } ``` These three fields read from the investigation, so a later run on it changes what an earlier job returns. Attachment paths in them come back as permanent `/v1/media` URLs; see [Images and attachments](/atlas/api#images-and-attachments). Runs with a `struct` also return `field_confidence`: each answered field has its `confidence`, `justification`, `sources` and the graph `nodes` that support it. See [Confidence Scoring](/api-reference/endpoint/field-confidence). ### Declined runs When the agents decline to research a subject, the run ends before any research. The job still reports `completed`: `result.notes` starts with `Declined:` and gives the agents' reason, and `structured_data` repeats your `lead_info` instead of answering your `struct`. A new investigation reads `failed` and gets no report. Check `notes` before you store `structured_data`. ## Bulk Intelligence [`POST /bulk-intelligence/people`](/api-reference/endpoint/bulk-intelligence) accepts `tier: "xhigh"` and runs one investigation per row on the default budget. `graph`, `max_credits`, `max_minutes` and `investigation_id` aren't accepted in bulk config. To set a budget per lead, start each run from [People Intelligence](#people-intelligence) instead. In the web app, [Upload CSV](/atlas/web-app#start-one-investigation-per-row) starts one investigation per row with a budget you choose. ## Workflows Set the tier of an [Enrich People](/guides/building-workflows) block to **X-High** to run an investigation for each row. In a workflow definition, set `"tier": "xhigh"` in the `lead_enrichment` block's specs. Each row creates its own investigation on the default budget, and your return fields become columns. See [Credits & Pricing Guide](/guides/credits-and-pricing#atlas-investigations) for what a row costs. The block also adds: | Column | What it holds | | - | - | | `enrichment_notes` | A short research summary. | | `enrichment_references` | Source URL → what was found there. | | `enrichment_research_notes` | The agents' research notes. | | `enrichment_graph` | A text rendering of the attribution graph. | Place filters before the block so only the rows worth an investigation reach it. ## Webhooks Pass `webhook_url` to `POST /people-intelligence-async` to receive the [result](#response) when the run finishes. See [Outgoing Webhooks](/api-reference/webhooks/outgoing). To follow a run started with `POST /v1/atlas/investigations`, poll `GET /job-status/{task_id}` or [stream its events](/atlas/api#following-a-run-live). ## MCP The [Sixtyfour MCP server](/developer-tools/mcp-setup) lets an AI assistant start and read investigations. The Atlas tools appear only when your organization has Atlas enabled. 1. `sixtyfour_atlas_start` starts an XHigh investigation on the default budget. Pass the `subject` (any of `name`, `email`, `phone`, `username`, `ip_address`, `profile_url`, `company`, `domain`, or a free-text `query`), a `request_id` as the idempotency key, an optional `objective`, and `private: true` to make the investigation visible only to you. 2. `sixtyfour_atlas_status` polls the task until it finishes. 3. `sixtyfour_atlas_inspect` reads the investigation's summary, or a page of its findings, links or files. 4. `sixtyfour_atlas_file` reads a chunk of a text file that `sixtyfour_atlas_inspect` listed. `sixtyfour_atlas_list` lists your organization's investigations. The tools read text only, so open the investigation in the web app or use the [Atlas API](/atlas/api#reading-the-graph-and-workspace) for images and other attachments. # Atlas Overview Source: https://docs.sixtyfour.ai/atlas/overview Atlas runs deep investigations: research agents build a persistent workspace and attribution graph on a subject, then write a report you can share. Atlas is Sixtyfour's deep investigation product. Give it a subject and a team of research agents builds a workspace of notes and an attribution graph that links every finding to its source, then writes a report you can read, share and export. The investigation stays yours to continue later. Use it for diligence, background research and any subject where a single enrichment answer isn't enough. > [!NOTE] > > Atlas is available to Enterprise customers and is enabled on request. [Talk to sales](https://cal.com/team/sixtyfour/discovery) to turn it on for your organization. ## How an investigation works ```mermaid theme={null} flowchart LR Start["Start an investigation"] --> Run["Agents research"] Run --> Workspace[("Workspace and attribution graph")] Workspace --> Report["Report"] Workspace --> Read["Read, edit and export"] Workspace --> Next["Continue with another run"] Next --> Run ``` Each run researches until its credit budget or its time limit runs out, until its agents report that they are blocked, or, when it may stop early, until its answers are complete. When a run completes, Atlas writes the report. Findings and files stay on the investigation, so a later run builds on them instead of starting over. ## Concepts | Term | What it is | | - | - | | Investigation | The persistent record of research on one subject: its workspace, graph, reports and runs. | | Run | One pass of research into an investigation. An investigation can have many. | | Engine | The model and research team behind a run: XHigh or Scout. | | Budget | How much research a run may buy, in units of the engine's price, from one to ten. | | Workspace | The notes the agents write, and the images and documents they save. | | Finding | One fact in the graph, with the source URLs it came from. | | Attribution graph | Findings and the confidence-weighted links between them, which show how each one was reached. | | Report | A PDF intelligence report written from the workspace. | ## Engines | Engine | Value | What it does | Access | | - | - | - | - | | XHigh | `xhigh` | Frontier model and a team of research agents. The deepest research, and the default. | Included with Atlas | | Scout | `scout` | A single researcher on a lighter model, with the same investigation tooling. Faster and lower cost. | Enabled per organization | See [Credits & Pricing Guide](/guides/credits-and-pricing#atlas-investigations) for the price per unit and what one unit buys. ## Atlas and the `xhigh` tier The `xhigh` tier of People Intelligence is a structured version of Atlas. It runs the same agents and creates the same investigation, then returns the fields you asked for in `structured_data`. Either way you can open the investigation in the app, read its graph, get its report and continue it later. | | Atlas API or web app | People Intelligence with `tier: "xhigh"` | | - | - | - | | Best for | Open-ended research you read as a report and graph | A known set of fields for your own systems | | You receive | An investigation | `structured_data`, plus the investigation | | When a run stops | When the budget runs out | When every `struct` field is answered, or the budget runs out | [Atlas API or People Intelligence](/atlas/integrations#atlas-api-or-people-intelligence) compares the two parameter by parameter. ## Where to run Atlas | Surface | Use it to | | - | - | | [Web app](/atlas/web-app) | Start, follow and steer investigations, read reports, share them and schedule recurring research. | | [Atlas API](/atlas/api) | Start and continue investigations from your own code, and read everything they produce. | | [People Intelligence](/atlas/integrations#people-intelligence) | Get structured fields from an Atlas run, with an optional webhook. | | [Bulk Intelligence](/atlas/integrations#bulk-intelligence) | Run one investigation per row of a file. | | [Workflows](/atlas/integrations#workflows) | Run an investigation per row as a step in a pipeline. | | [MCP](/atlas/integrations#mcp) | Start and read investigations from an AI assistant. | ## Check your access [`GET /v1/atlas/access`](/atlas/api#checking-access) returns whether your organization has Atlas and the engines it can run. # Using Atlas in the Web App Source: https://docs.sixtyfour.ai/atlas/web-app Start an investigation, follow and steer its agents, read the report, share it, and schedule recurring research in the Sixtyfour web app. Run and manage investigations from [Atlas](https://app.sixtyfour.ai/atlas) in the Sixtyfour web app. Atlas appears in the sidebar once it is enabled for your organization. See [Atlas Overview](/atlas/overview) for access. ## Start an investigation 1. Open [Atlas](https://app.sixtyfour.ai/atlas). 2. Under **New investigation**, enter a name, company or topic to research. 3. Optionally, attach files and set the options below. 4. Press Enter to start. Shift+Enter adds a line. | Option | What it does | | - | - | | Engine | XHigh, or Scout when it is enabled for your organization. | | Budget | XHigh runs on 1×, 2× or 4× the price of one unit, or a custom amount up to 10×. Scout runs are always one unit. | | Time limit | No limit, 15 minutes, 30 minutes, 1 hour, or a custom limit from 10 to 1,440 minutes. When time is up, the agent writes up what it found. | | Private | Only you can see the run. Appears when your team allows private runs. | | Use my saved instructions | Applies your [saved instructions](#save-instructions). Appears once you have saved some, and is on by default. | You can attach up to 5 files: PNG, JPG, WebP or GIF images up to 20 MB each, and PDF, DOCX, XLSX or CSV documents up to 25 MB each. ### Start one investigation per row 1. Select **Upload CSV**. The file needs a header row, and each column becomes a field on the lead. 2. Check the rows in **CSV Preview** and add an optional research direction. 3. Select **Submit All**, which shows the total credits. Each row starts its own investigation, up to 1,500 rows. Attached files aren't available on a CSV upload. ## Follow a run An investigation opens on its graph while it runs, and on its report once it completes. | View | What it shows | | - | - | | Report | The intelligence report, with citations that open the finding behind them. | | Graph | Findings and the links between them. Search, or filter by nodes, types and confidence. Select a node to see how the agents reached it and the evidence for it. | | Sources | Every cited page. Open one, copy its link or download it as a PDF. | | Agents | The research team, and each agent's timeline. | | Files | The notes and attachments in the workspace. | The investigation's status shows how the run is going: | Status | Meaning | | - | - | | Not started yet | The investigation exists and no run has begun. | | Research in progress | Agents are working. | | Stalled — no recent agent activity | The agents have gone quiet. | | Research complete | The run finished. | | Run stopped before finishing | The run failed or was stopped. Its findings are kept. | The status covers the whole investigation, not one run. Once a run has completed, it reads **Research complete** between runs, and stays that way when a later run fails or is stopped. A blue dot in the sidebar marks a report you haven't opened yet. ## Steer a run You can change what a live agent works on without restarting it. This needs edit access to the investigation. 1. Open **Agents** and select the main agent or a subagent. 2. Type in **Send a message…** and send it. Messages can be up to 4,000 characters, and queued messages appear in the timeline until the agent reads them. To point the agents at a note, use **Send to running agent** on that file. To stop, use **Stop latest run**. Findings are kept, and you can add an agent later to carry on. ## Add more research **Add Agent** starts another run on the same workspace and graph, during a run or after it has finished. It needs edit access and is billed as a new run. 1. Select **Add Agent**. 2. Enter a research direction. Later agents keep it. 3. Optionally, attach files and set the engine, budget and time limit. 4. Select **Deploy**. To follow up on one finding, select its node in the graph and use **Dig deeper on this node…**, which also starts a billed run. ## Edit notes and findings Notes save as you type. In the file list you can create notes and folders, and rename, move, duplicate and delete them, with undo. **File history** shows what changed between versions. In the graph, **Add finding** records something you know, and you can edit a node, connect it to another node or remove a connection. If a finding is wrong, use **Retire node**. The report leaves it out and later research won't add it back. **Restore node** undoes it. ## Read and revise the report Atlas writes the report when a run completes. To read one sooner, use **Generate early report** while research is running, and **Update report** to bring it up to date. To change a finished report, use **Revise report**. Add feedback of up to 4,000 characters and up to 5 evidence files, then select **Rewrite report**. Later reports keep your feedback. The person who created the investigation can revise its report once research has stopped. For an investigation started by an API key or a workflow, anyone with edit access can. **Earlier reports** lists previous versions as PDFs. ## Share an investigation Everyone on the team that owns an investigation can see it, unless the run is private. To share it further, use **Share**. | With | How | What they can do | | - | - | - | | Another team | **Team access…**, as a team owner or org admin | **Can view**: open and export. **Can edit**: also add agents and edit, with their runs billed to their team. | | Anyone with the link | **Public link** | Read the report, graph, sources and files, and download the PDF. | A public link shows the investigation as it is now, so later research appears there. For organizations with customer-managed encryption, it shows a public copy frozen when it was published, without the PDF download; **Sync public copy…** republishes it from the workspace as it is now. Public pages hide the agents and file history, and search engines don't index them. A private run can't be shared. To group investigations, add them to folders. **Share folder…** shares a folder with another team, including anything added to it later. ## Export **Share** → **Export** downloads one investigation as a **Workspace ZIP**, a **Report PDF** or a **CSV**. To export many, open [bulk export](https://app.sixtyfour.ai/atlas/export), pick investigations by search or date, and choose a format. A bulk export covers up to 5,000 investigations. ZIP and PDF exports build in the background and stay in **Recent exports** for 24 hours. ## Schedule recurring research An investigation is a snapshot. An automation re-runs research on a schedule, so you catch what changes without starting over. Open [Automations](https://app.sixtyfour.ai/atlas/automations), create one or start from an example, and fill in: | Field | What it does | | - | - | | Name | What the automation is called. | | Schedule | Every hour, day, week or month, at the time and timezone you choose. | | What should it do? | **Re-deploy existing** adds a run to an investigation. **Create new** starts a new investigation each time, so each one has its own full report. | | Research direction | What each run should look for. Optional. | Automation runs use XHigh on a one-unit budget, billed as a normal run. A run fails if your balance is below the price of one run, or if the investigation already has a run going. An automation pauses if the person who created it loses access to its team. Automations can't target private investigations or ones shared from another team. ## Save instructions Saved instructions apply to every run, so you don't repeat them. Set them in [Settings → Atlas](https://app.sixtyfour.ai/settings/atlas), up to 5,000 characters at each level: | Level | Applies to | Who can edit | | - | - | - | | Everyone in your organization | Every run in the organization, including runs started with an API key | Org admins and owners | | Just you | Runs you start, unless you turn them off for a run | You | When both are set, a run follows both. # MCP Server Source: https://docs.sixtyfour.ai/developer-tools/mcp-setup Connect Sixtyfour intelligence tools and docs to your AI assistant using the Model Context Protocol. Connect your AI assistant to Sixtyfour search, enrichment, and docs through two hosted MCP servers: | Server | URL | Auth | | - | - | - | | `sixtyfour-docs` | `https://docs.sixtyfour.ai/mcp` | None required | | `sixtyfour-intelligence` | `https://mcp.sixtyfour.ai/mcp` | API key or OAuth | ## Setup The `sixtyfour-intelligence` server accepts OAuth sign-in with your Sixtyfour account or an API key from [Get an API Key](/get-api-key). Send the key in an `x-api-key` header. A client that can't set headers can pass it on the URL as `?api_key=YOUR_API_KEY`, but a key in a URL can end up in logs and shared config files, so prefer the header. Usage is billed to the organization of the signed-in user or key. ### Cursor [![Install in Cursor](https://img.shields.io/badge/Install_with_one_click-Cursor-000000?style=for-the-badge\&logo=cursor\&logoColor=white\&labelColor=555555)](cursor://anysphere.cursor-deeplink/mcp/install?name=sixtyfour-intelligence\&config=eyJ1cmwiOiJodHRwczovL21jcC5zaXh0eWZvdXIuYWkvbWNwIn0=) Or add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project) manually. Replace `YOUR_API_KEY` with your key. ```json theme={null} { "mcpServers": { "sixtyfour-docs": { "url": "https://docs.sixtyfour.ai/mcp" }, "sixtyfour-intelligence": { "url": "https://mcp.sixtyfour.ai/mcp", "headers": { "x-api-key": "YOUR_API_KEY" } } } } ``` To sign in with OAuth instead, omit `headers`. Cursor opens a browser sign-in on first use: ```json theme={null} { "mcpServers": { "sixtyfour-intelligence": { "url": "https://mcp.sixtyfour.ai/mcp" } } } ``` > [!NOTE] > > Cursor limits tool names to 60 characters and shortens longer names with a hash suffix. If Cursor warns that a tool name is too long, the tool still works. ### VS Code [![Install in VS Code](https://img.shields.io/badge/Install_with_one_click-VS_Code-0066B8?style=for-the-badge\&logo=visualstudiocode\&logoColor=white\&labelColor=555555)](vscode:mcp/install?%7B%22name%22%3A%22sixtyfour-intelligence%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.sixtyfour.ai%2Fmcp%22%7D) Or add to `.vscode/mcp.json` in your project root manually. Replace `YOUR_API_KEY` with your key. ```json theme={null} { "servers": { "sixtyfour-docs": { "type": "http", "url": "https://docs.sixtyfour.ai/mcp" }, "sixtyfour-intelligence": { "type": "http", "url": "https://mcp.sixtyfour.ai/mcp", "headers": { "x-api-key": "YOUR_API_KEY" } } } } ``` ### Windsurf Add to `~/.codeium/windsurf/mcp_config.json`. Replace `YOUR_API_KEY` with your key. ```json theme={null} { "mcpServers": { "sixtyfour-docs": { "url": "https://docs.sixtyfour.ai/mcp" }, "sixtyfour-intelligence": { "url": "https://mcp.sixtyfour.ai/mcp?api_key=YOUR_API_KEY" } } } ``` > [!TIP] > > After saving the configuration, reload your MCP servers or restart your IDE. ### Claude Code Run these commands in your terminal. Add `--scope user` to make a server available across all projects. **Docs MCP** ```bash theme={null} claude mcp add --transport http sixtyfour-docs "https://docs.sixtyfour.ai/mcp" ``` **Intelligence MCP — OAuth (recommended)** Claude Code opens a browser sign-in on first use: ```bash theme={null} claude mcp add --transport http sixtyfour-intelligence "https://mcp.sixtyfour.ai/mcp" ``` **Intelligence MCP — API key** ```bash theme={null} claude mcp add --transport http sixtyfour-intelligence "https://mcp.sixtyfour.ai/mcp" --header "x-api-key: YOUR_API_KEY" ``` ### Claude Desktop In Claude Desktop, add two custom connectors: | Name | Remote MCP server URL | | - | - | | `Sixtyfour Docs` | `https://docs.sixtyfour.ai/mcp` | | `Sixtyfour Intelligence` | `https://mcp.sixtyfour.ai/mcp` | Claude Desktop handles authentication for the intelligence server via OAuth on first connection. ### Codex Install via the [Sixtyfour Codex plugin](https://github.com/sixtyfour-ai/sixtyfour-codex-plugin), or add the servers to `~/.codex/config.toml` manually. **Plugin install** Register the marketplace, then install the plugin. Start a new Codex session after installing — plugins load at session start. **Codex CLI:** ```bash theme={null} codex plugin marketplace add sixtyfour-ai/sixtyfour-codex-plugin codex plugin add sixtyfour@sixtyfour ``` Check installed plugins with `codex plugin list`. To install this plugin in the ChatGPT desktop app instead of the CLI, see [ChatGPT](#chatgpt). > [!NOTE] > > Which ChatGPT plans and apps can install plugins is set by OpenAI; see [Plugins in ChatGPT](https://help.openai.com/en/articles/20001256-plugins-in-chatgpt). On Enterprise, Edu, and Business workspaces, an admin must allow installing marketplace plugins under Workspace Settings → Plugins before it appears for members. > [!TIP] > > On first use, authenticate the intelligence server. Run `codex mcp login sixtyfour-intelligence` when prompted. **Manual config** Add the servers to `~/.codex/config.toml`, then restart Codex and sign in when prompted. **Docs MCP:** ```toml theme={null} [mcp_servers.sixtyfour-docs] command = "npx" args = ["-y", "mcp-remote", "https://docs.sixtyfour.ai/mcp"] ``` **Intelligence MCP:** ```toml theme={null} [mcp_servers.sixtyfour-intelligence] command = "npx" args = ["-y", "mcp-remote", "https://mcp.sixtyfour.ai/mcp"] ``` **Reset a stuck sign-in session** The manual `config.toml` + `mcp-remote` method caches its OAuth session in `~/.mcp-auth`, separate from Codex's own session state. If sign-in stops working, clear the cached session for the intelligence server: ```bash theme={null} find ~/.mcp-auth -type f -name "e7e3f47f4a14aca4e636f38d171a9178_*" -delete ``` Restart Codex — it prompts you to sign in again via browser. This only affects the Sixtyfour session; other `mcp-remote`-based connections are untouched. > [!TIP] > > To avoid clearing the cache altogether, switch to the [plugin install](#codex) method, which supports `codex mcp login`/`codex mcp logout` for reconnecting. ### ChatGPT The Sixtyfour plugin is published in the [ChatGPT Plugins Directory](https://chatgpt.com/plugins/plugin_asdk_app_6a514c90130c8191bb9a04df335d5ac6) — the same plugin installed via the CLI marketplace command in [Codex](#codex). [![View in ChatGPT Plugins](https://img.shields.io/badge/View_in-ChatGPT_Plugins-74aa9c?style=for-the-badge\&logo=openai\&logoColor=white\&labelColor=555555)](https://chatgpt.com/plugins/plugin_asdk_app_6a514c90130c8191bb9a04df335d5ac6) 1. Open the listing above, or turn on Work mode in ChatGPT (web or desktop app) and search "Sixtyfour" in Plugins. 2. Select the plugin and install it. 3. Authenticate the intelligence server when prompted. See [Codex](#codex) for plan requirements and workspace admin settings that gate plugin installs. *** ## Documentation MCP reference The `sixtyfour-docs` server gives your AI assistant direct access to Sixtyfour API references, guides, and code examples. | Tool | Purpose | | - | - | | `search_sixtyfour_ai_documentation` | Semantic search across Sixtyfour docs | | `query_docs_filesystem_sixtyfour_ai_documentation` | Read specific pages and run `rg`/`cat`/`tree` against the docs filesystem | **Example prompts** * *"How do I use the company-intelligence endpoint?"* * *"What parameters does find-email accept?"* * *"Show me an example of using webhooks with Sixtyfour."* *** ## Intelligence MCP reference The `sixtyfour-intelligence` server exposes the tools below, grouped by category. Credit costs are in the [Credits & Pricing Guide](/guides/credits-and-pricing). Each search response returns `charge_credits`. > [!WARNING] > > Tools ending in `_start` create billable work. A start call that times out may still have created the job, so do not repeat it automatically. Retry `sixtyfour_atlas_start` only with the same `request_id` and inputs. **Account** | Tool | Purpose | | - | - | | `sixtyfour_check_balance` | Return the organization's current credit balance | **Company search** | Tool | Purpose | | - | - | | `sixtyfour_company_get_searchable_fields` | List all filterable field names for company search | | `sixtyfour_company_explore_field_values` | Get top values for a field (e.g. valid industry names) | | `sixtyfour_company_search` | Search companies with a [`simple_filters`](/api-reference/search/filter-search#simple-filters) query | | `sixtyfour_company_next_page` | Fetch the next page of company search results | | `sixtyfour_company_get_company_details` | Expand a result handle into full company data | **People search** | Tool | Purpose | | - | - | | `sixtyfour_people_get_searchable_fields` | List all filterable field names for people search | | `sixtyfour_people_explore_field_values` | Get top values for a field | | `sixtyfour_people_search` | Search people with a [`simple_filters`](/api-reference/search/filter-search#simple-filters) query | | `sixtyfour_people_next_page` | Fetch the next page of people search results | | `sixtyfour_people_get_person_details` | Expand a result handle into full person data | Result handles and cursors expire after 20 minutes; rerun the search to get new ones. Set `count_only` to `true` on `sixtyfour_people_search` to return `total_available` without profiles, result handles, or pagination data. When an exact count is unavailable, `total_available` is `null`: ```json theme={null} { "query": { "locationCountry": "US" }, "count_only": true } ``` **Similar people (lookalike)** | Tool | Purpose | | - | - | | `sixtyfour_find_similar_start` | Find people similar to 1–1000 seed profiles — returns a `task_id` and `search_id` | | `sixtyfour_find_similar_status` | Poll a `sixtyfour_find_similar_start` `task_id` until `completed` | | `sixtyfour_find_similar_results` | Page the ranked matches of a completed search | Pass seeds as `linkedin_urls` (1–1000 LinkedIn profile URLs) or `csv_text` (the full text of a CSV with a LinkedIn URL column, up to 1000 rows), not both. The seeds define the audience; use `guidance` only for hard requirements, such as `"only people in the US"`. Seed profiles never appear in the results. A search typically takes 1–3 minutes. Poll with `sixtyfour_find_similar_status`, not `sixtyfour_enrichment_job_status`; set `wait_seconds` (up to 25) to wait server-side between polls. Page the results by `search_id`, then by the returned `next_cursor`. **Async enrichment and contact lookup** | Tool | Purpose | | - | - | | `sixtyfour_company_intelligence_start` | Start a company intelligence job — returns a `task_id` | | `sixtyfour_people_intelligence_start` | Start a people intelligence job — returns a `task_id` | | `sixtyfour_find_email_start` | Find email addresses for 1–100 leads — returns a `task_id` | | `sixtyfour_find_phone_start` | Find phone numbers for 1–100 leads — returns a `task_id` | | `sixtyfour_reverse_email_start` | Resolve the person behind 1–100 email addresses — returns a `task_id` | | `sixtyfour_reverse_phone_start` | Resolve the person behind 1–100 phone numbers — returns a `task_id` | | `sixtyfour_enrichment_job_status` | Poll any `_start` tool's `task_id` until `completed`, `failed`, `cancelled`, `terminated`, or `timed_out` | | `sixtyfour_enrich_linkedin` | Enrich a single LinkedIn profile or company URL — returns the result directly | The completed response includes `result`, `charge_credits`, and `task_type`. The intelligence start tools accept the `micro`, `low`, `medium`, and `high` tiers; for `xhigh` research, use the Atlas tools below. **Bulk intelligence** | Tool | Purpose | | - | - | | `sixtyfour_people_bulk_intelligence_start` | Research the same `struct` for many people in one job — returns a `task_id` | | `sixtyfour_company_bulk_intelligence_start` | Research the same `struct` for many companies in one job — returns a `task_id` | Pass the entities as `rows` (a list of identifier objects, such as name, company, and LinkedIn URL) or `csv_text` (CSV text with a header row), not both. `struct` is required: each key becomes an output field, described in plain English or as `{"description": "...", "type": "str"}`. The start response includes `estimated_cost_credits`, a lower bound on the job's cost. See [Credits & Pricing](/guides/credits-and-pricing). Poll with `sixtyfour_enrichment_job_status`. The completed response includes signed download URLs for the result files under `results`, and `charge_credits`. Rows pass through the model's context, so use these tools for up to a few hundred rows. For larger files, use the [web app](https://app.sixtyfour.ai) or the [Bulk Intelligence API](/api-reference/endpoint/bulk-intelligence). **Age verification** | Tool | Purpose | | - | - | | `sixtyfour_verify_age_start` | Start an age verification job for a person — returns a `task_id` | | `sixtyfour_verify_age_status` | Poll a `sixtyfour_verify_age_start` `task_id` until `completed`, `failed`, `cancelled`, `terminated`, or `timed_out` | Poll with `sixtyfour_verify_age_status`, not `sixtyfour_enrichment_job_status`. The completed response includes `date_of_birth`, `age`, `evidence`, and `sources`; see [Age Verification](/api-reference/endpoint/age-verification). **Atlas investigations** These tools appear only when your organization has [Atlas](/atlas/overview) (`xhigh` People Intelligence) enabled. See [Atlas Integrations](/atlas/integrations#mcp) for how they fit together. | Tool | Purpose | | - | - | | `sixtyfour_atlas_start` | Start an open-ended Atlas investigation on one subject — returns a task to poll | | `sixtyfour_atlas_status` | Poll an Atlas task until it completes | | `sixtyfour_atlas_list` | List your organization's Atlas investigations; page with `next_cursor` | | `sixtyfour_atlas_inspect` | Read an investigation's summary or a page of its findings, links, or files | | `sixtyfour_atlas_file` | Read a bounded chunk of a file path returned by `sixtyfour_atlas_inspect` | Investigations also appear under **Atlas** in the [web app](https://app.sixtyfour.ai/atlas). **Webhook testing** | Tool | Purpose | | - | - | | `sixtyfour_webhook_test_events` | List the webhook events Sixtyfour sends, with an example payload for each. Pass `type` to return one example. | | `sixtyfour_send_test_webhook` | Send a sample event to your public HTTPS receiver, signed the way real deliveries are, and report whether it arrived | Both tools are free and run no enrichment, monitor or workflow. `sixtyfour_send_test_webhook` counts toward the `POST /webhooks/test-events` [rate limit](/api-reference/rate-limits#webhooks). Each call sends a new event, so read `delivered` in its response instead of retrying. IDs in a sample event do not belong to real jobs or monitors. **Example prompts** * *"Search for US SaaS companies with 100–5000 employees."* * *"Research Sixtyfour — return headquarters, headcount, funding stage, and key executives."* * *"Enrich this person: Saarth Shah, CEO & Co-Founder at Sixtyfour."* * *"Find the CTO and VP of Engineering at Sixtyfour."* * *"Find the work email for Saarth Shah at Sixtyfour."* * *"Find a phone number for Jane Doe, VP of Sales at Acme Corp."* * *"Who owns the email [jane@acme.com](mailto:jane@acme.com)?"* * *"Verify the age of Jane Doe, who works at Acme Corp."* * *"Find 50 more people like the ones in this CSV."* * *"Find the work email and GitHub profile for each of these 20 people."* * *"Open my latest Atlas investigation and summarize the findings."* * *"Send a test monitor notification to my webhook receiver."* * *"How many Sixtyfour credits do I have left?"* # Skills for AI Agents Source: https://docs.sixtyfour.ai/developer-tools/skills Agent Skills that teach AI coding assistants (Claude Code, Cursor, etc.) how to use Sixtyfour — people and company intelligence at scale. The [sixtyfour-ai/skills](https://github.com/sixtyfour-ai/skills) repo packages Sixtyfour as an Agent Skill: one directory of instructions and a polling script that teaches an AI coding assistant to call the Sixtyfour API correctly. The skill calls the REST API with your API key. To give an assistant tools instead of instructions, use the [MCP Server](/developer-tools/mcp-setup). ## Prerequisites 1. Have an AI coding assistant installed (Claude Code, Cursor, etc.). 2. Sign up at [app.sixtyfour.ai](https://app.sixtyfour.ai). 3. Generate a key by following [Get an API Key](/get-api-key). 4. Set `SIXTYFOUR_API_KEY` in your agent's shell. ```bash theme={null} export SIXTYFOUR_API_KEY=YOUR_API_KEY ``` ## Install You can install the Sixtyfour skills from the [Cursor Plugin Directory](https://cursor.directory/plugins/sixtyfour), or manually from the [sixtyfour-ai/skills](https://github.com/sixtyfour-ai/skills) repo. ### Cursor Plugin Directory Open the [Cursor Plugin Directory listing](https://cursor.directory/plugins/sixtyfour) and use the install button. ### Manual install ```bash Claude Code theme={null} npx skills add sixtyfour-ai/skills --skill "sixtyfour" ``` ```bash Cursor theme={null} mkdir -p ~/.cursor/plugins/local git clone https://github.com/sixtyfour-ai/skills.git \ ~/.cursor/plugins/local/sixtyfour ``` ```bash Manual theme={null} git clone https://github.com/sixtyfour-ai/skills.git cp -r skills/skills/sixtyfour /sixtyfour ``` After cloning into Cursor, open the Command Palette (`Cmd+Shift+P` on macOS, `Ctrl+Shift+P` on Windows/Linux) and run `Developer: Reload Window`. Then open **Cursor Settings → Plugins** and confirm Sixtyfour appears with a **Local** badge and 1 skill. ## Usage Once installed, ask your assistant in plain English: | Task | Example prompt | API the skill uses | | - | - | - | | Investigate a person | "Tell me everything about Jane Doe at Acme Corp — title, background, LinkedIn, career trajectory" | [People Intelligence](/api-reference/endpoint/people-intelligence) | | Research a company | "Research Acme Corp — headcount, funding, engineering leadership" | [Company Intelligence](/api-reference/endpoint/company-intelligence) | | Search at scale | "Find 500 VPs of Sales at SaaS startups in NYC" | [Search](/api-reference/search/search-overview) | | Enrich a file | "Enrich this spreadsheet of 500 leads with title and LinkedIn" | [Bulk Intelligence](/api-reference/endpoint/bulk-intelligence) | | Build a pipeline | "Search for AI startups, find their CTOs, get contact info, and export to CSV" | [Workflows](/guides/building-workflows) | | Due diligence | "Deep investigation on this person for compliance review" | People Intelligence at `medium` or `high` tier | | Age verification | "Verify this person's date of birth for KYC" | [Age Verification](/api-reference/endpoint/age-verification) | ## Troubleshooting ### API key not set Check that the variable is exported in the same shell your agent runs in: ```bash theme={null} [ -n "$SIXTYFOUR_API_KEY" ] && echo "set" || echo "not set" ``` If it's not set, export it as in [Prerequisites](#prerequisites). Setting the variable in a different terminal window does not carry over. ### Environment variables don't persist in Cursor or VS Code Cursor and VS Code do not inherit shell environment variables when launched from the macOS Dock or Spotlight. Fix it one of three ways: * Launch from a terminal that already has the variable set: `cursor .` * Add the export to `~/.zshrc` or `~/.bashrc` so it's always available * Add it to `.claude/settings.json` under `env` ### Locating `poll_job.py` The polling script ships inside the skill, at `scripts/poll_job.py` in the skill directory. Where that directory is depends on how you installed the skill: | Install | Path | | - | - | | `npx skills add` (Claude Code) | `.claude/skills/sixtyfour/scripts/poll_job.py` in your project, or `~/.claude/skills/sixtyfour/scripts/poll_job.py` when installed with `-g` | | Cursor clone | `~/.cursor/plugins/local/sixtyfour/skills/sixtyfour/scripts/poll_job.py` | | Manual | `/sixtyfour/scripts/poll_job.py` | It requires Python 3 and uses only the standard library. ### Deep search fails with "Concurrency limit reached" Each organization runs up to 5 deep searches at a time. Wait for one to reach a final status, then start the next. See [Concurrency limits](/api-reference/rate-limits#deep-search). ### 403 on high, xhigh, or scout tier requests The `high`, `xhigh`, and `scout` tiers are enabled per organization. See [403 Forbidden](/api-reference/errors#403-forbidden). ### Results are empty or incomplete * Use `tier: "medium"`. The default `low` tier misses people with little online presence or common names. * For people intelligence, pass as many identifiers as possible in `lead_info`: `full_name`, `company`, `linkedin_url`, and `email` all improve matching. ### Deep search appears stuck A deep search runs for up to 15 minutes, then ends `timed_out`. `poll_job.py` prints progress messages such as `Analyzing your query...` while it runs. Start a new search only after the old one reaches a final status. ### Output file is CSV, not JSON Deep search results download as CSV. Parse them with `csv.DictReader`, not `json.load`. Columns include `fullName`, `currentPosition`, `currentCompany`, `linkedinUrl`, `skills`, `locationName`, and others. ### Signed download URLs expire Download URLs expire 15 minutes after they are issued. Call `GET /search/download` again with the same `resource_handle_id` for a new one. # Get an API Key Source: https://docs.sixtyfour.ai/get-api-key Create, scope and revoke Sixtyfour API keys. An API key authenticates your requests to the Sixtyfour API and charges usage to one team. ## Create a key **1. Sign in** Sign in at [app.sixtyfour.ai](https://app.sixtyfour.ai). New accounts create an organization during onboarding. **2. Open API Keys** Go to **Settings → API Keys**. The page shows the keys of your active team; switch teams to see another team's keys. **3. Create the key** Click **Create key**, enter a name and set the expiration, then confirm. **4. Copy the key** The full key is shown once. Store it in a secret manager or environment variable. Sixtyfour cannot show it again. Send the key in the `x-api-key` header: ```bash theme={null} curl -X POST "https://api.sixtyfour.ai/find-email" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"lead": {"name": "Jane Smith", "company": "Northgate Labs"}}' ``` Keys start with `api_`. See [Authentication](/api-reference/authentication) for OAuth tokens and how the API picks a credential. ## Team scope Every key belongs to one team. Requests made with the key draw on your organization's credits and count toward the team's monthly spend limit, if it has one. The workflows, runs and logs they create belong to that team. | Role | Can create keys for | Can revoke keys in | | - | - | - | | Team member | Teams they belong to | None | | Team owner | Teams they belong to | Teams they own | | Organization admin or owner | Any team in the organization | Any team in the organization | See [Roles and Permissions](/guides/roles-and-permissions) for how organization and team roles work. ## Expiration | Option | Behavior | | - | - | | **Never** (default) | The key works until it is revoked. | | Duration | The key expires after a number of days, weeks, months (30 days) or years. The maximum is 730 days. | The keys table shows each key's expiry date, or **Never**. Expired keys stop authenticating. Create a replacement before the expiry date and rotate your services to it. Sixtyfour emails your organization's members once before a key that is in use expires: | Key lifetime | Warning sent | | - | - | | 90 days or more | 14 days before expiry | | 30 to 89 days | 7 days before expiry | | 8 to 29 days | 1 day before expiry | | 7 days or less | No warning | ## Revoke a key Revoke a key from **Settings → API Keys**. A revoked key stops working within five minutes; see [Revocation and expiry](/api-reference/authentication#revocation-and-expiry). Schedules that run with the revoked key are paused; resume them with a working key. ## Keep keys safe * Keep keys server-side. Never ship them in browser or mobile code. * Use one key per service so you can revoke one without affecting the others. * Prefer [OAuth clients](/guides/oauth-clients) for machine-to-machine integrations that need short-lived tokens. # Age Verification Source: https://docs.sixtyfour.ai/guides/age-verification Check people's ages from public evidence and decide what to do with each result. This guide runs a batch of age verifications and sorts each person into pass, fail or manual review based on the evidence. For the request and response fields, see the [Age Verification reference](/api-reference/endpoint/age-verification). ## Try it in the dashboard Open [Age Verification in the API Playground](https://app.sixtyfour.ai/playground/verify-age), enter what you know about the person under Subject, and run it. The playground runs the same job as the API and shows the date of birth, age, evidence and sources. ## Verify a batch and gate on confidence Each verification takes a few minutes, so start them all, then poll. This script checks whether each person is at least 18 and sends anything uncertain to a human. ```python theme={null} import time import requests BASE_URL = "https://api.sixtyfour.ai" headers = {"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"} MIN_AGE = 18 MIN_CONFIDENCE = 80 # 0-100; raise it for legal or compliance decisions people = [ {"name": "Jane Doe", "company": "Acme Corp", "linkedin": "https://www.linkedin.com/in/janedoe"}, {"name": "John Smith", "location": "Austin, TX", "email": "john@example.com"}, ] task_ids = [] for person in people: start = requests.post(f"{BASE_URL}/verify-age", headers=headers, json={"subject": person}) start.raise_for_status() task_ids.append(start.json()["task_id"]) pending = dict(zip(task_ids, people)) while pending: for task_id, person in list(pending.items()): response = requests.get(f"{BASE_URL}/verify-age/{task_id}", headers=headers) if response.status_code in (429, 503): time.sleep(float(response.headers.get("Retry-After", 15))) continue response.raise_for_status() result = response.json() if result["status"] == "running": continue del pending[task_id] if result["status"] != "completed": print(person["name"], "review", f"job {result['status']}") continue age = result["age"] if age["value"] is None or age["confidence"] < MIN_CONFIDENCE: decision = "review" elif age["value"] >= MIN_AGE: decision = "pass" else: decision = "fail" print(person["name"], decision, age["value"], result["sources"]) if pending: time.sleep(15) ``` ## Reading the result * Gate on `age.confidence` or `date_of_birth.confidence`, not on whether a value is present. Both are 0–100. * A `null` date component means no source established it. A known birth year with an unknown day and month still pins the age to within a year. * `age.value` can be an estimate when no date of birth was found. Its confidence reflects that. * Keep `evidence` and `sources` with any decision you act on, so a reviewer can check them. ## Getting better matches * Send every identifier you have in `subject`: a company, location, email or LinkedIn URL along with the name. * Reuse People Intelligence payloads as they are: `lead_info` is accepted in place of `subject`. * From an AI client, use the `sixtyfour_verify_age_start` and `sixtyfour_verify_age_status` tools in the [MCP server](/developer-tools/mcp-setup). # Automated Workflows Source: https://docs.sixtyfour.ai/guides/automated-workflows Run a saved workflow on a recurring schedule from the Sixtyfour Workflow Editor. A schedule runs a saved workflow on a fixed cadence, with no manual trigger. Use it to keep lists fresh, send recurring digests, or re-enrich CRM data. To create and manage schedules from code, see the [Workflow Schedules API](/api-reference/workflows/workflow-schedules). | Use case | Example cadence | | - | - | | Keep a prospect list current | Every day at 9:00 AM — search for new accounts that match your ICP and append them to a notebook | | Refresh CRM enrichment | Every week on Monday — import HubSpot contacts, re-enrich, and send the changes to your system | | Watch a market | Every hour — scrape a careers page and post new roles to Slack | | Run heavy jobs overnight | Custom schedule `0 2 * * *` | Pair recurring runs with the Deduplicate with Notebook block so each run saves only new rows. See [Building Workflows](/guides/building-workflows). ## Create a schedule The workflow must be saved and runnable. Until every block is configured, the Schedules option is disabled. 1. Open the workflow in the [Workflow Editor](https://app.sixtyfour.ai/workflows). 2. Open the More options menu (⋮) next to **Run** and select **Schedules**. You can also click **Run** and then **Schedule**, or open schedules from the workflow's row in the Workflows list. ![Workflow Editor More options menu with Schedules highlighted](https://mintcdn.com/sixtyfour/K61A1YkNVyqC6uNE/images/schedules-editor-menu.png?fit=max\&auto=format\&n=K61A1YkNVyqC6uNE\&q=85\&s=ca66c0469e776174a5675b2b0ccd18b6) 3. Pick a cadence under **Repeats**, then set its options. 4. Optionally expand **Advanced options** to set a name and timezone. 5. Click **Create schedule**. The editor shows when the first run starts. | Repeats | Options | | - | - | | Every 15 minutes | None | | Every 30 minutes | None | | Every hour | At minute: `:00`, `:15`, `:30`, or `:45` | | Every day | At: time of day | | Every week | On days: one or more weekdays. At: time of day | | Every month | On day: 1st–28th. At: time of day | | Custom schedule | Cron expression | | Advanced option | Default | | - | - | | Name (optional) | The cadence, such as "Every day at 9:00 AM" | | Timezone | Your browser's timezone. The cadence is evaluated in this timezone. | Custom schedules take a standard 5-field cron expression, `minute hour day-of-month month day-of-week`: | Expression | Runs | | - | - | | `0 9 * * 1-5` | Weekdays at 9:00 AM | | `30 8 1 * *` | 8:30 AM on the 1st of every month | | `5 */4 * * *` | Every 4 hours at minute 5 | An invalid cron expression or timezone is rejected when you save. ## Manage schedules A workflow can have several schedules. Each one in the list shows its name, cadence, and next run, or `Paused` when it is paused. Per-schedule actions: * Pause or resume the schedule without deleting it. * Edit the cadence, name, or timezone, then click **Save changes**. * Delete the schedule. The workflow is not affected. Schedules belong to the workflow's team, and their runs count toward the team's monthly credit limit. A schedule keeps running after its creator leaves, unless an admin pauses it during the removal. Sixtyfour pauses a schedule that runs with an API key when that key is revoked or expires, and every schedule when its team is archived; the list shows why. After you resume a schedule, its runs are attributed to you. Scheduled runs use credits the same way manual runs do. See [Credits & Pricing](/guides/credits-and-pricing). > [!WARNING] > > A scheduled run of a workflow that starts with an Incoming Webhooks block processes the block's saved sample input every time, and each run uses credits. Without a sample input, every scheduled run fails. To process new rows from your systems, trigger the workflow through the [Workflows API](/guides/batch-processing#send-rows-in-the-request) instead. ## Tips * Spread schedules that share a cadence across `:00`, `:15`, `:30`, and `:45`. * Keep the cadence longer than a run takes. Each scheduled time starts a new run even while the previous one is still going. See [Overlapping and missed runs](/api-reference/workflows/workflow-schedules#overlapping-and-missed-runs). * Set the timezone deliberately. A 9:00 AM schedule created from a browser in another timezone runs at 9:00 AM there. # Batch Processing Source: https://docs.sixtyfour.ai/guides/batch-processing Run a list of records through a Sixtyfour workflow from the Workflow Editor or the Workflows API, then download the results. Batch processing sends every row of a list through one workflow and returns the enriched results as files. Build the workflow once in the [Workflow Editor](/guides/building-workflows) or [as JSON](/guides/programmatic-workflows), then feed it rows. | Input | How | Best for | | - | - | - | | A file you have | Start the workflow with an Upload Data block and attach a CSV, JSON, or NDJSON file in the editor | One-off lists | | Rows from your systems | Start the workflow with an Incoming Webhooks block and send rows in the API request | Recurring jobs and integrations | | A search | Start the workflow with a Find Data block such as Search | Building a new list | To enrich a list with People or Company Intelligence and nothing else, [Bulk Intelligence](/api-reference/endpoint/bulk-intelligence) needs no workflow. ## Run a batch in the editor 1. Open the workflow and click **Run**. The run dialog shows the estimated cost. 2. Click **Runs** in the editor header to follow progress block by block. 3. Download the results from the run, or deliver them with a Send Results block such as Append to Notebook or Outgoing Webhooks. ## Send rows in the request Runs started through the API follow three steps: ```mermaid theme={null} flowchart LR Run["POST /workflows/run"] --> Poll["GET .../live_status"] --> Download["GET .../results/download-links"] ``` `POST /workflows/run?workflow_id=WORKFLOW_ID` starts the run. For a workflow whose source block is Incoming Webhooks, pass the rows in `webhook_payload`: a list of objects, one per row, or a single object for one row. For an Upload Data (`read_csv`) source, [upload the input file](/api-reference/workflows/workflows-overview#uploading-an-input-file) and pass its `handle_id` as `specs_override.resource_handle_id` in the request body. Workflows that start with other source blocks run with no body. The run is asynchronous. Poll `live_status` every 10 seconds or slower while `overall_status` is `queued`, `running`, or `finalizing`, then request download links. The list has one file per block; the final results are the file named `{job_id}.csv`. Each `download_url` expires after 15 minutes and needs your `x-api-key` header. ```bash cURL theme={null} # Start the run curl -X POST "https://api.sixtyfour.ai/workflows/run?workflow_id=WORKFLOW_ID" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_payload": [ {"name": "Sarah Chen", "company": "Pacific View Studios"}, {"name": "James Park", "company": "Northgate Labs"} ] }' # Poll with the job_id from the response curl "https://api.sixtyfour.ai/workflows/runs/JOB_ID/live_status" \ -H "x-api-key: YOUR_API_KEY" # Get download links once overall_status is completed curl "https://api.sixtyfour.ai/workflows/runs/JOB_ID/results/download-links" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import time, requests BASE = "https://api.sixtyfour.ai" HEADERS = {"x-api-key": "YOUR_API_KEY"} run = requests.post( f"{BASE}/workflows/run", params={"workflow_id": "WORKFLOW_ID"}, headers=HEADERS, json={"webhook_payload": [ {"name": "Sarah Chen", "company": "Pacific View Studios"}, {"name": "James Park", "company": "Northgate Labs"}, ]}, ) run.raise_for_status() job_id = run.json()["job_id"] IN_PROGRESS = {"queued", "running", "finalizing"} while True: resp = requests.get(f"{BASE}/workflows/runs/{job_id}/live_status", headers=HEADERS) if resp.status_code in (429, 503): time.sleep(int(resp.headers.get("Retry-After", 10))) continue resp.raise_for_status() status = resp.json() if status["overall_status"] not in IN_PROGRESS: break time.sleep(10) if status["overall_status"] != "completed": raise RuntimeError(f"Run ended as {status['overall_status']}") links = requests.get(f"{BASE}/workflows/runs/{job_id}/results/download-links", headers=HEADERS).json() final = next(item for item in links if item["filename"] == f"{job_id}.csv") csv_file = requests.get(final["download_url"], headers=HEADERS) csv_file.raise_for_status() with open("results.csv", "wb") as f: f.write(csv_file.content) ``` ```javascript JavaScript theme={null} import fs from "node:fs/promises"; const BASE = "https://api.sixtyfour.ai"; const headers = { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }; const run = await fetch(`${BASE}/workflows/run?workflow_id=WORKFLOW_ID`, { method: "POST", headers, body: JSON.stringify({ webhook_payload: [ { name: "Sarah Chen", company: "Pacific View Studios" }, { name: "James Park", company: "Northgate Labs" }, ], }), }); const { job_id } = await run.json(); const IN_PROGRESS = ["queued", "running", "finalizing"]; let status; while (true) { const resp = await fetch(`${BASE}/workflows/runs/${job_id}/live_status`, { headers }); if (resp.status === 429 || resp.status === 503) { const retryAfter = Number(resp.headers.get("Retry-After") ?? 10); await new Promise((r) => setTimeout(r, retryAfter * 1000)); continue; } if (!resp.ok) throw new Error(`Live status failed: ${resp.status}`); status = await resp.json(); if (!IN_PROGRESS.includes(status.overall_status)) break; await new Promise((r) => setTimeout(r, 10000)); } if (status.overall_status !== "completed") throw new Error(`Run ended as ${status.overall_status}`); const links = await (await fetch(`${BASE}/workflows/runs/${job_id}/results/download-links`, { headers })).json(); const final = links.find((item) => item.filename === `${job_id}.csv`); const file = await fetch(final.download_url, { headers }); if (!file.ok) throw new Error(`Download failed: ${file.status}`); await fs.writeFile("results.csv", Buffer.from(await file.arrayBuffer())); ``` ```json Run response theme={null} { "status": "RUNNING", "workflow_id": "0b7a3c2e-5d4f-4e8a-9c61-2f3e4d5a6b7c", "job_id": "5e2d8f1a-3c4b-4a6d-8e9f-1a2b3c4d5e6f" } ``` ```json Live status (abridged) theme={null} { "run_id": "5e2d8f1a-3c4b-4a6d-8e9f-1a2b3c4d5e6f", "workflow_id": "0b7a3c2e-5d4f-4e8a-9c61-2f3e4d5a6b7c", "overall_status": "running", "current_block": "lead_enrichment", "total_blocks": 3, "completed_blocks": 1, "overall_progress_percentage": 48.0, "last_updated": "2026-01-15T10:31:12Z", "blocks": [] } ``` ```json Download links (abridged) theme={null} [ { "filename": "5e2d8f1a-3c4b-4a6d-8e9f-1a2b3c4d5e6f_1.csv", "row_count": 2, "block_number": 1, "content_type": "text/csv", "download_url": "https://api.sixtyfour.ai/storage/download/...", "download_expires_in_seconds": 900 }, { "filename": "5e2d8f1a-3c4b-4a6d-8e9f-1a2b3c4d5e6f.csv", "row_count": 2, "block_number": 3, "content_type": "text/csv", "download_url": "https://api.sixtyfour.ai/storage/download/...", "download_expires_in_seconds": 900 } ] ``` > [!NOTE] > > CSV downloads are spreadsheet-safe by default: formula-like cells, such as ones that start with `=`, `+`, or `@`, get a leading `'`, so `+14155550123` becomes `'+14155550123`. Add `?spreadsheet_safe=false` to the `download_url` for the file exactly as stored, or run with `result_formats=ndjson` when code reads the results. See [Spreadsheet-safe CSV](/api-reference/workflows/workflows-execution#spreadsheet-safe-csv). Retrying `POST /workflows/run` with the same `Idempotency-Key` header returns the existing run instead of starting a new one. Cancellation, failed-row details, JSON and NDJSON result formats, and every response field are documented in [Workflow Execution](/api-reference/workflows/workflows-execution). For credit costs, see [Credits & Pricing](/guides/credits-and-pricing). For error responses, see [Handling Errors](/api-reference/errors). ## Tips * Filter before enrichment. Fewer rows means faster runs and fewer credits. * Use Deduplicate with Notebook on recurring batches so each run saves only new rows. * Define only the return fields you need in enrichment blocks. * Add an Outgoing Webhooks block to receive results when the run completes instead of polling. # Building Workflows Source: https://docs.sixtyfour.ai/guides/building-workflows Build a workflow in the Sixtyfour Workflow Editor: import or find records, enrich them, clean the results, and send them where they need to go. A workflow is a pipeline of blocks that takes rows from a source, enriches and cleans them, and delivers the results. Most workflows follow the same shape: ```mermaid theme={null} flowchart LR Import["Import or find data"] --> Enrich --> Clean["Clean & organize"] --> Send["Send results"] ``` > [!NOTE] > > Organizations without the Workflow Editor and Templates build and run workflows with the [Workflows API](/api-reference/workflows/workflows-overview). ## Create a workflow 1. Open [Workflows](https://app.sixtyfour.ai/workflows) in the sidebar and click **New Workflow**. 2. Choose **Start from scratch** for a blank canvas, or **Use a template** to start from the [Templates gallery](/guides/workflow-templates). 3. Name the workflow, then add blocks from the block library on the right. Search the library or open a category, and drag a block onto the canvas or click it to add it. 4. Click a block to configure it. Blocks with configuration errors are flagged on the canvas, and the workflow cannot run until they are fixed. 5. Click **Run**. The run dialog shows the estimated cost before you start. Click **Schedule** in the same dialog to run it on a cadence instead — see [Automated Workflows](/guides/automated-workflows). Open **Runs** in the editor header to follow a run block by block and download its results. The More options menu (⋮) next to **Run** holds the actions that apply to the whole workflow: | Menu item | What it does | | - | - | | Lock workflow | Prevents edits until unlocked. Locked workflows can still be deleted through the API. | | Save as Template | Saves the workflow as a template for your organization. See [Create a template](/guides/workflow-templates#create-a-template). | | Schedules | Creates and manages recurring runs. Available once the workflow is saved and runnable. | | View Workflow Definition | Shows the Workflow ID and the JSON definition used by the API. See [Programmatic Workflows](/guides/programmatic-workflows). | ## Import Data Every workflow starts with a source block. Import Data blocks bring in records you already have. | Block | Description | | - | - | | Import Data | Upload a CSV, JSON, or NDJSON file, import accounts from a saved Salesforce list, or import job applicants from your connected Ashby account. See [Salesforce Integration](/integrations/salesforce-integration) and [Ashby Integration](/integrations/ashby-integration). | | Incoming Webhooks | Start the workflow with rows sent by API call. See [Incoming Webhooks](/api-reference/webhooks/incoming). | | HubSpot Import | Import contacts or companies from your connected HubSpot account. | | Read Notebook | Read rows from a sheet in a Sixtyfour notebook. | | Jobs Search | Search job postings with filters. | ## Find Data Find Data blocks generate rows from a search. | Block | Description | | - | - | | Search | Find people or companies in Sixtyfour's dataset with a natural-language query. The most common starting point. | | Google Maps Search | Search for local businesses and places. | | X Search | Find X profiles that match a description, such as "AI researchers in NYC". | | TikTok Following | List the accounts a TikTok profile follows. | | GitHub Contributors | Find contributors, stargazers, or watchers of GitHub repositories. | | GitHub Search | Search GitHub for repositories, users, or code. | | Y Combinator Companies | Fetch Y Combinator companies by batch, industry, or tag. | ## Intelligence Intelligence blocks research each row and append new columns. Each block works on one data type: Lead (people) or Company (organizations). | Block | Data type | Description | | - | - | - | | Enrich People | Lead | Research each person and fill the return fields you define, such as title, LinkedIn URL, or skills. Runs [People Intelligence](/api-reference/endpoint/people-intelligence). | | Enrich Company | Company | Research each company and fill the return fields you define. Can also discover people at each company. Runs [Company Intelligence](/api-reference/endpoint/company-intelligence). | | Find Email | Lead | Find professional or personal email addresses. See [Find Email](/api-reference/endpoint/find-email). | | Find Phone | Lead | Find phone numbers. See [Find Phone](/api-reference/endpoint/find-phone). | | Enrich Profile | Lead | Pull profile details from a LinkedIn URL column. | | Web Research Agent | Any | Browse the web to answer custom questions for each row. Use it for specialized fields the other blocks don't cover. | | Verify Email | Lead | Check whether existing email addresses are valid and deliverable. See [Verify Email](/api-reference/endpoint/verify-email). | | Verify Phone | Lead | Check whether existing phone numbers are valid and belong to the lead. Returns validity, activity score, line type, name match, and a contact grade. | | Reverse Email | Lead | Identify the person and company behind an email address. See [Reverse Email](/api-reference/endpoint/reverse-email). | | Reverse Phone | Lead | Identify the person and company behind a phone number. See [Reverse Phone](/api-reference/endpoint/reverse-phone). | Verify Phone needs a phone column and a full-name column. Select both in the block's column mapping; if the editor suggests a replacement column, apply it only when it matches your data. ## Monitor | Block | Description | | - | - | | Monitor | Watch the rows that reach this block for changes over time. Shown when Monitors are enabled for your organization. See [Monitors](/api-reference/monitors/monitors-overview). | ## Extract From Websites | Block | Data type | Description | | - | - | - | | Scrape Web URL | Any | Scrape the pages in a URL column as markdown or HTML, with optional geo targeting and JavaScript rendering. | | Find Subdomains | Company | List subdomains for a domain, with optional time filters. | | Backlinks Referring Domains | Company | List the domains that link to a target domain or URL. | ## Clean & Organize | Block | Description | | - | - | | Filter | Keep or remove rows by column conditions, such as only verified emails. | | Deduplicate Rows | Remove duplicate rows based on one or more columns. | | LLM Output | Generate text or typed output for each row with the model you choose. Define a schema for structured output. | | Deduplicate with Notebook | Drop rows that already exist in a notebook. Use it on recurring workflows so each run saves only new rows. | | Group By | Group rows by column values with aggregations such as count, sum, or list. | | Remove Columns | Drop selected columns and keep every row. | | Extract Leads | Turn the people discovered by Enrich Company into individual Lead rows. | | Leads to Company | Group Lead rows into Company rows, with each company's leads nested. | | Transform Data | Compute new columns and reshape existing ones with AI. | ## AI Actions | Block | Description | | - | - | | Generate Text | Write custom text for each row from a prompt. Reference columns with `{column_name}`. Use it for personalized messages. | ## Send Results Output blocks deliver results. | Block | Description | | - | - | | Append to Notebook | Write results to a new or existing Sixtyfour notebook. | | Knowledge Graph | Add each row to a [Lattice](/lattice/getting-started) graph. In beta and enabled on request. | | Slack | Post a message to a Slack channel. Use `{column_name}` to insert values and `{@column_name}` to mention the Slack user whose email is in that column. | | Outgoing Webhooks | POST results to your URL when the run completes. See [Outgoing Webhooks](/api-reference/webhooks/outgoing). | For the `block_name` and `specs` of each block in the API, see the [Workflow Blocks Reference](/api-reference/workflows/workflow-blocks). ## Lead and Company data Every row in a workflow is either a Lead (a person) or a Company. Lead-only blocks cannot run on Company rows. > [!WARNING] > > Enrich Company with people discovery still outputs Company rows. Add **Extract Leads** after it before any Lead-only block, such as Find Email or Find Phone. ## Common patterns ### Find people and get their emails ```mermaid theme={null} flowchart LR Search --> Find_Email[Find Email] --> Notebook[Append to Notebook] ``` ### Research companies, find decision-makers, get contact info ```mermaid theme={null} flowchart LR Search --> Enrich_Company[Enrich Company] --> Extract_Leads[Extract Leads] --> Find_Email[Find Email] --> Notebook[Append to Notebook] ``` ### Enrich an existing list and save only new rows ```mermaid theme={null} flowchart LR Upload[Import Data] --> Enrich_People[Enrich People] --> Filter --> Dedupe[Deduplicate with Notebook] --> Notebook[Append to Notebook] ``` ### Import from your CRM, enrich, and notify your team ```mermaid theme={null} flowchart LR HubSpot[HubSpot Import] --> Enrich_People[Enrich People] --> Find_Email[Find Email] --> Filter --> Slack ``` ## Tips * Filter before enrichment. Fewer rows means faster runs and fewer credits. * Define only the return fields you need in enrichment blocks. * Check the cost estimate in the run dialog before large runs. Block pricing is listed in [Credits & Pricing](/guides/credits-and-pricing). # Credits & Pricing Source: https://docs.sixtyfour.ai/guides/credits-and-pricing What every Sixtyfour API call and workflow block costs in credits, what each plan includes, and how top-ups work. Every enrichment, search, and AI operation deducts credits from your organization's balance; this page lists the cost of each one. The API and the [workflow](/api-reference/workflows/workflows-overview) blocks share one price list: running a block costs the same as calling the matching endpoint. [MCP server](/developer-tools/mcp-setup) tools call the same endpoints and are charged the same way. > [!NOTE] > > Prices below are list prices. Enterprise contracts can set negotiated rates per operation; your dashboard and usage logs always show the rate your organization is charged. ## Plans Paid plans include a monthly credit allowance. The Free plan's 64 credits go once, to the organization created at sign-up. Organizations you create later start with 0. | Plan | Credits | Rollover | Seats | | - | -: | - | -: | | Free | 64, once | No | 1 | | Individual | 100 / month | No | 1 | | Starter | 1,200 / month | Up to 2x the monthly allowance | 2 | | Growth | 5,000 / month | Up to 2x the monthly allowance | 4 | | Enterprise | Custom | Indefinite rollover or annual true-up | Unlimited | Members and pending invites count toward seats. Every plan includes one team, the organization's default team. Contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai) for more teams. Change your plan at any time in the [billing section](https://app.sixtyfour.ai/billing) of the dashboard. ## Intelligence Pricing People and Company Intelligence are priced per record by `tier`. [Bulk Intelligence](/api-reference/endpoint/bulk-intelligence) charges the same per-row price. Company Intelligence's `find_people` and `full_org_chart` add no charge. ### People Intelligence Tiers | Tier | Credits | Description | Access | | - | -: | - | - | | `micro` | 0.3 | Lightweight enrichment, up to 10 fields | All organizations | | `low` | 1 | Basic enrichment (default) | All organizations | | `medium` | 20 | Extended enrichment | All organizations | | `high` | 40 | Full enrichment including [OSINT](https://en.wikipedia.org/wiki/Open-source_intelligence) research | On request | | `scout` | 10 | Full OSINT research, faster and at a lower price | On request | | `xhigh` | 250 per unit | Deep investigation in [Atlas](#atlas-investigations) | On request | ### Company Intelligence Tiers | Tier | Credits | Description | Access | | - | -: | - | - | | `micro` | 0.3 | Lightweight firmographics, up to 10 fields | All organizations | | `low` | 1 | Basic enrichment (default) | All organizations | | `medium` | 5 | Extended enrichment | All organizations | | `high` | 15 | Full enrichment including OSINT research | On request | Tiers marked "On request" are enabled per organization. Contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai) for access. ### Atlas Investigations People Intelligence runs at `tier: "xhigh"` create or continue an [Atlas](/atlas/overview) investigation. The same pricing applies to `POST /v1/atlas/investigations` and `POST /v1/atlas/investigations/{investigation_id}/runs`. | Engine | Price per unit | `max_credits` range | What one unit buys | | - | -: | -: | - | | X-High (`engine: "xhigh"`, or `tier: "xhigh"` on People Intelligence) | 250 | 250–2,500 | 800 research turns | | Scout (`engine: "scout"`, or `tier: "scout"` with `graph: true` on People Intelligence) | 50 | 50–500 | 100 research turns | * `max_credits` sets the run's budget and defaults to one unit. Turns scale with the budget. * A run starts only when your balance covers its `max_credits`. Credits it doesn't spend return to your balance when it ends. * A run that stops early is charged for the turns it used, at least one unit. * A run that reaches `max_minutes` is charged for the turns it used, rounded up to the next 50 credits, even when that is less than one unit. * A run can spend part of its budget on [paid sources](#paid-sources). It never costs more than its `max_credits`. * A run the agents decline to research is not charged. See [Declined runs](/atlas/integrations#declined-runs). Scout in Atlas is enabled per organization. ### Paid Sources Atlas runs can pay for outside sources while they research, out of the run's `max_credits`. No other run uses paid sources. | Source | Credits | Unit | | - | -: | - | | Browser | 1 | Per task | The Browser completes tasks on web pages that need interaction, such as an official records search form. * A task is charged once the Browser starts it, even if the run later fails. A task that never starts is not charged. * `charge_credits` includes paid tasks. * Team owners and organization admins turn each source on or off, and cap it per run in credits or calls, under **Settings → Paid sources**. The Browser is on by default. * Organizations with customer-managed encryption can't use paid sources. ### Email & Phone Discovery | Operation | Credits | Unit | | - | -: | - | | Find Email (professional, default) | 0.5 | Per lead with an email found | | Find Email (personal) | 2 | Per lead with an email found | | Find Phone | 3 | Per lead with a phone number found | Contact lookups charge only for results found. Bulk endpoints charge the same price per lead and need a balance that covers every lead in the request. ### Reverse Lookup | Operation | Credits | Unit | | - | -: | - | | Reverse Email | 0.5 | Per match found | | Reverse Phone | 3 | Per match found | Reverse lookups follow the same rules. ### Verification | Operation | Credits | Unit | | - | -: | - | | `POST /verify-email` | 0.1 | Per request | | Verify Email block | 0.07 | Per row, at least 0.1 per run | | Verify Phone block | 0.15 | Per row verified | | `POST /verify-age` | 40 | Per request | `POST /verify-email` is not charged when it returns `UNKNOWN`. Verify Phone rows skipped because the phone is blank or the name is missing are not charged. ### Search Search charges per result: a page of 10 results costs 1 credit. | Operation | Credits | Unit | | - | -: | - | | Filter Search (`/search/query`, and the deprecated `/search/start-filter-search`) | 0.1 | Per result returned | | Deep Search (`/search/start-deep-search`) | 0.1 | Per result in its CSV | | Search Export (`/search/export`) | 0.1 | Per row exported | | Lookalike Search (`/search/lookalike`) | 0.1 | Per result read with `/search/query` or exported with `/search/export`. Starting the search is free. | * A deep search with `output_mode: "query_only"` writes no CSV. Its results are charged as you read them with `/search/query`. * A deep search (including `query_only`) or an export needs a balance that covers `max_results` results; each `/search/query` page needs one that covers `page_size` results. * Viewing lookalike results in the web app is not charged. > [!TIP] > > Enterprise plans can run searches from the web app without consuming credits. Contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai) for Enterprise access. ### AI Agents | Operation | Credits | Unit | | - | -: | - | | QA Agent | 0.5 | Per row | | Research Agent | 0.3 | Per row | | LinkedIn Enrichment (`/enrich-linkedin`) | 0.3 | Per request, including when `profile_found` is `false` | | Email Generation | 0.1 | Per row | ### Monitors [Research monitors](/api-reference/monitors/monitors-overview) bill per row, per check: | Operation | Credit Cost | Notes | | - | -: | - | | Probe | 0.2 | Per active row with a baseline, on every scheduled or triggered check. | | First check | Intelligence tier price | Baseline research for a row missing watched values: the [People Intelligence](#people-intelligence-tiers) price at the monitor's tier for `lead` monitors, the [Company Intelligence](#company-intelligence-tiers) price for `company` monitors. Rows that supply every watched value skip it. | | Re-check | Intelligence tier price | Only when the probe finds a likely change, at the monitor's tier. Same pricing as the first check. | `POST /monitors/{monitor_id}/trigger` is billed like a scheduled check of every active row. On a monitor with 100,000 active rows, the probes alone cost 20,000 credits. Switched-off rows and cancelled monitors incur no charge. Each probe, first check, and re-check is charged only if it runs to completion; a completed probe is still charged when the re-check after it fails. [LinkedIn monitors](/api-reference/monitors/linkedin-monitors) cost 0.1 credits per profile check that detects a change, however many watched fields change. Baseline checks, unchanged checks and failed profile reads are free. A check with insufficient credits does not publish changes or advance the baseline. ### Lattice | Operation | Credits | Unit | | - | -: | - | | Knowledge Graph block | 1 | Per row added to the graph | Rows that fail are not charged. [Lattice](/lattice/getting-started) is in beta and enabled on request. ## Data Source & Web Scraping Pricing ### Data Sources | Operation | Credits | Unit | | - | -: | - | | Google Maps Search | 0.1 | Per page (up to 20 results) | | X (Twitter) Search | 3 | Per query | | TikTok Get Following | 0.1 | Per page fetched (auto-paginated) | | Find Subdomains | 0.005 | Per subdomain | | Backlinks / Referring Domains | 0.01 | Per result | ### Web Scraping Web scraping has a base cost plus optional add-ons, charged per page scraped. | Configuration | Credits | | - | -: | | Base | 0.01 | | + Premium proxies (`use_super`) | +0.06 | | + JavaScript rendering (`render_js`) | +0.06 | ### AI Model Pricing The AI text generation block charges per row by the model you select. | Model | Credits / row | | - | -: | | GPT-4.1 Mini (default) | 0.1 | | GPT-5 Nano | 0.15 | | GPT-5 Nano with reasoning | 0.2 | | Gemini 2.5 Flash | 0.1 | | DeepSeek V4 Flash | 0.1 | | Claude Sonnet 4.6 | 0.8 | | Claude Sonnet 5 | 0.6 | | Claude Opus 5 | 1.4 | ### Transform Data | Configuration | Credits / row | | - | -: | | `low` tier (default) | 0.1 | | `medium` tier | 0.3 | | Each list match | +0.1 | Rows whose output fields are all mapped by rules pay only for list matches. ## Search Export Limits In the web app, one search can return or export up to: | Plan | Results | | - | -: | | Free | 100 | | Individual | 1,000 | | Starter | 10,000 | | Growth | 25,000 | | Enterprise | 50,000 | For API keys and OAuth clients, see [Search limits](/api-reference/search/search-results#search-limits). ## Costs in API Responses Responses report each charge in credits, next to a deprecated field that counts 10 per credit. A 250-credit job returns `"charge_credits": 250` and `"charge_amount": 2500`. | Credit field | Deprecated field | Returned by | | - | - | - | | `charge_credits` | `charge_amount` | `GET /job-status/{task_id}` | | `cost_credits` | `cost_cents` | Find Email, Find Phone, Reverse Email, Reverse Phone, their webhook callbacks, and Atlas investigations | | `estimated_cost_credits` | `estimated_cost_cents` | Bulk Intelligence submit | | `metrics.workflow_cost_credits` | `metrics.workflow_cost` | List workflow runs | > [!WARNING] > > The deprecated fields keep their values. Read the credit fields. A response has both fields of a pair or neither. Credit values can be fractional, and a reported charge can be `0`. ## What's Free These operations do not consume credits: * Chatting with the Sixtyfour agent; credits are consumed only when a workflow or search runs * Importing data: CSV uploads, CRM imports, webhook imports * Filter and deduplicate blocks * Column removal and lead-to-company mapping * Writing workflow results to CSV and downloading result files. [Search exports](#search) are charged per row. * Slack messages * Reading notebooks (Read Notebook block) * Polling job status (`/job-status/{job_id}`) and Age Verification results (`GET /verify-age/{task_id}`), and checking your balance (`/check-balance`) * Requesting `field_confidence` on People or Company Intelligence * Reading [Atlas](/atlas/api) investigations, generating their reports, and downloading their exports * Failed workflow rows: a row that fails at a step is not charged for that step or any later step. See [Failed rows](/api-reference/workflows/workflows-execution#failed-rows). * Single-record jobs that fail, sync or async, except [paid-source](#paid-sources) tasks already started. * Requests rejected with `402`, `429`, or `503` ## Credit Top-Ups Organization admins and owners buy one-time top-ups in the [billing section](https://app.sixtyfour.ai/billing) of the dashboard. Top-ups need an active paid plan and cost $0.30 per credit, with a $50 minimum, unless your organization has its own rate or minimum; your rate is shown before you buy. Plan credits cost less per credit. Organizations billed by contract arrange additional credits with the Sixtyfour team. > [!NOTE] > > Top-up credits expire 1 year from the date of purchase. Auto-recharge tops up your balance automatically when it falls below a threshold you set in the dashboard. ## Cost Optimization * Filter and deduplicate before enriching so credits go only to records you will use. * Use professional email mode. Personal email discovery costs 4x more. * Start with the lowest tier that returns the fields you need; `micro` and `low` cover most high-volume lookups. * Run new workflows on 10–20 records to validate results before scaling. * Track spend on the [Usage & API Logs](/guides/usage-logs) page, which shows credit consumption by endpoint and over time. # Getting Support Source: https://docs.sixtyfour.ai/guides/getting-support Get answers from the docs assistant or the Sixtyfour Agent, or contact the Sixtyfour team. Get answers instantly from the docs assistant or the Sixtyfour Agent, or email the Sixtyfour team. ## Docs assistant Click **Ask AI** at the top of any docs page to get answers from the Sixtyfour documentation. ![Docs AI Assistant](https://mintcdn.com/sixtyfour/zETpjyO1kKdpOp2f/images/docs_assistant.png?fit=max\&auto=format\&n=zETpjyO1kKdpOp2f\&q=85\&s=5166e034438b8017f1658fb909a7dcb5) ## Sixtyfour Agent In the [web app](https://app.sixtyfour.ai), ask the [Sixtyfour Agent](/guides/using-agents) for help building workflows, running searches, or reading results. Ask it to send feedback and it forwards your message to the Sixtyfour team. ![Sixtyfour Agent](https://mintcdn.com/sixtyfour/zETpjyO1kKdpOp2f/images/agent_support.png?fit=max\&auto=format\&n=zETpjyO1kKdpOp2f\&q=85\&s=b79ea659622e3ffeea82682c05f526a9) ## Support by plan | Plan | Support | | - | - | | Free | Docs assistant | | Individual | Email | | Starter | Email | | Growth | Email and Slack | | Enterprise | Priority support and onboarding | ## Contact us Email [support@sixtyfour.ai](mailto:support@sixtyfour.ai) with your organization name. For API issues, include the request path, the time, the response's [`X-Correlation-ID`](/api-reference/authentication#request-ids) (**Request ID** in [Usage & API Logs](/guides/usage-logs), `correlation_id` in an error body), and the job or run ID for async work. For priority support, custom SLAs, or a dedicated point of contact, [talk to sales](https://cal.com/team/sixtyfour/discovery) about the Enterprise plan. # Using OAuth Clients Source: https://docs.sixtyfour.ai/guides/oauth-clients Set up an OAuth client in the Sixtyfour dashboard for server-to-server authentication without sharing long-lived API keys. An OAuth client lets a server exchange a signed JWT assertion for a short-lived access token instead of holding a long-lived API key. Sixtyfour verifies the assertion against a public key you upload or publish at a JWKS URL; there is no client secret. Organization admins and owners manage OAuth clients. See [Roles and Permissions](/guides/roles-and-permissions). | Property | Value | | - | - | | Grant type | `client_credentials` | | Client auth | `private_key_jwt` | | Signing algorithm | `RS256` | | Scope | `full_access` (the only scope; may be omitted) | | Token URL | `https://api.sixtyfour.ai/oauth/token` | | Discovery URL | `https://api.sixtyfour.ai/.well-known/oauth-authorization-server` | ## Open OAuth Clients Go to [Settings → OAuth Clients](https://app.sixtyfour.ai/settings/oauth-clients). ![OAuth Clients settings page](https://mintcdn.com/sixtyfour/O829If3JLSrTpZLW/images/oauth-clients-empty.png?fit=max\&auto=format\&n=O829If3JLSrTpZLW\&q=85\&s=68816a2eb9ff1882e634b89b6342a5bf) ## Create a client 1. Click **Create client** 2. Enter a **Client Name** (for example, `my-production-app`) 3. Optionally enter a **JWKS URL**: a public HTTPS endpoint that serves your JSON Web Key Set > [!NOTE] > > Provide a JWKS URL for automatic key rotation, or leave it blank to upload public keys. You can't change this after creation. 4. Click **Create and continue** Sixtyfour generates a `client_id` and opens the client's detail page. ## Register a public key Pick the flow that matches how you created the client. ### With a JWKS URL If you configured a JWKS URL, Sixtyfour fetches your public keys from that endpoint automatically. Validate it at any time: 1. On the client detail page, click **Validate JWKS** 2. Confirm the health indicator shows a green check and a recent validation timestamp ### With an uploaded public key If you did not set a JWKS URL, generate an RSA key pair and upload the public half: ```bash theme={null} openssl genrsa -out private.pem 2048 openssl rsa -in private.pem -pubout -out public.pem ``` 1. Click **Upload key** 2. Paste the contents of `public.pem` into **Public Key (PEM)** (including the `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----` lines) 3. Click **Upload key** Copy the generated **Key ID** (`kid`). It goes in the JWT header. > [!WARNING] > > Keep `private.pem` on the server that signs assertions. Never commit it or send it to a browser. ## Request an access token Your server signs a short-lived JWT assertion with the private key, then POSTs it to the token endpoint. ```python Python theme={null} import jwt, time, uuid, requests # pip install PyJWT cryptography requests CLIENT_ID = "YOUR_CLIENT_ID" KEY_ID = "YOUR_KEY_ID" TOKEN_URL = "https://api.sixtyfour.ai/oauth/token" ISSUER = TOKEN_URL.replace("/oauth/token", "") # or fetch from discovery # Load your private key (keep this secret, never commit it) with open("private.pem") as f: private_key = f.read() # Build the signed JWT assertion now = int(time.time()) assertion = jwt.encode( { "iss": CLIENT_ID, "sub": CLIENT_ID, "aud": ISSUER, "iat": now, "exp": now + 60, "jti": str(uuid.uuid4()), }, private_key, algorithm="RS256", headers={"kid": KEY_ID}, ) # Exchange for an access token resp = requests.post(TOKEN_URL, data={ "grant_type": "client_credentials", "client_id": CLIENT_ID, "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion": assertion, "scope": "full_access", }) access_token = resp.json()["access_token"] print("Access token:", access_token) ``` ```javascript JavaScript theme={null} import jwt from "jsonwebtoken"; import { randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; const CLIENT_ID = "YOUR_CLIENT_ID"; const KEY_ID = "YOUR_KEY_ID"; const TOKEN_URL = "https://api.sixtyfour.ai/oauth/token"; const ISSUER = "https://api.sixtyfour.ai"; const privateKey = readFileSync("private.pem", "utf8"); const now = Math.floor(Date.now() / 1000); const assertion = jwt.sign( { iss: CLIENT_ID, sub: CLIENT_ID, aud: ISSUER, iat: now, exp: now + 60, jti: randomUUID(), }, privateKey, { algorithm: "RS256", header: { kid: KEY_ID } }, ); const body = new URLSearchParams({ grant_type: "client_credentials", client_id: CLIENT_ID, client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", client_assertion: assertion, scope: "full_access", }); const resp = await fetch(TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body, }); const { access_token } = await resp.json(); console.log("Access token:", access_token); ``` The response contains a bearer token: ```json theme={null} { "access_token": "s4at_...", "token_type": "Bearer", "expires_in": 3600, "scope": "full_access" } ``` The access token is an opaque string that starts with `s4at_`. Send it as is; do not parse it. Reuse it until shortly before `expires_in` runs out instead of requesting a new one per call: `POST /oauth/token` is [rate limited](/api-reference/rate-limits#oauth-tokens). ### Assertion rules * Sign with `RS256` and put a registered key's `kid` in the header. Assertions with a `jku`, `x5u`, or `x5c` header are refused. * Set `iss` and `sub` to your `client_id`, and `aud` to the discovery document's `issuer`, `https://api.sixtyfour.ai`. * Set `iat` and `exp` as integers. An assertion can live at most 300 seconds (`exp - iat`), and Sixtyfour allows 60 seconds of clock skew. * Use a new `jti` for every assertion; each assertion works once. Token errors use the OAuth format, `{"error": "...", "error_description": "..."}`. See [OAuth token errors](/api-reference/errors#oauth-token-errors). ## Call the API Send the token in the `Authorization` header on any Sixtyfour API request: ```http theme={null} Authorization: Bearer YOUR_ACCESS_TOKEN Content-Type: application/json ``` OAuth access tokens work on every endpoint that accepts `x-api-key`. When a request sends both, the API uses the token. See [Authentication](/api-reference/authentication). An OAuth client acts for your organization's default team. Its tokens see only that team's workflows, runs and monitors, and count toward that team's monthly credit limit. ## Managing a client ### Adjust token TTL **Token TTL** sets how long issued access tokens stay valid: the default (1 hour), **1 minute**, **5 minutes**, **1 hour** or **24 hours**. The token response's `expires_in` reflects the setting. ### Rotate keys If you configured a JWKS URL, publish the new key in your JWKS document and click **Validate JWKS**. Sixtyfour can take up to 5 minutes to accept the new `kid`, so keep signing with the old key until then. If you uploaded a public key directly, upload the new key, switch your signer to the new `kid`, then revoke the old key from the Public Keys table. ### Permanently disable a client **Disable**, then **Disable permanently**, invalidates every outstanding token at once. A disabled client can't be re-enabled; create a new client to restore access. ### Delete a client Click the trash icon on the detail page, then **Delete client**. Deleting revokes all tokens and keys for that client and cannot be undone. # Programmatic Workflows Source: https://docs.sixtyfour.ai/guides/programmatic-workflows Define, version, and deploy Sixtyfour workflows as JSON with the Workflows API. Every workflow is a JSON definition you can keep in version control and deploy with the Workflows API, without opening the Workflow Editor. To build workflows in the UI, see [Building Workflows](/guides/building-workflows). To run a workflow on a list of records, see [Batch Processing](/guides/batch-processing). ## The workflow definition A workflow definition is a directed graph of blocks and edges. This is the body for a new workflow. An update also sends `expected_revision`; see [Deploy from CI](#deploy-from-ci). ```json theme={null} { "workflow_name": "Enrich inbound leads", "workflow_description": "Incoming webhook → enrich people → outgoing webhook", "workflow_definition": { "blocks": [ { "block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b01", "block_name": "webhook", "specs": { "input_schema": { "email": { "type": "string" }, "company": { "type": "string" } }, "dataframe_type": "LEAD" } }, { "block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b02", "block_name": "lead_enrichment", "specs": { "return_fields": { "title": "string", "linkedin_url": "string" } } }, { "block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b03", "block_name": "outgoing_webhook", "specs": { "url": "https://example.com/hooks/sixtyfour", "timeout_seconds": 10 } } ], "edges": [ { "from_block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b01", "to_block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b02" }, { "from_block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b02", "to_block_id": "6f1c2a4e-1b7d-4c55-9a43-0d1e5f6a7b03" } ] } } ``` | Field | Type | Description | | - | - | - | | `workflow_name` | string | Name shown in the editor and in `GET /workflows`. | | `workflow_description` | string | Free-form description. | | `workflow_definition.blocks` | array | The processing steps. Execution follows `edges`, not array order. | | `workflow_definition.edges` | array | Directed connections from `from_block_id` to `to_block_id`. | | `id` | string | Optional UUID to use as the Workflow ID. It must not already be in use. | Each block: | Field | Type | Description | | - | - | - | | `block_id` | string | Optional UUID referenced by `edges`. A missing or non-UUID value is replaced with a new UUID every time the workflow is saved, so use UUIDs. | | `block_name` | string | The block to run, such as `lead_enrichment`. See the [Workflow Blocks Reference](/api-reference/workflows/workflow-blocks). | | `specs` | object | Block configuration. The schema depends on `block_name`. | ## Get the JSON for an existing workflow Build the workflow in the editor once, then export it: * In the editor, open the More options menu (⋮) next to **Run** and select **View Workflow Definition**. The dialog shows the Workflow ID and the JSON definition. * With the API, `GET /workflows/{workflow_id}` returns the graph under `workflow_definition` and its revision under `edit_revision`. For an update, export the graph and revision from the same response. Run this in a fresh directory; keep `workflow-base.json` unchanged and edit the content fields in `workflow-update.json`. ```bash theme={null} set -euo pipefail curl --fail --silent --show-error "https://api.sixtyfour.ai/workflows/WORKFLOW_ID" \ -H "x-api-key: YOUR_API_KEY" \ | jq '{workflow_name: .name, workflow_description: .description, workflow_definition: .workflow_definition, expected_revision: .edit_revision}' \ > workflow-base.json cp workflow-base.json workflow-update.json ``` Leave `expected_revision` as exported, even if the workflow changes before CI runs. ## Create a workflow from JSON Save the definition above as `workflow.json`, without `expected_revision`, and POST it to `create_workflow`. Keep the `id` from the response to run it. ```bash cURL theme={null} curl --fail -X POST "https://api.sixtyfour.ai/workflows/create_workflow" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d @workflow.json ``` ```python Python theme={null} import json, requests with open("workflow.json") as f: body = json.load(f) resp = requests.post( "https://api.sixtyfour.ai/workflows/create_workflow", headers={"x-api-key": "YOUR_API_KEY"}, json=body, ) resp.raise_for_status() print("created:", resp.json()["id"]) ``` ```javascript JavaScript theme={null} import fs from "node:fs/promises"; const body = JSON.parse(await fs.readFile("workflow.json", "utf8")); const resp = await fetch("https://api.sixtyfour.ai/workflows/create_workflow", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!resp.ok) throw new Error(await resp.text()); const { id } = await resp.json(); console.log("created:", id); ``` ```json Response (abridged) theme={null} { "id": "0b7a3c2e-5d4f-4e8a-9c61-2f3e4d5a6b7c", "name": "Enrich inbound leads", "description": "Incoming webhook → enrich people → outgoing webhook", "status": "created", "created_at": "2026-01-15T10:30:00Z", "updated_at": "2026-01-15T10:30:00Z", "workflow_definition": { "blocks": [], "edges": [] } } ``` With an API key, an invalid definition is rejected with a `validation_errors` list. With an OAuth access token, it is saved as a draft, which never runs. See [Validation errors](/api-reference/workflows/workflows-management#validation-errors) and [Limits](/api-reference/workflows/workflows-management#limits). ## Deploy from CI Commit `workflow-base.json` and your edited `workflow-update.json` together. CI submits `workflow-update.json` as is, without fetching a newer revision. If the workflow changed after export, the update is refused. Export the current workflow to a new directory, reapply your changes to it, and submit again. Copying only the new `edit_revision` into your old file overwrites the other changes. See [workflow edit conflicts](/api-reference/errors#409-conflict-and-428-precondition-required-workflow-edits). ```bash cURL theme={null} set -euo pipefail jq -e '.expected_revision | numbers | select(. >= 0 and . == floor)' workflow-update.json > /dev/null curl --fail -X POST "https://api.sixtyfour.ai/workflows/update_workflow?workflow_id=WORKFLOW_ID" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d @workflow-update.json ``` ```python Python theme={null} import json, os, requests with open("workflow-update.json") as f: body = json.load(f) revision = body.get("expected_revision") if type(revision) is not int or revision < 0: raise ValueError("workflow-update.json must contain its exported expected_revision") resp = requests.post( "https://api.sixtyfour.ai/workflows/update_workflow", params={"workflow_id": os.environ["WORKFLOW_ID"]}, headers={"x-api-key": "YOUR_API_KEY"}, json=body, ) resp.raise_for_status() ``` ```javascript JavaScript theme={null} import fs from "node:fs/promises"; const body = JSON.parse(await fs.readFile("workflow-update.json", "utf8")); if (!Number.isSafeInteger(body.expected_revision) || body.expected_revision < 0) { throw new Error("workflow-update.json must contain its exported expected_revision"); } const resp = await fetch( `https://api.sixtyfour.ai/workflows/update_workflow?workflow_id=${process.env.WORKFLOW_ID}`, { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json" }, body: JSON.stringify(body), }, ); if (!resp.ok) throw new Error(await resp.text()); ``` ## Authentication The Workflows API accepts an API key or an OAuth access token. A workflow belongs to the team of the credential that creates it. To copy a workflow to another organization, create it with that organization's credential, without `expected_revision`. See [Get API Key](/get-api-key) and [OAuth Clients](/guides/oauth-clients). For error responses, see [Handling Errors](/api-reference/errors). # Roles & Permissions Source: https://docs.sixtyfour.ai/guides/roles-and-permissions How organization roles and team membership control access in Sixtyfour. Access in Sixtyfour is set by two things: your role in the organization, and the teams you belong to. ## Organization roles Every member has one organization role. | Role | Summary | | - | - | | **Member** | Works inside the teams they belong to. | | **Admin** | Everything a member can do, plus organization-wide management: teams, credits, API request logs, webhook signing and OAuth clients. Acts as an owner of every team. | | **Owner** | Everything an admin can do, plus changing organization roles. | An organization always has at least one owner. ## Teams Workflows, runs, API keys and logs belong to a team. Credits are shared across the organization, and admins can set a monthly credit limit for each team. Members only see the teams they belong to. Everyone in the organization belongs to its default team. Every plan includes one team, the default team; see [Plans](/guides/credits-and-pricing#plans) for seats and more teams. Within a team, a member is a team member or a team owner. Every team except the default team has at least one owner; the default team has none. Organization admins and owners act as owners of every team. Sixtyfour can turn the `high` and `xhigh` tiers off for one team while the rest of the organization keeps them. Contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai) to set this up. ## What each role can do | Action | Team member | Team owner | Org admin | Org owner | | - | :-: | :-: | :-: | :-: | | Run workflows and API calls in the team | ✓ | ✓ | ✓ | ✓ | | See the team's workflow runs and spend | ✓ | ✓ | ✓ | ✓ | | Create API keys for the team | ✓ | ✓ | ✓ | ✓ | | Revoke the team's API keys | | ✓ | ✓ | ✓ | | Manage team membership and invites | | ✓ | ✓ | ✓ | | Turn [paid sources](/guides/credits-and-pricing#paid-sources) on or off and cap them per run | | ✓ | ✓ | ✓ | | See each member's spend in the team (organizations with private runs) | | ✓ | ✓ | ✓ | | Create teams and set each team's monthly credit limit | | | ✓ | ✓ | | See API request logs, export logs, see organization-wide spend | | | ✓ | ✓ | | Manage the [webhook signing secret](/api-reference/webhooks/signing-secrets) | | | ✓ | ✓ | | Manage [OAuth clients](/guides/oauth-clients) | | | ✓ | ✓ | | Change organization roles | | | | ✓ | Every member can see whether a webhook signing secret is configured. ## Manage members and roles Go to **Settings → People & teams**. The **Members** list shows everyone in the organization with their role. Owners change a member's role from the **Role** column. Use **Invite member** to add people by email. Members and pending invites count toward your plan's seats. # Struct & Type Casting Source: https://docs.sixtyfour.ai/guides/struct-and-type-casting Define the fields an enrichment returns and the type each value comes back as. The `struct` parameter lists the fields you want back and, optionally, the type of each one, so [People Intelligence](/api-reference/endpoint/people-intelligence), [Company Intelligence](/api-reference/endpoint/company-intelligence) and [QA Agent](/api-reference/endpoint/qa-agent) return data your code can use without parsing. ## Define a field Each key in `struct` is a field name. Its value is either a description or a typed object. ```json theme={null} { "struct": { "email": "The individual's work email address", "years_of_experience": { "description": "Total years of professional experience", "type": "int" } } } ``` | Form | Example | Returned as | | - | - | - | | Description | `"The individual's work email address"` | String | | Typed object | `{"description": "...", "type": "int"}` | The given type | Field names must be letters, numbers and underscores. The description is what the agent researches, so say exactly what you want: `"Current job title at the company in the input"` beats `"title"`. Use a typed object for every field that isn't a string ([details below](#how-each-endpoint-resolves-types)). ## Types | Type | Returned as | Example value | | - | - | - | | `"str"` or `"string"` | String | `"Pacific View Studios"` | | `"int"` or `"integer"` | Integer | `50` | | `"float"` or `"number"` | Number | `95.5` | | `"bool"` or `"boolean"` | Boolean | `true` | | `"list"`, `"list[str]"` or `"list[string]"` | Array of strings | `["SaaS", "Fintech"]` | | `"list[int]"` or `"list[integer]"` | Array of integers | `[2019, 2023]` | | `"list[float]"` or `"list[number]"` | Array of numbers | `[1.5, 2.7]` | | `"list[bool]"` or `"list[boolean]"` | Array of booleans | `[true, false]` | | `"dict"` | JSON object serialized as a string | `"{\"plan\": \"pro\"}"` | | `"enum"` | One of `options` | `"Series A"` | | `"list[enum]"` | Array of values from `options` | `["Sales", "Marketing"]` | | `"object"` | Object with the given `subfields` | `{"city": "Austin", "country": "US"}` | | `"object_list"` | Array of objects with the given `subfields` | `[{"name": "Acme", "role": "CTO"}]` | Parse `dict` values client-side with `json.loads()` (Python) or `JSON.parse()` (JavaScript). ### Enums `enum` and `list[enum]` require a non-empty `options` list. The value is constrained to those options. ```json theme={null} { "seniority": { "description": "Seniority of the current role", "type": "enum", "options": ["IC", "Manager", "Director", "VP", "C-level"] } } ``` ### Nested objects `object` and `object_list` take `subfields`: a list of `{"key", "type", "description"}` objects. A subfield can be any type except `object` and `object_list`; `enum` and `list[enum]` subfields take `options`. `type` defaults to `string` and `description` is optional. ```json theme={null} { "work_history": { "description": "Previous roles, most recent first", "type": "object_list", "subfields": [ {"key": "company", "type": "string", "description": "Company name"}, {"key": "title", "type": "string", "description": "Job title"}, {"key": "start_year", "type": "int", "description": "Year the role started"} ] } } ``` ## Missing values Every field you request is present in the response. A field the agent couldn't establish is `null` (Company Intelligence returns `[]` for list fields). Values are never invented to satisfy a type. ## How each endpoint resolves types Only typed objects return the same type on every People and Company Intelligence request. A bare type name (`"int"`) returns that type with no description, and an example value (`50`) returns a string, except on Company Intelligence with `full_org_chart: true`, which reads a bare type name as a description and returns an example value as its own type. QA Agent accepts descriptions only and returns every field as a string. See [QA Agent](/api-reference/endpoint/qa-agent#struct). ## Type casting When a researched value doesn't match its field's type, Sixtyfour converts it only if the conversion loses nothing: | Target type | Accepted | | - | - | | String | Strings, plus numbers and booleans, converted to text | | Integer | Integers, whole-number floats (`5.0`), integer strings (`"5"`) | | Number | Numbers and numeric strings (`"95.5"`) | | Boolean | Booleans and the strings `"true"` and `"false"` | | Array | Arrays; each item is converted to the item type | Nothing else is cast: `"about 50"` never becomes `50`. ## Input fields in the output In People Intelligence, fields you send in `lead_info` but don't list in `struct` come back in `structured_data` unchanged, with their original values and types. Fields you do list in `struct` are researched and returned with the struct's type, even if you sent a value for them. ```json theme={null} { "lead_info": {"name": "Jane Doe", "company": "Acme Corp", "crm_id": 48213}, "struct": { "title": "Current job title", "years_of_experience": {"type": "int", "description": "Total years of professional experience"} } } ``` ```json theme={null} { "structured_data": { "title": "VP of Sales", "years_of_experience": 12, "name": "Jane Doe", "company": "Acme Corp", "crm_id": 48213 } } ``` > [!NOTE] > > There is no image type. A field whose description asks for an image (a headshot, a logo, a screenshot) is an ordinary string field whose value is a permanent image URL hosted by Sixtyfour. See [Image fields](/api-reference/endpoint/people-intelligence#image-fields). # Usage & API Logs Source: https://docs.sixtyfour.ai/guides/usage-logs Track credit spend, inspect workflow runs and API requests, and export API logs as CSV. The **Usage** page in the dashboard shows what your team has spent and every run and request behind it. Usage is scoped to your active team. Switch teams to see another team's usage. ## Spend The top of the page shows **Credits spent** in the selected range and **Credits left**, with a spend chart over time. Members see their team's spend; organization admins and owners can switch to organization-wide spend. In organizations with private runs, team owners and admins also see each member's spend. > [!TIP] > > Check your balance programmatically with [`/check-balance`](/api-reference/account/check-balance). ## Logs The logs table lists workflow runs, searches and API requests in one place. | Column | Description | | - | - | | **Status** | Success, Failed or In progress. Cancelled and timed-out runs show as Failed. | | **Request** | The workflow, search or endpoint | | **Run By** | The member or API key that started it | | **Duration** | Total processing time | | **Cost** | Credits consumed | | **Timestamp** | When it started | Click a row to see its details. For API calls, they include the request, the response and the **Request ID**, which is the response's `X-Correlation-ID` header. > [!NOTE] > > For organizations with customer-managed encryption, Sixtyfour does not store API request and response bodies or error messages. The details view and log exports leave them empty. Narrow the table with the filters above it: | Filter | Options | | - | - | | Source | API, MCP, API Playground | | Status | Success, Failed, In progress | | User | Any member of the team | API request logs are visible to organization admins and owners. Members see workflow runs and searches. ## Export API logs Organization admins and owners can export the active team's API request logs. Click **Export** above the logs table. | Option | Values | | - | - | | Time Range | **7 Days**, **30 Days**, **90 Days**, or a custom date range | | Optional Fields | **Include Request Input** (request body), **Include Response Output** (response body) | The dialog previews the row count. Exports are capped at 10,000 rows; narrow the time range to export older logs in parts. Download the result as CSV or send it to Google Sheets. Every export includes Request ID, Timestamp, Endpoint, Source, Status, Duration (ms), Cost (credits), HTTP Status, API Key and Error Message. # Using the Agent Source: https://docs.sixtyfour.ai/guides/using-agents Ask the Sixtyfour Agent in plain English to search, enrich, and build workflows in the Sixtyfour web app. The Sixtyfour Agent turns a plain-English request into searches, enrichment runs, and saved workflows in the [Sixtyfour web app](https://app.sixtyfour.ai). It picks the [workflow blocks](/api-reference/workflows/workflow-blocks), connects them, runs them, and shows the results in chat. > [!NOTE] > > The Agent is included on the Enterprise plan. [Talk to sales](https://cal.com/team/sixtyfour/discovery) to enable it. ## Start a chat Click **New chat** in the sidebar and describe what you want. Follow up in the same chat to refine filters, add enrichment steps, or change the output. ![Getting Started](https://mintcdn.com/sixtyfour/aEUTQzZtClD_Aqe3/images/agent_input.png?fit=max\&auto=format\&n=aEUTQzZtClD_Aqe3\&q=85\&s=d6c1c7a992c48dc1e8bf3dc5e3d4fb47) ## What the Agent can do | Capability | What it does | | - | - | | Search people and companies | Runs searches across Sixtyfour's people and company data from a description, and finds people similar to a set of seed profiles | | Build and edit workflows | Creates workflows, reads and edits existing ones, and starts from curated workflow templates | | Run and cancel | Runs a workflow or a single block on your data, tracks progress, and cancels runs | | Work with your files | Reads attached tabular files (CSV, JSON, JSONL, NDJSON) and documents (PDF, Word, PowerPoint, OpenDocument, RTF, text) | | Analyze results | Filters, joins, and summarizes result data privately, within the chat | | Notebooks | Lists and inspects [notebooks](/guides/building-workflows#send-results) and writes workflow output to them | | Run history | Lists workflow runs with status, rows processed, and credit cost | | Account | Shows your credit balance, usage, enrichment tiers available to your organization, and team members | | Web research | Looks up current information on the web to inform a search or workflow | | Docs | Answers questions from the Sixtyfour documentation | Example requests: * `"Find CTOs at Series B AI startups in San Francisco"` * `"Build a workflow that finds VPs of Sales at fintech companies, gets their emails, and saves verified results to a notebook"` * `"Add a filter to my workflow that removes leads without a LinkedIn URL"` * `"Take the attached CSV, enrich each person, and keep only verified emails"` * `"How many credits did my workflows use this week?"` ![Search for People and Companies](https://mintcdn.com/sixtyfour/aEUTQzZtClD_Aqe3/images/agent_chat.png?fit=max\&auto=format\&n=aEUTQzZtClD_Aqe3\&q=85\&s=3b5c93e0a2156e5b707726758a675411) To invite teammates, use **Settings → Organization**. The Agent lists members but does not send invites. ## How the Agent builds workflows ```mermaid theme={null} flowchart LR Query["Your request"] --> Plan["Agent picks and connects blocks"] --> Approve["You approve actions"] --> Run["Workflow runs"] --> Results["Results in chat and notebook"] ``` * Workflows pass two data types, Lead (people) and Company. The Agent inserts conversion blocks such as Extract Leads when a step needs the other type. * The Agent places filters before costly enrichment steps and asks before actions that spend credits, unless YOLO mode is on. ## Agent preferences Open **Settings → Agent Preferences** to configure the Agent. ![Agent Preferences](https://mintcdn.com/sixtyfour/aEUTQzZtClD_Aqe3/images/agent_preferences.png?fit=max\&auto=format\&n=aEUTQzZtClD_Aqe3\&q=85\&s=786a087f1fa6f37e9dba55d1cbe8c91d) | Setting | Scope | Behavior | | - | - | - | | Guidance | Your conversations only | Instructions and context the Agent follows in every chat, up to 5,000 characters | | YOLO mode | Organization | Auto-approves tool actions without confirmation. Only admins can change it | > [!WARNING] > > With YOLO mode on, the Agent spends credits, edits notebooks, and runs workflows without asking first, for everyone in your organization. ![YOLO Mode](https://mintcdn.com/sixtyfour/GWbtdCZRg6aZumWB/images/yolo_mode.png?fit=max\&auto=format\&n=GWbtdCZRg6aZumWB\&q=85\&s=cc694fff8a165c459ed85cf08f2c2c81) ## Tips * Name the job titles, locations, company criteria, and output you want. Specific requests produce a better first result. * Ask the Agent to show the plan before it runs anything costly. * Put standing instructions, such as your ideal customer profile or columns you always want, in Guidance instead of repeating them. # Using Webhooks Source: https://docs.sixtyfour.ai/guides/using-webhooks Trigger workflows from external events and receive results without polling. Webhooks connect Sixtyfour to your systems in both directions: external events start workflows, and Sixtyfour posts results to your server when work completes. | Direction | Use it to | Reference | | - | - | - | | Incoming | Start a workflow when a form, CRM or app fires an event | [Incoming Webhooks](/api-reference/webhooks/incoming) | | Outgoing | Receive async job results or workflow output at your URL | [Outgoing Webhooks](/api-reference/webhooks/outgoing) | | Signing | Verify that deliveries come from Sixtyfour | [Signing Secrets](/api-reference/webhooks/signing-secrets) | ## Trigger a workflow from an external event Build a workflow whose first block is a `webhook` source, then POST the event data to `/workflows/run` with your API key: ```bash theme={null} curl -X POST "https://api.sixtyfour.ai/workflows/run?workflow_id=WORKFLOW_ID" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_payload": [ { "name": "Jane Smith", "company": "Northgate Labs" } ] }' ``` Each object in `webhook_payload` becomes a row. Payload rules and the response are in [Incoming Webhooks](/api-reference/webhooks/incoming). ## Receive async results at your URL Add `webhook_url` to any async endpoint. Sixtyfour POSTs the result when the job completes. ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/find-email-async" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "lead": { "name": "Jane Smith", "company": "Northgate Labs" }, "webhook_url": "https://your-server.com/webhooks/sixtyfour" }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/find-email-async", headers={"x-api-key": "YOUR_API_KEY"}, json={ "lead": {"name": "Jane Smith", "company": "Northgate Labs"}, "webhook_url": "https://your-server.com/webhooks/sixtyfour", }, ) task_id = response.json()["task_id"] ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/find-email-async", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ lead: { name: "Jane Smith", company: "Northgate Labs" }, webhook_url: "https://your-server.com/webhooks/sixtyfour", }), }); const { task_id } = await response.json(); ``` Store the `task_id` so you can match the callback and fall back to `GET /job-status/{task_id}` if no callback arrives. The supported endpoints and payload fields are in [Outgoing Webhooks](/api-reference/webhooks/outgoing#async-job-webhooks). To post workflow output instead, end the workflow with an `outgoing_webhook` block. See [Workflow outgoing\_webhook block](/api-reference/webhooks/outgoing#workflow-outgoing_webhook-block). ## Handle deliveries A receiver verifies the signature, acknowledges fast and processes later: ```python theme={null} from flask import Flask, request app = Flask(__name__) seen_deliveries = set() def delivery_key(event: dict) -> str: if "task_id" in event: # async-job callback return f"task:{event['task_id']}" if event.get("event") == "outgoing_webhook.completed": # workflow block return f"run:{event['run_id']}:{event['previous_block_id']}" return f"{event['type']}:{event.get('event_group_id') or event['run_id']}" # monitor @app.post("/webhooks/sixtyfour") def sixtyfour_webhook(): raw_body = request.get_data() if not verify(raw_body, request.headers.get("Sixtyfour-Signature", ""), SECRET): return "", 401 event = request.get_json() key = delivery_key(event) if key in seen_deliveries: return "", 200 enqueue(event) # process outside the request seen_deliveries.add(key) # only after the event is queued return "", 200 ``` `verify` is the function in [Signing Secrets](/api-reference/webhooks/signing-secrets#verify-signatures). Verify against the raw body before parsing JSON. Use a persistent store for `seen_deliveries` in production. `delivery_key` dedupes on the job or run, not `event_id`: monitor notifications carry no `event_id`, and Sixtyfour can deliver the same result again with a new `event_id`. ## Test locally Sixtyfour only delivers to public hosts, and async-job callbacks and test deliveries require HTTPS. Expose your local server through a tunnel such as ngrok or Cloudflare Tunnel and use the tunnel's HTTPS URL as `webhook_url`. ## Test deliveries Send a sample delivery to your receiver to check its signature verification and parsing before a real job runs. A test delivery runs no job and uses no credits. `GET /webhooks/test-events` lists the samples. Each entry has a `type`, a `description`, and an `example` payload. Add `?type=find_email` to get that one example payload on its own. `POST /webhooks/test-events` sends one sample to your URL with the same signing and retries as a real delivery: | Field | Required | Description | | - | - | - | | `event` | Yes | A `type` from `GET /webhooks/test-events`. | | `url` | Yes | Your receiver's public `https://` URL. | | `signing` | No | `auto` (default) signs the delivery when your organization has a signing secret, like a real delivery. `required` refuses to send when there is no active signing secret. | ```bash cURL theme={null} curl -X POST "https://api.sixtyfour.ai/webhooks/test-events" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "event": "find_email", "url": "https://your-server.com/webhooks/sixtyfour", "signing": "required" }' ``` ```python Python theme={null} import requests response = requests.post( "https://api.sixtyfour.ai/webhooks/test-events", headers={"x-api-key": "YOUR_API_KEY"}, json={ "event": "find_email", "url": "https://your-server.com/webhooks/sixtyfour", "signing": "required", }, timeout=120, ) response.raise_for_status() print(response.json()) ``` ```javascript JavaScript theme={null} const response = await fetch("https://api.sixtyfour.ai/webhooks/test-events", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ event: "find_email", url: "https://your-server.com/webhooks/sixtyfour", signing: "required", }), }); if (!response.ok) throw new Error(await response.text()); console.log(await response.json()); ``` ```json theme={null} { "id": "21b3e340-e326-4c01-9a42-7dc426ec4481", "delivered": true, "status_code": 200, "attempts": 1 } ``` `delivered` is `true` only when your receiver returned `2xx`. `status_code` is the last attempt's HTTP status, or `null` when no response arrived. The call waits for every retry and can take up to 100 seconds. Each call sends a new event, so don't retry it automatically. Test deliveries carry these headers on top of the [delivery headers](/api-reference/webhooks/outgoing#headers): | Header | Value | | - | - | | `Sixtyfour-Test` | `true` | | `Sixtyfour-Test-Run-Id` | The `id` from the response. The sample payload uses it as its event or task ID. | | `Sixtyfour-Test-Case` | The `event` you sent | The catalog also lists other samples. These `type` values match deliveries Sixtyfour sends: | `type` | Matches | | - | - | | `find_email`, `find_phone`, `reverse_enrichment` | Find Email, Find Phone, and Reverse Email or Reverse Phone callbacks | | `lead_enrichment`, `lead_enrichment_xhigh` | People Intelligence callbacks. The `xhigh` sample adds `investigation` | | `company_enrichment` | Company Intelligence callbacks | | `qa_agent` | QA Agent callbacks | | `outgoing_webhook` | The workflow `outgoing_webhook` block. Its download links are `null` | | `monitor.field.changed`, `monitor.execution.completed`, `monitor.execution.failed`, `monitor.run.completed` | [Monitor notifications](/api-reference/monitors/monitor-notifications) | Test deliveries are [rate limited](/api-reference/rate-limits#webhooks). For error responses, see [Handling Errors](/api-reference/errors). ## Production checklist * Create a signing secret in **Settings → Webhooks** and reject unsigned deliveries. * Return `2xx` within 10 seconds, including for events you ignore. * Dedupe on the job or run, as `delivery_key` does above. * Poll `GET /job-status/{task_id}` when a callback is late. Failed jobs send no callback. # Workflow Templates Source: https://docs.sixtyfour.ai/guides/workflow-templates Start a workflow from a pre-built template, or save your own workflow as a template for your organization. A template is a pre-built workflow you copy and customize instead of starting from a blank canvas. The gallery holds Sixtyfour's curated templates plus the templates your organization saves. ## Open the gallery Open [Workflows](https://app.sixtyfour.ai/workflows), click **New Workflow**, and choose **Use a template**. The gallery lives at [app.sixtyfour.ai/workflows/templates](https://app.sixtyfour.ai/workflows/templates). Search by name, description, category, tag, or block with the **Search templates, tools, and workflows...** bar, or filter by category: | Filter | Templates for | | - | - | | All Templates | Everything available to your organization | | Intelligence | Researching people and companies | | Data Enrichment | Filling missing fields on existing lists | | Recruitment | Sourcing and candidate research | | Email Campaigns | Finding verified emails for outbound | | Marketing | Campaign, audience, and competitor research | | Trust & Safety | Entity diligence, KYC/KYB, and risk research | Templates in other categories, such as Finance, Company Research, and Sales Automation, appear under All Templates. Cards marked `Popular` are featured. Cards marked `Locked` use high-tier intelligence that is not enabled for your organization; open one to book a call with the Sixtyfour team. ## Use a template 1. Click a template card. The preview shows its category, complexity, use case, and every step in the workflow. 2. Click **Use Template**. ![Template preview listing the steps in an Entity Due Diligence workflow](https://mintcdn.com/sixtyfour/UuWvY4Q3QswkqZu-/images/template-preview.png?fit=max\&auto=format\&n=UuWvY4Q3QswkqZu-\&q=85\&s=6f7db25ed8d41346568617a2fddf3996) Sixtyfour creates a new workflow in the Workflow Editor with the template's blocks and connections. Run it as is or change any block first. Edits to your copy do not change the template. For what each block does, see [Building Workflows](/guides/building-workflows). ## Create a template Save any workflow with at least one block as a template for your organization: 1. Open the workflow and select **Save as Template** from the More options menu (⋮) next to **Run**. From the Workflows list, open the row's menu and select **Save as template**. 2. Fill in the template fields. 3. Click **Create Template**. | Field | Required | Description | | - | - | - | | Template Name | Yes | Short, descriptive name, such as "Inbound Lead Enrichment". | | Description | Yes | One- or two-sentence overview shown on the card. | | When to Use This Template | Yes | The situation the template fits. Shown in the preview. | | Category | Yes | The gallery category, such as Intelligence or Recruitment. | | Complexity | Yes | `Quick`, `Standard`, or `Advanced`. | | Estimated Time | Yes | How long a typical run takes, such as "10 minutes". | | Tags | No | Search keywords such as platforms, data types, and industries. | | Feature this template | No | Shows the template at the top of the gallery for your organization. | A template captures the workflow's blocks, settings, and connections. Input files and results from past runs are not included. To change or remove one of your organization's templates, open the card's menu and select **Edit** or **Delete**. ## Tips * Write When to Use This Template as advice to a new teammate. * Save your team's proven workflows as templates so new teammates start from a working baseline. # Ashby Integration Source: https://docs.sixtyfour.ai/integrations/ashby-integration Import job applicants from Ashby into Sixtyfour workflows to enrich and rank them. Connect Ashby to Sixtyfour to import job applicants into a workflow, then enrich, score, and export them. Sixtyfour only reads from Ashby. It never changes or writes back to your Ashby data. > [!NOTE] > > The Ashby integration is in early access. Contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai) to turn it on for your organization. ## What Sixtyfour reads The API key created from the Sixty Four tile in Ashby has read access to two Ashby modules. | Ashby module | What Sixtyfour reads | | - | - | | Jobs | Open jobs, their titles, and job posting descriptions | | Candidates | Applications (status, interview stage, applied date, source), candidate profiles (name, email, phone, social links, current title, company, school, location), and résumé files | Sixtyfour reads Ashby only when you connect a key, pick jobs in the Import Data block, or run a workflow. ## Create an API key in Ashby You need Admin access in Ashby to create the key. **1. Open Admin** Sign in to [Ashby](https://app.ashbyhq.com) and click **Admin** in the top navigation bar. ![Ashby top navigation bar with Admin highlighted](https://mintcdn.com/sixtyfour/nb2qh5icG8Oz3-RM/images/ashby-admin-nav.png?fit=max\&auto=format\&n=nb2qh5icG8Oz3-RM\&q=85\&s=1a88594c4df1f7cb7f3027b14818fcc5) **2. Open Integrations** In the left panel, open **Integrations** and click **Integrations**. ![Ashby Admin Integrations page with the Integrations item highlighted in the left panel](https://mintcdn.com/sixtyfour/nb2qh5icG8Oz3-RM/images/ashby-integrations-page.png?fit=max\&auto=format\&n=nb2qh5icG8Oz3-RM\&q=85\&s=c3b0f5183741c4e4c7426fbd5e2a2893) **3. Open the Sixty Four tile** Search for **Sixty Four** and select the tile. ![Ashby Integrations page filtered to the Sixty Four tile](https://mintcdn.com/sixtyfour/nb2qh5icG8Oz3-RM/images/ashby-sixtyfour-tile.png?fit=max\&auto=format\&n=nb2qh5icG8Oz3-RM\&q=85\&s=fe95f35702fa2e2a8bb07cfb2217a558) **4. Create and copy the key** Click **Create API key for Sixty Four** and copy the key Ashby shows. You paste it into Sixtyfour in the next section. ![Sixty Four panel in Ashby with the Create API key for Sixty Four button highlighted](https://mintcdn.com/sixtyfour/nb2qh5icG8Oz3-RM/images/ashby-create-api-key.png?fit=max\&auto=format\&n=nb2qh5icG8Oz3-RM\&q=85\&s=b28179641b16b508d36ae2bf1294dee0) ## Connect the key in Sixtyfour **1. Open the Ashby integration** In the [Sixtyfour dashboard](https://app.sixtyfour.ai), go to **Settings → Integrations** and select **Ashby**. ![Sixtyfour Integrations settings page showing the Ashby card](https://mintcdn.com/sixtyfour/nb2qh5icG8Oz3-RM/images/ashby-sixtyfour-integrations.png?fit=max\&auto=format\&n=nb2qh5icG8Oz3-RM\&q=85\&s=ceba8e02a313004a55613f49741d042d) **2. Paste the key and connect** Paste the key into **API key** and click **Connect**. Sixtyfour checks that the key can read jobs and candidates before saving it. ![Ashby settings page in Sixtyfour with the API key field and Connect button highlighted](https://mintcdn.com/sixtyfour/nb2qh5icG8Oz3-RM/images/ashby-sixtyfour-connect.png?fit=max\&auto=format\&n=nb2qh5icG8Oz3-RM\&q=85\&s=0f1d027d4fa06b7c8169ba6844dcb947) The connection belongs to your current team. Every workflow in the team that imports from Ashby uses it. Sixtyfour encrypts the key with your organization's own encryption key. ## Import applicants into a workflow **1. Add an Import Data block** Open the [Workflow Editor](https://app.sixtyfour.ai/workflows), add an **Import Data** block, and set **Import from** to **Ashby · Job applicants**. ![Import Data block configured to import Ashby job applicants](https://mintcdn.com/sixtyfour/nb2qh5icG8Oz3-RM/images/ashby-import-data-block.png?fit=max\&auto=format\&n=nb2qh5icG8Oz3-RM\&q=85\&s=124db1f19e9050a595be3c6789681456) **2. Choose which applicants to import** | Setting | What it does | | - | - | | Jobs | The open jobs to import applicants from. Leave all unchecked to import from every open job. | | Application status | Active, Lead, Hired, or Archived. Defaults to Active. | | Interview stages | Comma-separated stage names, such as `Application Review, Recruiter Screen`. Leave empty for every stage. | | Applied in the last (days) | Only applicants who applied in this many days. Leave empty for any time. | | Add job description | Adds the job posting's description as `job_description`. | | Read résumés | Adds the text of each résumé PDF as `resume_text`. | | Maximum rows | Caps how many applicants the run imports. | **3. Add enrichment and output blocks** Each applicant becomes one row, so any people block can follow. A common pattern enriches applicants and sends the results to your own system: ```mermaid theme={null} flowchart LR Ashby[Import Data: Ashby] --> Enrich_People[Enrich People] --> Webhook[Outgoing Webhooks] ``` **4. Run the workflow** Run the workflow from the editor, or put it on a [schedule](/guides/automated-workflows) to pick up new applicants as they apply. ### Imported columns | Column | Description | | - | - | | `name`, `email`, `phone` | From the candidate's Ashby profile | | `linkedin`, `github`, `website` | The candidate's social links in Ashby | | `title`, `company`, `school`, `location` | The candidate's current role and background in Ashby | | `job_title` | The job the candidate applied to | | `application_status`, `interview_stage` | Where the application stands | | `applied_at`, `source` | When and how the candidate applied | | `ashby_application_id`, `ashby_candidate_id`, `ashby_job_id` | Ashby IDs, for matching results back to Ashby | | `ashby_profile_url` | Link to the candidate in Ashby | | `job_description` | Present when **Add job description** is on | | `resume_text` | Present when **Read résumés** is on | ## Disconnect Ashby * In Sixtyfour, go to **Settings → Integrations → Ashby**, click the trash icon next to the key, and confirm **Disconnect**. * In Ashby, deactivate the key under **Admin → API Credentials**. > [!WARNING] > > Workflows that import from Ashby stop until the team connects a key again. ## Troubleshooting | Message | Fix | | - | - | | Ashby rejected this team's connection | The key was deactivated in Ashby. Create a new key from the Sixty Four tile and use **Replace key** in Sixtyfour. | | The Ashby key can't read Jobs and Candidates | The key is missing a permission. Create the key from the Sixty Four tile, which grants the permissions Sixtyfour needs. | | The run imports no applicants | Check **Application status** and **Applied in the last (days)**. Only open jobs appear in the job list. | ## Block reference | Field | Value | | - | - | | Block type | `read_csv` with `specs.source.type` set to `ashby_applicants` | | Pipeline position | Source (first block) | | Requires integration | Yes, the team must have Ashby connected | | Output data type | Lead | # GitHub Integration Source: https://docs.sixtyfour.ai/integrations/github-integration Turn GitHub repositories, users, and code search results into enriched lead lists with Sixtyfour workflows. Turn GitHub contributors, stargazers, watchers, and search results into lead lists you can enrich in a Sixtyfour workflow. > [!NOTE] > > Integrations are part of the Sixtyfour platform, included on the Enterprise plan. [Talk to sales](https://cal.com/team/sixtyfour/discovery) to enable it. ## Quickstart **1. Connect your GitHub account** In the [Sixtyfour web app](https://app.sixtyfour.ai), go to **Settings → Integrations**, select **GitHub**, and authorize your account. **2. Add a GitHub block** Open the [Workflow Editor](https://app.sixtyfour.ai/workflows) and add a GitHub block: | Block | Use case | | - | - | | **GitHub Find People** | Get contributors, stargazers, or watchers of the repositories in a column | | **GitHub Search** | Search GitHub for repositories, users, or code | **3. Add enrichment and output blocks** ```mermaid theme={null} flowchart LR GFP[GitHub Find People] --> EP[Enrich People] --> FE[Email address] --> Append[Notebook] ``` **4. Run the workflow** Sixtyfour pulls profiles from GitHub and runs each one through the pipeline. ## GitHub Find People Reads repositories from a column, as URLs or `owner/repo` (for example `sixtyfour-ai/docs`), and returns one row per user. Block type: `github_find_people`. | Field | Default | Description | | - | - | - | | Repository column | Required | Column with repository URLs or `owner/repo` values | | User type | Required | `contributors`, `stargazers`, or `watchers` | | Limit per repo | `1000` | Maximum users returned per repository | | Fetch user details | `true` | Add profile details such as public email and company | | Include anonymous | `false` | Include anonymous contributors. Contributors only | | Min contributions per repo | None | Skip contributors below this count. Contributors only | ## GitHub Search Runs a GitHub search query and returns one row per result. Block type: `github_search`. | Field | Default | Description | | - | - | - | | Search type | Required | `repositories`, `users`, or `code` | | Query | Required | GitHub search syntax, for example `webrtc language:typescript stars:>20` | | Sort | Best match | Repositories: `stars`, `forks`, `updated`. Users: `followers`, `repositories`, `joined`. Code: `indexed` | | Order | `desc` | `asc` or `desc` | | Max results | `100` | Up to `1000`, the GitHub search limit | ## Example workflows ### Recruit contributors from an open-source project ```mermaid theme={null} flowchart LR GFP[GitHub Find People] --> Enrich_People[Enrich People] --> Find_Email[Email address] --> Filter --> Append[Notebook] ``` ### Find developers working with a technology ```mermaid theme={null} flowchart LR GS[GitHub Search] --> Enrich_People[Enrich People] --> Find_Email[Email address] --> Slack[Send Slack Message] ``` # Google Sheets Integration Source: https://docs.sixtyfour.ai/integrations/google-sheets-integration Export search results, notebooks, and Agent results from the Sixtyfour web app to a new Google Sheet. Export results from the Sixtyfour web app to a new Google Sheet in your Drive in one click. ## Export to Google Sheets **1. Open the export menu** In the [Sixtyfour web app](https://app.sixtyfour.ai), open the results you want to export and choose **Export to Google Sheets**. **2. Sign in with Google** The first time you export, sign in with the Google account whose Drive should hold the sheet. Sixtyfour asks only for access to files it creates, not the rest of your Drive. Sixtyfour remembers the connection for later exports. **3. Open the sheet** Sixtyfour creates a new spreadsheet with one column per result column. Click **Open sheet** in the confirmation to view it. ## What you can export | Results | Where | | - | - | | People search results | Search results view | | Company search results | Search results view | | Notebooks | Notebook page, including workflow output saved to a notebook | | Agent results | Result cards in an [Agent](/guides/using-agents) chat | Each export creates a new spreadsheet. Empty tables can't be exported. > [!NOTE] > > Google Sheets export runs in the web app. To get results programmatically, use the [API](/api-reference/introduction) or a workflow's [webhook](/guides/using-webhooks) instead. # HubSpot Integration Source: https://docs.sixtyfour.ai/integrations/hubspot-integration Import HubSpot contacts and companies into Sixtyfour workflows for enrichment and contact finding. Import HubSpot contacts or companies into a Sixtyfour workflow to enrich them, find contact details, and route the results to your team. > [!NOTE] > > Integrations are part of the Sixtyfour platform, included on the Enterprise plan. [Talk to sales](https://cal.com/team/sixtyfour/discovery) to enable it. ## Quickstart **1. Connect your HubSpot account** In the [Sixtyfour web app](https://app.sixtyfour.ai), go to **Settings → Integrations**, select **HubSpot**, and authorize your account. **2. Add the HubSpot Import block** Open the [Workflow Editor](https://app.sixtyfour.ai/workflows) and add **HubSpot Import** as the source block. **3. Configure the import** Choose the object type, then optionally pick properties and a record limit (see [Block reference](#block-reference)). **4. Add enrichment and output blocks** ```mermaid theme={null} flowchart LR HubSpot[HubSpot Import] --> Enrich_People[Enrich People] --> Find_Email[Email address] --> Filter --> Append[Notebook] ``` **5. Run the workflow** Sixtyfour imports your HubSpot records and runs each one through the pipeline. ## Block reference Block type: `hubspot_import`. | Field | Default | Description | | - | - | - | | Object type | Required | `contacts` imports people as **Lead** data; `companies` imports **Company** data | | Properties | HubSpot default properties | HubSpot properties to import as columns. Select all to import every available property | | Limit | All records | Maximum number of records to import | ## Example workflows ### Research companies and find decision-makers ```mermaid theme={null} flowchart LR HubSpot[HubSpot Import] --> Enrich_Companies[Enrich Companies] Enrich_Companies --> Extract_Leads[Extract Leads] Extract_Leads --> Find_Email[Email address] Find_Email --> Slack[Send Slack Message] ``` ### Skip records you already processed ```mermaid theme={null} flowchart LR HubSpot[HubSpot Import] --> Dedup[Deduplicate with Notebook] --> Enrich_People[Enrich People] --> Find_Email[Email address] --> Append[Notebook] ``` Use **Deduplicate with Notebook** on recurring imports so already-enriched records are not charged again. # Salesforce Integration Source: https://docs.sixtyfour.ai/integrations/salesforce-integration Import Salesforce account lists and their owners into Sixtyfour workflows to enrich them and route findings to each owner. Connect Salesforce to Sixtyfour to import a saved Account list into a workflow, then enrich the accounts and send what you find to each account owner. Sixtyfour only reads from Salesforce. It never changes or writes back to your Salesforce data. ## What Sixtyfour reads Sixtyfour signs in to Salesforce as the user who connects it and requests the `api`, `refresh_token`, and `openid` scopes. Salesforce applies that user's permissions and sharing rules, so Sixtyfour sees only what the user can see. | Salesforce data | What Sixtyfour reads | | - | - | | Account list views | The names of the saved Account list views the user can open | | Accounts | ID, name, and website of each account in the chosen list | | Account owners | The owner's user ID, name, and email | | Connected user | The user's Salesforce username, shown as the connection's label | Sixtyfour reads Salesforce only when you connect, open the Import Data block, or run a workflow. ## Before you start * A Salesforce admin installs Sixtyfour in your Salesforce org once, before anyone can connect. See [Install Sixtyfour in Salesforce](#install-sixtyfour-in-salesforce). * You need to be a team owner, or an organization admin or owner, in Sixtyfour to connect or disconnect Salesforce. Otherwise, ask one of them to complete the setup. See [Roles & Permissions](/guides/roles-and-permissions). * The Salesforce user needs the **API Enabled** permission and read access to Accounts and their owners. * Sixtyfour imports saved Account list views. Create the list in Salesforce first, with the filters you want, and make sure the connecting user can open it. * Sixtyfour signs in through `login.salesforce.com`, so connect a production or Developer Edition org. Sandbox orgs are not supported. Sixtyfour lists only the Account list views that Salesforce's API can query, so some views in the picker on the Salesforce **Accounts** tab don't appear. ![Salesforce Accounts tab with the list view picker open](https://mintcdn.com/sixtyfour/OqitwTstCHo6UGNO/images/salesforce-account-list-views.png?fit=max\&auto=format\&n=OqitwTstCHo6UGNO\&q=85\&s=668798a02ed2b68c8d6550be700c703e) ## Install Sixtyfour in Salesforce A Salesforce admin does this once for the whole Salesforce org. Until then, connecting stops at a Salesforce page that says the external client app is not installed in this org. **1. Open the install link** In Sixtyfour, go to **Settings → Integrations → Salesforce** and click **Install in Salesforce**, or open the [install link](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tbm000000tmwrAAA) directly. If you are not a Salesforce admin, click **Copy link for admin** and send the link to one. **2. Install the package** Sign in to Salesforce as an admin, click **Install**, and confirm if Salesforce asks. After the install, a Sixtyfour team owner or organization admin can connect Salesforce, signing in as any Salesforce user in the org with the **API Enabled** permission. ## Connect Salesforce **1. Open the Salesforce integration** In the [Sixtyfour dashboard](https://app.sixtyfour.ai), go to **Settings → Integrations** and select **Salesforce**. ![Sixtyfour Integrations settings page with the Salesforce card highlighted](https://mintcdn.com/sixtyfour/OqitwTstCHo6UGNO/images/salesforce-sixtyfour-integrations.png?fit=max\&auto=format\&n=OqitwTstCHo6UGNO\&q=85\&s=f9b5a8a5f0b4c8660d006b6c3243f8f1) **2. Click Connect Salesforce** ![Salesforce settings page in Sixtyfour with the Connect Salesforce button highlighted](https://mintcdn.com/sixtyfour/OqitwTstCHo6UGNO/images/salesforce-sixtyfour-connect.png?fit=max\&auto=format\&n=OqitwTstCHo6UGNO\&q=85\&s=14f84168335fadc6dad848f98ece1fd4) **3. Sign in and allow access** Sixtyfour opens the Salesforce sign-in page. Sign in as the user whose lists and permissions workflows should use, then click **Allow**. If Salesforce shows a security warning above the request, first check that the page says **Sixtyfour is asking to** and that you started from **Connect Salesforce**. Finish within 10 minutes of clicking **Connect Salesforce**. ![Salesforce Allow Access page for Sixtyfour with the Allow button highlighted](https://mintcdn.com/sixtyfour/OqitwTstCHo6UGNO/images/salesforce-allow-access.png?fit=max\&auto=format\&n=OqitwTstCHo6UGNO\&q=85\&s=f3e2d330ecb87923355ab22aa2e565cf) Salesforce returns you to Sixtyfour, and the connection shows **Connected** with the user's Salesforce username. Salesforce asks only the first time. Later connections from the same user skip this page. The connection belongs to your current team. Every workflow in the team can import with it. Sixtyfour encrypts the Salesforce tokens with your organization's own encryption key. > [!NOTE] > > Imports run as the user who connected Salesforce. A list such as **My Accounts** returns that user's accounts, not the accounts of the person running the workflow. ## Import accounts into a workflow **1. Add an Import Data block** Open the [Workflow Editor](https://app.sixtyfour.ai/workflows), add an **Import Data** block, and set **Import from** to **Salesforce**. ![Import Data block configured to import a Salesforce account list, with a preview of the accounts](https://mintcdn.com/sixtyfour/OqitwTstCHo6UGNO/images/salesforce-import-data-block.png?fit=max\&auto=format\&n=OqitwTstCHo6UGNO\&q=85\&s=570df896aca50897212cd11f4287780b) **2. Choose the list** | Setting | What it does | | - | - | | Salesforce account | The team's Salesforce connection to import with. Sixtyfour selects it when the team has only one. | | Account list | A saved Account list view. Click the refresh icon after you create or share a list in Salesforce. | | Maximum rows | Caps how many accounts a run imports. Defaults to 1,000, up to 100,000. | The block previews the first five accounts and shows how many accounts the list holds. **3. Add enrichment and output blocks** Each account becomes one Company row, so any company block can follow. A common pattern enriches the accounts and messages each owner in Slack, with **DM email column** in the [Send Slack Message](/integrations/slack-integration) block set to `owner_email`: ```mermaid theme={null} flowchart LR Salesforce[Import Data: Salesforce] --> Enrich_Company[Enrich Company] --> Slack[Send Slack Message] ``` **4. Run the workflow** Run the workflow from the editor, or put it on a [schedule](/guides/automated-workflows). Each run imports the list's current members with its current filters, so a scheduled workflow picks up new accounts as they join the list. > [!TIP] > > The Sixtyfour agent can set up the block for you. Name the Salesforce list in your request, for example: "Import my Platinum and Gold SLA Customers list from Salesforce and enrich each account." ### Imported columns | Column | Description | | - | - | | `salesforce_id` | The Salesforce Account ID | | `name`, `website` | The account's name and website in Salesforce | | `owner_id` | The Salesforce user ID of the account owner | | `owner_name`, `owner_email` | The account owner's name and email | | `salesforce_url` | Link to the account in Salesforce | ## Disconnect Salesforce * In Sixtyfour, go to **Settings → Integrations → Salesforce**, click the trash icon next to the connection, and confirm **Disconnect**. * In Salesforce, open your personal settings, go to **Advanced User Details**, and click **Revoke** next to **Sixtyfour** under **OAuth Apps**. > [!WARNING] > > Workflows using a disconnected Salesforce connection stop. If you clicked **Disconnect** in Sixtyfour, connect Salesforce again, select the new connection in each affected **Import Data** block, and save the workflows. If you only revoked access in Salesforce, use **Reconnect** on the existing Sixtyfour connection. ## Troubleshooting | Message | Fix | | - | - | | External client app is not installed in this org | A Salesforce admin hasn't installed Sixtyfour in your Salesforce org yet. See [Install Sixtyfour in Salesforce](#install-sixtyfour-in-salesforce). | | Invalid OAuth state. Please try connecting again. | More than 10 minutes passed between clicking **Connect Salesforce** and clicking **Allow**. Click **Connect Salesforce** again. | | Salesforce authorization expired or was revoked | Click **Reconnect** in **Settings → Integrations → Salesforce** and sign in again. | | Salesforce denied access | The connected user is missing the **API Enabled** permission or read access to Accounts or their owners. Ask your Salesforce admin to grant it, then reconnect. | | The Salesforce connection this block uses is not connected for the team running this workflow | The block points to a connection that was disconnected or belongs to another team. Select a current connection in the **Import Data** block and save the workflow. | | No lists found | Create an Account list view in Salesforce, or share one with the connected user, then click the refresh icon next to **Account list**. | | This Salesforce list is no longer available | The list was deleted or is no longer shared with the connected user. Choose another list. | | This list cannot be imported | Choose a saved Account list view. | | Salesforce API quota reached, or rate limit reached | Your Salesforce org reached its API request limit. Run the workflow again after the limit resets. | | The Salesforce list changed while importing | Accounts were deleted or hidden from the connected user during the run. Run the workflow again. | ## Block reference | Field | Value | | - | - | | Block type | `read_csv` with `specs.source.type` set to `salesforce_accounts` | | Pipeline position | Source (first block) | | Requires integration | Yes, the team must have Salesforce connected | | Output data type | Company | # Slack Integration Source: https://docs.sixtyfour.ai/integrations/slack-integration Post workflow results to Slack channels, DMs, and threads with the Send Slack Message block. Post each row of a workflow's results to Slack channels or DMs, with column values filled into your message. This page covers the **Send Slack Message** workflow block. To chat with Sixtyfour inside Slack, see [Install the Sixtyfour Slack Bot](/integrations/slack-bot-install). > [!NOTE] > > Integrations are part of the Sixtyfour platform, included on the Enterprise plan. [Talk to sales](https://cal.com/team/sixtyfour/discovery) to enable it. ## What you can do | Capability | Description | | - | - | | Post to channels | Send a message to one or more public or private channels | | Send DMs | Message a fixed email address, or the person whose email is in a column | | Template messages | Fill `{column_name}` placeholders with each row's values | | Mention people | `{@column_name}` mentions the Slack user whose email is in that column; `{@jane@example.com}` mentions a fixed address | | Threaded replies | Post follow-up messages as replies in the thread of the main message | | Pre-written messages | Send the text of a column as-is instead of a template | ## Quickstart **1. Connect your Slack workspace** In the [Sixtyfour web app](https://app.sixtyfour.ai), go to **Settings → Integrations**, select **Slack**, and authorize your workspace. **2. Add the Send Slack Message block** Open the [Workflow Editor](https://app.sixtyfour.ai/workflows) and add **Send Slack Message** as an output block. **3. Choose destinations** Pick one or more channels, a DM email address, or a column of DM emails. Invite the Sixtyfour app to a channel with `/invite @Sixtyfour` before posting to it. **4. Write the message** Reference columns with `{column_name}`: ```text theme={null} New lead: {name} at {company} Title: {title} Owner: {@owner_email} ``` **5. Run the workflow** Each row sends one message to each destination. ## Block reference | Field | Required | Description | | - | - | - | | Workspace | Yes | Connected Slack workspace | | Channels | One destination required | Channels to post to | | DM email | One destination required | Fixed email address to direct-message | | DM email column | One destination required | Column containing the email address to direct-message | | Message body | One message source required | Template with `{column_name}` and `{@column_name}` placeholders | | Message column | One message source required | Column containing pre-written message text | | Thread replies | No | Templates posted as replies under the main message. Requires a message body | Set at least one destination (channels, DM email, or DM email column) and one message source (message body or message column). The block type in workflow definitions is `send_slack_message`. ## Example workflow Enrich leads, find emails, keep verified addresses, and notify your sales team: ```mermaid theme={null} flowchart LR Search --> Enrich_People[Enrich People] --> Find_Email[Email address] --> Filter --> Slack[Send Slack Message] ``` # Welcome to Sixtyfour Source: https://docs.sixtyfour.ai/introduction Research agents for people and companies, through one API. Send what you know about a person or company, name the fields you want, and get back structured, sourced answers. ## Get started > [card] [Get an API key](/get-api-key) > > Create a key in the web app. > [card] [Make your first call](/api-quick-start) > > Enrich a person in one request. > [card] [Try the Playground](https://app.sixtyfour.ai/playground/) > > Send live requests from your browser. ## APIs > [card] Intelligence > > Research any person or company. > > * [People Intelligence](/api-reference/endpoint/people-intelligence) > * [Company Intelligence](/api-reference/endpoint/company-intelligence) > * [Confidence Scoring](/api-reference/endpoint/field-confidence) > * [Age Verification](/api-reference/endpoint/age-verification) > [card] Atlas > > Run deep investigations. > > * [Overview](/atlas/overview) > * [Web App](/atlas/web-app) > * [API](/atlas/api) > * [Integrations](/atlas/integrations) > [card] Lattice > > Connect records into one graph. In beta. > > * [Getting Started](/lattice/getting-started) > [card] Contact > > Find and verify emails and phones. > > * [Find Email](/api-reference/endpoint/find-email) > * [Verify Email](/api-reference/endpoint/verify-email) > * [Find Phone](/api-reference/endpoint/find-phone) > * [Reverse Email](/api-reference/endpoint/reverse-email) > * [Reverse Phone](/api-reference/endpoint/reverse-phone) > [card] Search > > Build lists of people and companies. > > * [Search Overview](/api-reference/search/search-overview) > * [Deep Search](/api-reference/search/search-endpoints) > * [Filter Search](/api-reference/search/filter-search) > * [Lookalike Search](/api-reference/search/lookalike-search) > * [Search Results](/api-reference/search/search-results) > [card] Workflows > > Run multi-step pipelines over a table. > > * [Overview](/api-reference/workflows/workflows-overview) > * [Blocks](/api-reference/workflows/workflow-blocks) > * [Schedules](/api-reference/workflows/workflow-schedules) > * [Templates](/guides/workflow-templates) > [card] Monitors > > Track fields and get notified on change. > > * [Overview](/api-reference/monitors/monitors-overview) > * [Endpoints](/api-reference/monitors/monitors-endpoints) > * [Notifications](/api-reference/monitors/monitor-notifications) > [card] Guides > > Go deeper. > > * [Batch Processing](/guides/batch-processing) > * [Using Webhooks](/guides/using-webhooks) > * [Struct & Type Casting](/guides/struct-and-type-casting) > * [Credits & Pricing](/guides/credits-and-pricing) ## Developer tools > [card] [MCP Server](/developer-tools/mcp-setup) > > Give your AI coding tools Sixtyfour's docs and API. > [card] [Agent Skills](/developer-tools/skills) > > Teach coding agents to build with Sixtyfour. # Getting Started with Lattice Source: https://docs.sixtyfour.ai/lattice/getting-started Turn rows of people and companies into one connected graph that resolves identities and maps the relationships between them. Lattice turns rows of people and companies into one connected graph. Every person, email, phone, company, place, profile and IP address in your data becomes an entity, linked by relationships such as "works at" or "has email". > [!NOTE] > > Lattice is in beta. It is available to Enterprise customers and is enabled on request. [Talk to sales](https://cal.com/team/sixtyfour/discovery) to turn it on for your organization. It does two things a spreadsheet can't: * Resolves the same person across records. An email, phone, profile URL or domain that appears in several rows becomes one entity, so those rows connect through it. Lattice merges two people when the evidence shows they are the same person, such as a shared email. When it isn't sure, it asks you under **Same entity?** instead of guessing. It never merges people on name alone. * Maps relationships. Select any entity to see everything connected to it: who shares a phone, an employer, a city or an IP address. ## What it's built for * Mapping connections. See everything tied to a person, company, email or phone across all your records, and who else shares it. * Finding clusters and rings. Find groups of "different" accounts that reuse the same phone, email, address or IP address. Use it for fraud checks, duplicate cleanup and vetting. ## Before you start * Opening and exploring a graph needs you to sign in to the [web app](https://app.sixtyfour.ai/lattice). An API key can only run a workflow that builds one. * For organizations with customer-managed encryption, Lattice needs a one-time setup. Contact [support@sixtyfour.ai](mailto:support@sixtyfour.ai) to enable it. * A graph belongs to the team whose workflow run first built it. It is listed only in that team, and only that team's runs can add to it. * Graph names are unique across your organization, so a run that names another team's graph fails. To run by API, use an API key from the graph's team. ## Build your first graph You build graphs in [workflows](/guides/building-workflows), by adding a Knowledge Graph block to the end of a workflow. 1. Open [Workflows](https://app.sixtyfour.ai/workflows) and open a workflow that produces rows of people or companies. To start a new one, add a source such as Import Data or Search. 2. Add **Knowledge Graph**, under **Send Results**, as the last block and connect it. 3. In the block, select **Create a new graph…** and type a name, such as `software-engineers-in-sf`. To add this run to a graph you already have, select that graph instead. 4. Select **Run workflow**, check the cost estimate, and start the run. 5. When the run finishes, open [Lattice](https://app.sixtyfour.ai/lattice) in the sidebar and select your graph's card. ![Knowledge Graph block at the end of a workflow](https://mintcdn.com/sixtyfour/Q-DmdzWXaUc2TwK8/images/lattice-knowledge-graph-block.png?fit=max\&auto=format\&n=Q-DmdzWXaUc2TwK8\&q=85\&s=8103b44d44240384f448444b71003c66) See [Credits & Pricing Guide](/guides/credits-and-pricing#lattice) for what the block costs. ## Explore the graph A graph opens in table view. Browse entities by type, search, filter and sort. To open graph view, select any entity to see the nodes connected to it, or select **Show all** in the top right to see the whole graph. ![Graph view with the list of entities beside it](https://mintcdn.com/sixtyfour/Q-DmdzWXaUc2TwK8/images/lattice-graph-view.png?fit=max\&auto=format\&n=Q-DmdzWXaUc2TwK8\&q=85\&s=14586b2e5f9697543a7252d1bd602cfd) Selecting a node opens its panel: | Tab | What it shows | | - | - | | Node Details | The entity's properties and its timeline, such as employers and accounts. | | Connections | Everything connected to the entity. | | Reasoning | Why Lattice connected it. | ## Example: a shared IP address cluster Use a graph to find accounts that look unrelated but reuse the same details. In this sample data, each row is one account. Four accounts signed up from the same IP address, `203.0.113.24`, and several of them also share a phone number. Neither pattern shows up when you read the rows one at a time. ![Accounts connected through a shared IP address and phone number](https://mintcdn.com/sixtyfour/Q-DmdzWXaUc2TwK8/images/lattice-shared-ip-cluster.png?fit=max\&auto=format\&n=Q-DmdzWXaUc2TwK8\&q=85\&s=eebf2ef69f676a790027b1ffacab7777) ## Run it by API Build the workflow once in the web app, starting from an Import Data block, then run it by API with a new file each time: [upload the file](/api-reference/workflows/workflows-overview#uploading-an-input-file), then run the workflow with its `handle_id`. See [Workflows](/api-reference/workflows/workflows-overview) for more on running workflows by API. ```bash cURL theme={null} # Upload the file. Returns a handle_id. curl -X POST "https://api.sixtyfour.ai/storage/csv/upload" \ -H "x-api-key: YOUR_API_KEY" \ -F "file=@people.csv" # Run the workflow with that file. Returns a job_id. curl -X POST "https://api.sixtyfour.ai/workflows/run?workflow_id=YOUR_WORKFLOW_ID" \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "specs_override": { "resource_handle_id": "HANDLE_ID" } }' # Repeat every 10 s while overall_status is queued, running or finalizing. curl "https://api.sixtyfour.ai/workflows/runs/JOB_ID/live_status" \ -H "x-api-key: YOUR_API_KEY" ``` ```python Python theme={null} import time import requests API = "https://api.sixtyfour.ai" HEADERS = {"x-api-key": "YOUR_API_KEY"} with open("people.csv", "rb") as file: upload = requests.post(f"{API}/storage/csv/upload", headers=HEADERS, files={"file": file}) upload.raise_for_status() run = requests.post( f"{API}/workflows/run", params={"workflow_id": "YOUR_WORKFLOW_ID"}, headers=HEADERS, json={"specs_override": {"resource_handle_id": upload.json()["handle_id"]}}, ) run.raise_for_status() job_id = run.json()["job_id"] while True: status = requests.get(f"{API}/workflows/runs/{job_id}/live_status", headers=HEADERS) if status.status_code in (429, 503): time.sleep(int(status.headers.get("Retry-After", "10"))) continue status.raise_for_status() overall_status = status.json()["overall_status"] if overall_status not in ("queued", "running", "finalizing"): print(overall_status) break time.sleep(10) ``` ```javascript JavaScript theme={null} import { readFile } from "node:fs/promises"; const API = "https://api.sixtyfour.ai"; const headers = { "x-api-key": "YOUR_API_KEY" }; const form = new FormData(); form.append("file", new Blob([await readFile("people.csv")]), "people.csv"); const upload = await fetch(`${API}/storage/csv/upload`, { method: "POST", headers, body: form }); if (!upload.ok) throw new Error(await upload.text()); const { handle_id } = await upload.json(); const run = await fetch(`${API}/workflows/run?workflow_id=YOUR_WORKFLOW_ID`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ specs_override: { resource_handle_id: handle_id } }) }); if (!run.ok) throw new Error(await run.text()); const { job_id } = await run.json(); while (true) { const response = await fetch(`${API}/workflows/runs/${job_id}/live_status`, { headers }); if (response.status === 429 || response.status === 503) { const seconds = Number(response.headers.get("Retry-After") ?? 10); await new Promise((resolve) => setTimeout(resolve, seconds * 1000)); continue; } if (!response.ok) throw new Error(await response.text()); const { overall_status } = await response.json(); if (!["queued", "running", "finalizing"].includes(overall_status)) { console.log(overall_status); break; } await new Promise((resolve) => setTimeout(resolve, 10000)); } ``` Starting the run returns the `job_id` you poll with: ```json theme={null} { "status": "RUNNING", "workflow_id": "7c2d9e41-5b8a-4f3e-a1d6-0e9b4c7f2a58", "job_id": "3f8b2c1e-9d4a-4f6b-8c2e-1a7b5d9e0f34" } ``` While the run is in progress, its `overall_status` is `queued`, `running` or `finalizing`. [Workflow Execution](/api-reference/workflows/workflows-execution#get-live-status) lists the final statuses. ## Questions and feedback Email [support@sixtyfour.ai](mailto:support@sixtyfour.ai) with questions or feedback.