Create a bulk customer loyalty program job.
Use this endpoint to upsert customer loyalty program memberships from NDJSON rows. The API creates a job, returns a job_id (UUID), and (for upload mode) provides an 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 (loyalty membership upsert semantics)
Each non-empty NDJSON line is a loyalty membership payload plus bulk helper fields. Rows are upserts keyed by customer_id + loyalty_program_id.
- If a matching loyalty membership exists, the row updates it.
- If no matching loyalty membership exists, the row creates it.
- 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 loyalty rows reuse the same fields and validation rules as the Published Customer Loyalty Program create and update APIs. Each membership row supports the same field set documented there (such as loyalty_status, membership_number, point_balance, and reward_balance), plus bulk helper fields like _client_row_id. See the Published Customer Loyalty Program API reference for the complete field list; anything accepted there is also accepted in bulk.
Example NDJSON payload
{"_client_row_id": "L-1", "customer_id": 104172, "loyalty_program_id": 1, "loyalty_status": 10}
{"_client_row_id": "L-2", "customer_id": 104173, "loyalty_program_id": 1, "point_balance": "100.00"}Optional: balance fields
The loyalty membership payload supports optional balance fields: adjust_point_balance, point_balance, and reward_balance. If you provide multiple balance fields, adjust_point_balance takes precedence over point_balance.
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_loyalty_program_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… | |||
