Upsert por Lote
Crea o actualiza hasta 1000 estados de ciclo de vida en una sola operación, con reporte de resultado por item.
POST /api/commerce-lifecycle-snapshots/batch-upsert
Crea o actualiza (upsert) hasta 1000 estados de ciclo de vida en una sola operación. Es la vía recomendada para que el motor sincronice un tenant completo de forma eficiente.
Auth: Requerida — permiso commerce-lifecycle-snapshot:create
Éxito parcial. La request nunca falla en bloque por items inválidos: cada
item se valida de forma independiente, los válidos se persisten, y los que
fallan se reportan en errors[] con un código estable. Revisa siempre
summary.failed y errors en la respuesta.
Identificador dual homogéneo
Cada item identifica su comercio con exactamente uno de commerce_id (UUID) o external_id (código externo). Además, todos los items del lote deben usar el mismo tipo de identificador — mezclar commerce_id y external_id en un mismo lote responde 400.
Cuerpo de la Petición
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
items | object[] | Sí | Lista de estados a escribir (mínimo 1, máximo 1000). |
Cada elemento de items:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
commerce_id | UUID | Uno de los dos | UUID del comercio. Excluyente con external_id. |
external_id | string | Uno de los dos | Código externo del comercio (1–255 caracteres). Excluyente con commerce_id. |
activity_generation_config_id | UUID | Sí | AGC bajo la cual se escribe el estado. |
code | string | Sí | Código del estado de retención (1–255 caracteres). Debe corresponder a un código válido del tenant (estados de usuario). |
stage | enum | Sí | Etapa: ACTIVATION, ENGAGEMENT o RESURRECTION. |
last_computed_at | ISO 8601 | Sí | Cuándo el motor calculó este estado. |
Límite de tasa (rate limit)
Este endpoint está limitado a 300 peticiones por minuto por IP. Al excederlo responde 429 Too Many Requests con un encabezado Retry-After; respétalo para regular el ritmo de envío. Con lotes llenos de 1000 items esto permite sincronizar hasta ~300.000 estados por minuto.
Ejemplo
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()Respuesta 200 OK
En este ejemplo, tres items se escribieron correctamente y uno falló (SV-099 no existe en el tenant):
{
"summary": {
"total": 4,
"created": 1,
"updated": 2,
"failed": 1
},
"errors": [
{
"index": 3,
"identifier": "SV-099",
"error": "unresolved_commerce",
"message": "No existe un comercio con external_id \"SV-099\"."
}
]
}Campos de la respuesta
| Campo | Tipo | Descripción |
|---|---|---|
summary.total | number | Total de items enviados. |
summary.created | number | Items insertados (estados nuevos). |
summary.updated | number | Items actualizados (estados existentes modificados). |
summary.failed | number | Items que fallaron validación y no se persistieron. |
errors | object[] | Detalle por item fallido (vacío si todos tuvieron éxito). |
errors[].index | number | Posición (base 0) del item dentro del array items enviado. |
errors[].identifier | string | El identificador (commerce_id o external_id) del item fallido. |
errors[].error | string | Código de error estable (ver tabla siguiente). |
errors[].message | string | Mensaje legible para humanos/logs. |
Códigos de error por item
error | Significado |
|---|---|
unresolved_commerce | El comercio (por commerce_id o external_id) no existe en el tenant. |
unknown_code | El code no corresponde a ningún UserRetentionStatusConfig del tenant. |
duplicate_in_batch | El par (comercio, AGC) aparece más de una vez en el mismo lote. Deduplica antes de enviar. |
Errores (de la request completa)
| Status | Descripción |
|---|---|
400 | items vacío o con más de 1000 elementos; un item con ambos o ningún identificador; o mezcla de commerce_id y external_id en el lote. |
429 | Se excedió el límite de 300 peticiones/minuto. Respeta el encabezado Retry-After. |
401 | Clave de API faltante o inválida. |
403 | La credencial no tiene el permiso commerce-lifecycle-snapshot:create. |