Bulk operations & jobs
Four endpoints accept batches. Below the sync cap they answer inline; above it — or whenever you ask — they return a job you poll. Always handle both shapes: the same endpoint can do either.
| Endpoint | Sync cap | Async cap | Job operation |
|---|---|---|---|
| POST /products/bulk | 50 | 5,000 | product_create |
| POST /inventory/bulk-update | 100 | 5,000 | inventory_update |
| POST /orders/bulk-ship | 50 | 1,000 | order_ship |
| POST /orders/bulk-cancel | 50 | 1,000 | order_cancel |
The job mechanism below applies to all four bulk operations. Choose sync or async per batch size and retry requirements.
Choosing sync or async
A request runs asynchronously when either of these is true:
- The batch is larger than the sync cap.
- You send
Prefer: respond-async— this works at any size, so you can opt into the job flow even for a two-item batch.
Send an Idempotency-Key on every bulk submission. It is optional on
POST /products/bulk and POST /inventory/bulk-update but strongly recommended: it lets a
retried submission replay the original job instead of creating a duplicate batch. Reusing a key with a different batch
returns 422 IDEMPOTENCY_KEY_REUSED. Exceeding the async cap returns 400 VALIDATION_ERROR.
The async submission response
Async submissions return HTTP 202 with two headers that tell you where and when to poll:
HTTP/1.1 202 Accepted
Location: /open/v1/jobs/job_01HXYZ…
Retry-After: 5
{
"success": true,
"data": {
"job_id": "job_01HXYZ…",
"status": "pending",
"location": "/open/v1/jobs/job_01HXYZ…"
}
}
Follow Location and wait Retry-After seconds before the first poll. A response that is not
202 means your batch ran synchronously — read the inline result instead.
Polling a job
{
"success": true,
"data": {
"job_id": "job_01HXYZ…",
"operation": "product_create",
"status": "partial_success",
"summary": {
"total": 500,
"succeeded": 486,
"failed": 12,
"outcome_unknown": 2
},
"items": [
{ "index": 0, "status": "succeeded",
"resource_id": "prod_abc123" },
{ "index": 14, "status": "failed",
"error": { "code": "CATEGORY_NOT_FOUND", "message": "Category does not exist" } }
],
"pagination": { "offset": 0, "limit": 100 }
}
}
index is the position in the array you submitted, so results map back to your input.
resource_id appears on successful creates; result carries an operation-specific payload where
one exists; error appears on failures.
Job status values
| Status | Terminal | Meaning |
|---|---|---|
| pending | no | Accepted, not yet started |
| processing | no | Running. Keep polling |
| completed | yes | Every item succeeded |
| partial_success | yes | Some items failed. Read items — a 200 here does not mean your batch worked |
| failed | yes | Every item failed, or the job could not run |
Item status values
Individual entries in items use a different set from the job as a whole. Do not reuse
one enum for both.
| Item status | Meaning |
|---|---|
| pending | Not yet attempted |
| processing | In flight |
| succeeded | Applied. resource_id is present for creates |
| failed | Rejected. error.code tells you why |
| outcome_unknown | Submitted, result unconfirmed — see below |
outcome_unknown needs explicit handling. These items were submitted but the platform could not
confirm whether they took effect — typically a timeout while the write was being confirmed. They are neither
successes nor failures. Reconcile each by reading the resource directly before retrying, or you risk duplicating it.
Reading results
items is paginated via offset and limit (maximum 100). For a large
batch, page until you have read summary.total entries. Items are indexed against the order you submitted
them, so index maps back to your original array.
Jobs run to completion. There is no cancel endpoint — once submitted, a job progresses to a terminal state on its own. Poll it to read the outcome, and use item-level results to decide what to retry.
Polling guidance
- Wait the
Retry-Aftervalue from the 202 (currently 5 seconds) before the first poll, then back off to a 5–10 second interval. Job polls draw on your rate limit like any other call. - Stop when
statusis terminal. Do not poll on a fixed timer forever. - Persist the
job_idat submission. It is the only way to recover the outcome if your process restarts.