Reten Docs

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

FieldTypeRequiredDescription
itemsobject[]YesList of states to write (minimum 1, maximum 1000).

Each element of items:

FieldTypeRequiredDescription
commerce_idUUIDOne of the twoUUID of the commerce. Mutually exclusive with external_id.
external_idstringOne of the twoExternal code of the commerce (1–255 characters). Mutually exclusive with commerce_id.
activity_generation_config_idUUIDYesAGC under which the state is written.
codestringYesRetention state code (1–255 characters). Must correspond to a valid code of the tenant (user states).
stageenumYesStage: ACTIVATION, ENGAGEMENT or RESURRECTION.
last_computed_atISO 8601YesWhen 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

FieldTypeDescription
summary.totalnumberTotal items submitted.
summary.creatednumberItems inserted (new states).
summary.updatednumberItems updated (existing states modified).
summary.failednumberItems that failed validation and were not persisted.
errorsobject[]Detail per failed item (empty if all succeeded).
errors[].indexnumberPosition (0-based) of the item within the submitted items array.
errors[].identifierstringThe identifier (commerce_id or external_id) of the failed item.
errors[].errorstringStable error code (see table below).
errors[].messagestringHuman/log-readable message.

Per-item error codes

errorMeaning
unresolved_commerceThe commerce (by commerce_id or external_id) does not exist in the tenant.
unknown_codeThe code does not correspond to any UserRetentionStatusConfig of the tenant.
duplicate_in_batchThe (commerce, AGC) pair appears more than once in the same batch. Deduplicate before sending.

Errors (for the whole request)

StatusDescription
400items 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.
429The limit of 300 requests/minute was exceeded. Respect the Retry-After header.
401Missing or invalid API key.
403The credential lacks the commerce-lifecycle-snapshot:create permission.