Create a bulk customer import job.
Use this endpoint to import (create/update) many customers from an NDJSON payload. The API creates a job, returns a job_id (UUID), and (for upload mode) provides a presigned upload_url where you upload the NDJSON file.
Basic workflow (upload mode)
- POST this endpoint to create a job.
- Upload NDJSON to
upload_url. - POST the
startaction to queue processing. - Poll status / download results via the Bulk Jobs API.
Inline mode
Pass ingest_mode=1 with rows in inline_payload. The job queues automatically—no upload or start call required.
Store binding
Bulk jobs are bound to the store specified in the X-Active-Store header during job creation. This header is required for all bulk job operations. The store context is used for:
- Permission checks during job creation and execution
- Stamping created/updated records with the appropriate store identifier
Important: When starting an upload-mode job, you must use the same X-Active-Store value that was provided during job creation. If a different store is specified, the start request will be rejected with a 400 error (job_store_mismatch).
For status checks, cancellation, and results retrieval, any store with appropriate permissions may be used.
NDJSON rows (upsert semantics)
Each non-empty NDJSON line is a customer payload plus bulk helper fields.
- If
customer_idis omitted, the row is treated as a create. - If
customer_idis provided, the row is treated as an update. - Each row MUST include
_client_row_id, a required client-supplied identifier used to correlate input rows to results; this value is echoed back on every results NDJSON row. - Delete rows (
_action='delete') are not supported for this endpoint.
Bulk customer rows reuse the same fields and validation rules as the Published Customer Create and Update APIs. Create rows (without customer_id) support the full Published Customer create field set; update rows (with customer_id) support the full Published Customer update field set. See the Published Customer API reference for the complete field list; anything accepted there is also accepted in bulk.
To clear a field on update, set it explicitly to null or "". Omitting a field leaves it unchanged.
Example NDJSON payload
{"_client_row_id": "CUST-1", "customer_number": "BULK-10005", "first_name": "John", "last_name": "Doe", "email": "[email protected]"}
{"_client_row_id": "CUST-2", "customer_id": 104172, "customer_number": "BULK-10005", "email": "[email protected]"}Results file format
When processing completes, download the results NDJSON from results_url. Each line is a JSON object with the following fields:
| Field | Description |
|---|---|
_client_row_id | Required client-supplied identifier, echoed from input. Use to correlate results to input rows. |
status | Row-level status code (see Status Codes table below). |
action_taken | Database action code (see Action Taken Codes table below). |
<entity_id> | The entity's primary key (e.g., customer_id). Present on success rows and on error rows where the conflicting record can be identified (for example, when a business-rule failure references an existing record). |
errors | Array of {field, code, message} objects. Present on error rows (see Error Codes table below). |
Status codes
The status field uses these integer codes:
| Code | Label | Description |
|---|---|---|
| 0 | SUCCESS | Row processed successfully |
| 1 | ERROR | Row failed validation or processing |
| 2 | CANCELLED | Row processing was cancelled |
Action taken codes
The action_taken field uses these integer codes:
| Code | Label | Description |
|---|---|---|
| 0 | INSERTED | New record created |
| 1 | UPDATED | Existing record updated |
| 2 | DELETED | Existing record deleted |
| 3 | NONE | No database change (typically due to error) |
Error codes
The code field in error objects uses these integer codes:
| Code | Label | Description |
|---|---|---|
| 0 | REQUIRED | Required field missing |
| 1 | INVALID | Invalid value or format |
| 2 | BUSINESS_RULE | Business logic constraint violated |
| 3 | UNIQUE | Uniqueness constraint violated |
| 4 | INVALID_SCHEMA | Schema or structure error |
| 5 | PERMISSION | Permission denied |
| 6 | NOT_FOUND | Referenced entity not found |
Example result rows
{"_client_row_id": "ROW-1", "status": 0, "action_taken": 0, "customer_id": 104200}
{"_client_row_id": "ROW-2", "status": 1, "action_taken": 3, "errors": [{"field": "email", "code": 1, "message": "Enter a valid email address."}]}Optional: callback authentication
If you provide callback_url, the system will POST a completion callback when the job reaches a terminal state. To authenticate callbacks, provide callback_secret in the create request. When set, callbacks include HMAC signature headers:
X-ERC-Callback-Timestamp: Unix epoch secondsX-ERC-Callback-Signature:v1=<hex>where the hex value is an HMAC-SHA256 over<timestamp>.<canonical_body>
Receivers should validate timestamp freshness and verify the signature before trusting the payload. Signature verification is performed against a canonical JSON form; do not compute the HMAC over the raw HTTP body. Use the following algorithm:
timestamp = request.headers['X-ERC-Callback-Timestamp']
signature = request.headers['X-ERC-Callback-Signature'] # 'v1=<hex>'
payload = JSON.parse(request.body)
canonical_body = JSON.stringify(payload, sort_keys=true, separators=(',', ':'))
expected_hex = HMAC_SHA256_HEX(secret, timestamp + '.' + canonical_body)
expected = 'v1=' + expected_hex
compare(expected, signature) # timing-safe compareError responses
- 400
invalid_schema: Request body failed validation. - 401
authentication_required: Missing or invalid authentication. - 403
permission_denied: API key lacks required bulk job permission. - 429
job_limit_reached: Tenant has reached maximum active bulk jobs.
| Time | Status | User Agent | |
|---|---|---|---|
Retrieving recent requests… | |||
