Batch Upsert
Create or update up to 1000 lifecycle states in a single operation, with per-item result reporting.
POST /api/commerce-lifecycle-snapshots/batch-upsert
Creates or updates (upsert) up to 1000 lifecycle states in a single operation. It is the recommended way for the engine to sync an entire tenant efficiently.
Auth: Required — commerce-lifecycle-snapshot:create permission
Partial success. The request never fails wholesale because of invalid
items: each item is validated independently, the valid ones are persisted, and
the failing ones are reported in errors[] with a stable code. Always check
summary.failed and errors in the response.
Homogeneous dual identifier
Each item identifies its commerce with exactly one of commerce_id (UUID) or external_id (external code). In addition, every item in the batch must use the same identifier type — mixing commerce_id and external_id in the same batch responds 400.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
items | object[] | Yes | List of states to write (minimum 1, maximum 1000). |
Each element of items:
| Field | Type | Required | Description |
|---|---|---|---|
commerce_id | UUID | One of the two | UUID of the commerce. Mutually exclusive with external_id. |
external_id | string | One of the two | External code of the commerce (1–255 characters). Mutually exclusive with commerce_id. |
activity_generation_config_id | UUID | Yes | AGC under which the state is written. |
code | string | Yes | Retention state code (1–255 characters). Must correspond to a valid code of the tenant (user states). |
stage | enum | Yes | Stage: ACTIVATION, ENGAGEMENT or RESURRECTION. |
last_computed_at | ISO 8601 | Yes | When the engine computed this state. |
Rate limit
This endpoint is limited to 300 requests per minute per IP. On exceeding it, it responds 429 Too Many Requests with a Retry-After header; respect it to pace your sending. With full batches of 1000 items, this allows syncing up to ~300,000 states per minute.
Example
curl -X POST "https://api.reten.ai/api/commerce-lifecycle-snapshots/batch-upsert" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"external_id": "SV-001",
"activity_generation_config_id": "a1b2c3d4-0000-4000-8000-000000000001",
"code": "AT_RISK_MONTHLY",
"stage": "ENGAGEMENT",
"last_computed_at": "2026-06-10T06:00:00.000Z"
},
{
"external_id": "SV-004",
"activity_generation_config_id": "a1b2c3d4-0000-4000-8000-000000000001",
"code": "ONBOARDING",
"stage": "ACTIVATION",
"last_computed_at": "2026-06-10T06:00:00.000Z"
},
{
"external_id": "SV-003",
"activity_generation_config_id": "a1b2c3d4-0000-4000-8000-000000000001",
"code": "WIN_BACK",
"stage": "RESURRECTION",
"last_computed_at": "2026-06-10T06:00:00.000Z"
},
{
"external_id": "SV-099",
"activity_generation_config_id": "a1b2c3d4-0000-4000-8000-000000000001",
"code": "HEALTHY",
"stage": "ENGAGEMENT",
"last_computed_at": "2026-06-10T06:00:00.000Z"
}
]
}'const response = await fetch(
"https://api.reten.ai/api/commerce-lifecycle-snapshots/batch-upsert",
{
method: "POST",
headers: {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
items: [
{ external_id: "SV-001", activity_generation_config_id: "a1b2c3d4-0000-4000-8000-000000000001", code: "AT_RISK_MONTHLY", stage: "ENGAGEMENT", last_computed_at: "2026-06-10T06:00:00.000Z" },
{ external_id: "SV-004", activity_generation_config_id: "a1b2c3d4-0000-4000-8000-000000000001", code: "ONBOARDING", stage: "ACTIVATION", last_computed_at: "2026-06-10T06:00:00.000Z" },
{ external_id: "SV-003", activity_generation_config_id: "a1b2c3d4-0000-4000-8000-000000000001", code: "WIN_BACK", stage: "RESURRECTION", last_computed_at: "2026-06-10T06:00:00.000Z" },
{ external_id: "SV-099", activity_generation_config_id: "a1b2c3d4-0000-4000-8000-000000000001", code: "HEALTHY", stage: "ENGAGEMENT", last_computed_at: "2026-06-10T06:00:00.000Z" },
],
}),
},
);
const result = await response.json();import requests
response = requests.post(
"https://api.reten.ai/api/commerce-lifecycle-snapshots/batch-upsert",
headers={"x-api-key": "YOUR_API_KEY"},
json={
"items": [
{"external_id": "SV-001", "activity_generation_config_id": "a1b2c3d4-0000-4000-8000-000000000001", "code": "AT_RISK_MONTHLY", "stage": "ENGAGEMENT", "last_computed_at": "2026-06-10T06:00:00.000Z"},
{"external_id": "SV-004", "activity_generation_config_id": "a1b2c3d4-0000-4000-8000-000000000001", "code": "ONBOARDING", "stage": "ACTIVATION", "last_computed_at": "2026-06-10T06:00:00.000Z"},
{"external_id": "SV-003", "activity_generation_config_id": "a1b2c3d4-0000-4000-8000-000000000001", "code": "WIN_BACK", "stage": "RESURRECTION", "last_computed_at": "2026-06-10T06:00:00.000Z"},
{"external_id": "SV-099", "activity_generation_config_id": "a1b2c3d4-0000-4000-8000-000000000001", "code": "HEALTHY", "stage": "ENGAGEMENT", "last_computed_at": "2026-06-10T06:00:00.000Z"},
],
},
)
result = response.json()Response 200 OK
In this example, three items were written successfully and one failed (SV-099 does not exist in the tenant):
{
"summary": {
"total": 4,
"created": 1,
"updated": 2,
"failed": 1
},
"errors": [
{
"index": 3,
"identifier": "SV-099",
"error": "unresolved_commerce",
"message": "No commerce exists with external_id \"SV-099\"."
}
]
}Response fields
| Field | Type | Description |
|---|---|---|
summary.total | number | Total items submitted. |
summary.created | number | Items inserted (new states). |
summary.updated | number | Items updated (existing states modified). |
summary.failed | number | Items that failed validation and were not persisted. |
errors | object[] | Detail per failed item (empty if all succeeded). |
errors[].index | number | Position (0-based) of the item within the submitted items array. |
errors[].identifier | string | The identifier (commerce_id or external_id) of the failed item. |
errors[].error | string | Stable error code (see table below). |
errors[].message | string | Human/log-readable message. |
Per-item error codes
error | Meaning |
|---|---|
unresolved_commerce | The commerce (by commerce_id or external_id) does not exist in the tenant. |
unknown_code | The code does not correspond to any UserRetentionStatusConfig of the tenant. |
duplicate_in_batch | The (commerce, AGC) pair appears more than once in the same batch. Deduplicate before sending. |
Errors (for the whole request)
| Status | Description |
|---|---|
400 | items empty or with more than 1000 elements; an item with both or neither identifier; or a mix of commerce_id and external_id in the batch. |
429 | The limit of 300 requests/minute was exceeded. Respect the Retry-After header. |
401 | Missing or invalid API key. |
403 | The credential lacks the commerce-lifecycle-snapshot:create permission. |