Guides
Contact jobs
POST /v1/contact-retrieval-jobs retrieves contacts for up to 500 people in one bounded, durable job. You get a free estimate first if you want one, then a job id to read or a webhook to receive.
Estimate before you spend
Send the same body with quote_only: true and nothing is persisted, nothing is held, and nothing is charged. The answer prices the work per component after deduplication, so it is the number to show a user before they commit.
{
"items": 2,
"components": 3,
"reserved_microunits_estimate": "3000",
"lines": [
{
"meter": "contact_email.v1",
"units": 2,
"unit_price_microunits": "1000",
"total_microunits": "2000"
},
{
"meter": "contact_phone.v1",
"units": 1,
"unit_price_microunits": "1000",
"total_microunits": "1000"
}
],
"omitted": { "duplicates": 0, "not_servable": 0 }
}Amounts in these samples are placeholders, not rates. Pricing is consumption-based, so you only pay for data you get back, and it is shown to signed-in accounts under Pricing in the console.
An estimate is an estimate: a person whose contacts your organization already holds costs nothing when the job actually runs, and a channel the provider cannot answer is never charged.
Create the job
Without quote_only the same request creates the job. It answers 202 with a Job once acceptance is durable — the reservation is taken inline, so a 402 arrives synchronously and no job exists.
curl --request POST "https://developer-api.seekout.io/v1/contact-retrieval-jobs" \
--header "Authorization: Bearer $SEEKOUT_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen)" \
--data '{
"items": [
{ "person_id": "person_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "channels": ["email"] },
{ "person_id": "person_ccccccccccccccccccccccccccccccccccccccccccc", "channels": ["email", "phone"] }
]
}'The answer is the job. reserved_microunits is what is held against your balance while it runs; charged_microunits grows only as components are delivered, and whatever is not delivered is released.
HTTP/1.1 202 Accepted
{
"job_id": "01JEXAMPLEJOB0000000000000",
"type": "contact_retrieval",
"status": "queued",
"counts": { "requested": 3, "delivered": 0, "pending": 3 },
"reserved_microunits": "3000",
"charged_microunits": "0",
"result_available": false,
"result_url": "/v1/job-results/01JEXAMPLEJOB0000000000000"
}What a job bounds
| Bound | Value | Over it |
|---|---|---|
| Items per job | 500 | 422 — send fewer rows |
| Running jobs per organization | 2 | 429 contact_job_concurrency_cap with Retry-After |
| Job lifetime | 30 minutes | The job ends expired; delivered components are charged and the rest are released |
| Result retention | 7 days | 409 result_set_expired — create the job again |
Rows are deduplicated per (person, channel) before anything is priced or reserved, and the answer reports what it dropped in omitted.duplicates. Asking for the same pair twice never pays twice.
Read the job
GET /v1/jobslists your own jobs, newest first, filterable bytypeandstatusand paged withcursorandlimit.GET /v1/jobs/{job_id}is one job: its status, counts, reserved and charged microunits, and whether a result is available yet.GET /v1/job-results/{job_id}is the items — one entry per(person, channel)with its values,reused, and its own money fact. Reading it again never charges.POST /v1/jobs/{job_id}/cancelstops what has not started: delivered components settle and everything else is released.
Every pending single-person retrieval is readable through the same two reads, using the job_id its 202 carried — one Jobs API for both shapes (see contacts).
Job statuses
| Status | Meaning | Result readable |
|---|---|---|
queued | Accepted and funded; not started yet | No |
running | Components are with the provider | No |
completed | Every component resolved | Yes |
partial | Some components resolved and some did not | Yes |
failed | No component could be delivered | No |
cancelling | A cancel was requested and is converging | No |
cancelled | Cancelled; delivered components were charged | Yes |
expired | The 30-minute deadline passed | Yes |
GET /v1/job-results/{job_id} answers 409 job_not_finished until the job is terminal — it never returns a half-built page. And a job id belonging to another workspace is 404 job_not_found, exactly as an absent one is, so a job id is never a way to learn what exists.Estimates are free, so are all four reads and the cancel. Only delivered components charge — see credits.