Reten Docs
Estados de Ciclo de Vida de Comercios

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

CampoTipoRequeridoDescripción
itemsobject[]Lista de estados a escribir (mínimo 1, máximo 1000).

Cada elemento de items:

CampoTipoRequeridoDescripción
commerce_idUUIDUno de los dosUUID del comercio. Excluyente con external_id.
external_idstringUno de los dosCódigo externo del comercio (1–255 caracteres). Excluyente con commerce_id.
activity_generation_config_idUUIDAGC bajo la cual se escribe el estado.
codestringCódigo del estado de retención (1–255 caracteres). Debe corresponder a un código válido del tenant (estados de usuario).
stageenumEtapa: ACTIVATION, ENGAGEMENT o RESURRECTION.
last_computed_atISO 8601Cuá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

CampoTipoDescripción
summary.totalnumberTotal de items enviados.
summary.creatednumberItems insertados (estados nuevos).
summary.updatednumberItems actualizados (estados existentes modificados).
summary.failednumberItems que fallaron validación y no se persistieron.
errorsobject[]Detalle por item fallido (vacío si todos tuvieron éxito).
errors[].indexnumberPosición (base 0) del item dentro del array items enviado.
errors[].identifierstringEl identificador (commerce_id o external_id) del item fallido.
errors[].errorstringCódigo de error estable (ver tabla siguiente).
errors[].messagestringMensaje legible para humanos/logs.

Códigos de error por item

errorSignificado
unresolved_commerceEl comercio (por commerce_id o external_id) no existe en el tenant.
unknown_codeEl code no corresponde a ningún UserRetentionStatusConfig del tenant.
duplicate_in_batchEl par (comercio, AGC) aparece más de una vez en el mismo lote. Deduplica antes de enviar.

Errores (de la request completa)

StatusDescripción
400items 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.
429Se excedió el límite de 300 peticiones/minuto. Respeta el encabezado Retry-After.
401Clave de API faltante o inválida.
403La credencial no tiene el permiso commerce-lifecycle-snapshot:create.