Developer portal
Async jobs
How long-running API operations (exports, bulk imports, regulatory filings, evidence bundles) report progress and completion.
Operations that use this pattern
- Full tenant export (JSON or CSV, with or without attachments)
- Bulk create / update / delete of records
- Regulatory filing submissions (OSHA 300A, EPA TRI, CSRD XBRL)
- Evidence bundle generation (signed ZIP with timestamped manifest)
- Data-warehouse initial sync + backfill
- SCIM large-group provisioning
Kick off a job
Any endpoint in the async family returns 202 Accepted immediately with the job descriptor. Location also points at the status URL so you can poll directly.
POST /api/v1/exports
Authorization: Bearer <token>
Content-Type: application/json
{"format":"json","includeAttachments":true}
HTTP/1.1 202 Accepted
Location: https://app.qehsethos.com/api/v1/jobs/job-abc123
Content-Type: application/json
{
"jobId": "job-abc123",
"statusUrl": "https://app.qehsethos.com/api/v1/jobs/job-abc123",
"webhookUrl": null
}Poll for progress
Status responses always follow this shape. Non-terminal responses include a Retry-After header (seconds) so clients can back off politely. Terminal states omit it.
GET /api/v1/jobs/job-abc123
HTTP/1.1 200 OK
Retry-After: 5
Cache-Control: no-store
{
"jobId": "job-abc123",
"status": "running", // queued | running | succeeded | failed | cancelled
"progress": 42, // 0-100; optional
"enqueuedAt": "2026-04-01T12:00:00Z",
"startedAt": "2026-04-01T12:00:03Z"
}When status is succeeded, the response includes a result field with the typed payload. When failed, it includes a structured error: { code, message } object.
Subscribe to completion (webhook)
Skip polling entirely by supplying a webhookUrl on the originating request. We POST the same status snapshot once when the job reaches a terminal state. Webhook requests are signed per the webhook signing docs so you can verify authenticity.
Cancel a job in flight
DELETE /api/v1/jobs/job-abc123
HTTP/1.1 200 OK
{
"jobId": "job-abc123",
"status": "cancelled",
"enqueuedAt": "2026-04-01T12:00:00Z",
"finishedAt": "2026-04-01T12:00:47Z"
}Cancellation is best-effort. Jobs already in a terminal state return their final snapshot unchanged. Partially-completed work is documented per operation, exports, for instance, leave partial output available for download; bulk imports roll back the transaction.
SDK helpers do this for you
Every published SDK provides sdk.jobs.await(jobId) (polls to completion and returns the result) and sdk.jobs.stream(jobId) (async generator yielding progress snapshots). You rarely touch the raw status endpoint unless building a UI.