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.

Request
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 }
}
202 Accepted
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.

Request
GET /api/v1/jobs/job_2b8c1a7b3e5d4c8f0a1b2c3d
Authorization: Bearer zap_live_...
200 OK
{
  "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 processed
  • processing — currently running
  • completed — finished; result is populated
  • failed — retried and exhausted its attempts; error explains 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.

job.ts
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)
}