Async Jobs
Run a deterministic capability over a large batch outside the request/response cycle. Submit once, get a job handle back immediately, and poll for the result when it's ready.
When to use jobs
The synchronous endpoints are ideal for interactive, request-sized payloads. Reach for a job when a batch is large enough that a single request would be slow or risk timing out. A job accepts up to 10,000 records and runs one capability: normalize, transform, validate, dedupe, or anomalies.
1. Submit the job
POST /api/v1/jobs returns 202 Accepted with a queued job and a status_url. The whole batch is billed here, up front, against your account-level monthly record allowance — so if it would exceed your remaining allowance the submit returns 429 and nothing is queued or metered.
POST /api/v1/jobs
Authorization: Bearer zap_live_...
Content-Type: application/json
{
"capability": "dedupe",
"records": [
{ "email": "billing@acme.com", "company": "Acme Incorporated" },
{ "email": "billing@acme.com", "company": "ACME Inc." }
],
"params": { "threshold": 0.9 }
}HTTP/1.1 202 Accepted
{
"request_id": "req_xxx",
"job": {
"id": "job_2b8c1a7b3e5d4c8f0a1b2c3d",
"status": "queued",
"capability": "dedupe",
"record_count": 2,
"status_url": "/api/v1/jobs/job_2b8c1a7b3e5d4c8f0a1b2c3d"
},
"usage": { "credits_used": 2, "credits_remaining": 998 }
}2. Poll for the result
GET /api/v1/jobs/{id} returns the current status. It is read-only and never metered, so you can poll as often as your rate limit allows. When status is completed the full result is included; on failed an error message is returned instead.
GET /api/v1/jobs/job_2b8c1a7b3e5d4c8f0a1b2c3d
Authorization: Bearer zap_live_...{
"request_id": "req_yyy",
"job": {
"id": "job_2b8c1a7b3e5d4c8f0a1b2c3d",
"status": "completed",
"capability": "dedupe",
"record_count": 2,
"attempts": 1,
"result": {
"capability": "dedupe",
"records_processed": 2,
"result": { "duplicate_groups": [{ "records": [0, 1], "confidence": 0.96 }] }
},
"error": null,
"created_at": "2026-09-08T12:00:00.000Z",
"started_at": "2026-09-08T12:00:03.000Z",
"completed_at": "2026-09-08T12:00:03.400Z"
}
}Status lifecycle
queued— accepted and waiting to be processedprocessing— currently runningcompleted— finished;resultis populatedfailed— retried and exhausted its attempts;errorexplains why
A transient failure is retried automatically before a job is marked failed. Jobs are strictly scoped to your organization: a job id that belongs to another account is indistinguishable from one that does not exist.
With the SDK
The JavaScript SDK wraps submit-and-poll in waitForJob, so you can await a batch to completion without writing the polling loop yourself.
import { Zapinner } from "@zapinner/sdk"
const zap = new Zapinner({ apiKey: process.env.ZAPINNER_API_KEY })
// Submit and poll to completion in one call.
const { job } = await zap.submitJob({
capability: "dedupe",
records,
params: { threshold: 0.9 },
})
const finished = await zap.waitForJob(job.id, { intervalMs: 2000, timeoutMs: 60000 })
if (finished.job.status === "completed") {
console.log(finished.job.result)
}