Reten Docs

Commerce Lifecycle States

Consume and update the retention lifecycle state of each commerce — dual identifiers, freshness, and behavior with unresolved data.

Lifecycle states are the current snapshot of each commerce's retention state, computed by the Reten engine. Each state answers the question: "where in the retention cycle is this commerce, under which configuration, and since when?".

This section documents the read and write contract for those states.

Capabilities

The API exposes five capabilities over the commerce-lifecycle-snapshots resource:

Authentication

Every lifecycle state endpoint accepts two authentication methods:

MethodHeadersTypical use
API Keyx-api-key: YOUR_API_KEYExternal integrators / partners. The key resolves the tenant automatically — do not send x-tenant-id.
JWTAuthorization: Bearer <token> + x-tenant-id: <tenant-id>Internal use / admin panel.

Required permissions:

PermissionCapabilities
commerce-lifecycle-snapshot:viewlist, look up by commerce, batch lookup
commerce-lifecycle-snapshot:createsingle upsert, batch upsert
activity-generation-config:viewdiscover the available AGCs (see AGC Setup)

The examples in this section use the API Key method (x-api-key), which is the expected path for an external integrator. If you consume from the internal panel with a JWT token, replace that header with Authorization: Bearer <token> + x-tenant-id: <tenant-id>.

The resource: state snapshot

A lifecycle state (snapshot) is represented with this canonical shape. The surfaces that return the "raw" resource (list, batch lookup, upsert) use exactly these fields:

{
  "id": "5d000000-0000-4000-8000-000000000001",
  "commerce_id": "c0000000-0000-4000-8000-000000000001",
  "external_id": "SV-001",
  "activity_generation_config_id": "a1b2c3d4-0000-4000-8000-000000000001",
  "code": "AT_RISK_MONTHLY",
  "label": "At risk (monthly)",
  "stage": "ENGAGEMENT",
  "last_computed_at": "2026-06-10T06:00:00.000Z",
  "state_changed_at": "2026-05-22T06:00:00.000Z"
}
FieldTypeDescription
idUUIDUnique identifier of the snapshot.
commerce_idUUIDUUID of the commerce in Reten.
external_idstring | nullExternal code of the commerce in your system (e.g. "SV-001"). Denormalized at upsert time; null if the commerce has no external code.
activity_generation_config_idUUIDAGC under which this state was computed. See AGC identifier.
codestringRaw retention state code (e.g. "AT_RISK_MONTHLY"). Corresponds to a UserRetentionStatusConfig of the tenant.
labelstringHuman-readable label of the state, derived (denormalized) from the code config. The config is the source of truth.
stageenumLifecycle stage. See stages.
last_computed_atISO 8601When the engine last computed this snapshot. See freshness.
state_changed_atISO 8601When the visible state (stage, code) last changed. See freshness.

Identifier model (dual)

A commerce can be referenced in two equivalent ways across the entire states API:

  • Reten UUID (commerce_id) — the internal identifier, e.g. c0000000-0000-4000-8000-000000000001.
  • External code (external_id) — the commerce's code in your own system, e.g. "SV-001".

Each endpoint declares which one(s) it accepts:

EndpointUUIDExternal codeRule
Look up by commerce (GET /:commerceIdentifier)The path accepts either of the two, interchangeably.
Batch lookup✅ (commerce_ids)✅ (external_ids)A single type per request (no mixing).
Batch upsert✅ (commerce_id)✅ (external_id)Homogeneous: every item in the batch uses the same type.
Single upsert✅ (commerce_id)UUID only.

Using the external code spares you from maintaining a mapping of Reten UUIDs in your system: you write and read using the same codes you already handle internally.

Lifecycle stages (stage)

The stage field takes one of three possible values:

  • ACTIVATION
  • ENGAGEMENT
  • RESURRECTION

The (stage, code) pair defines a commerce's visible state.

Freshness: last_computed_at vs state_changed_at

These are two timestamps with distinct semantics; understanding the difference is key to consuming this data correctly.

FieldWhat it answersWho controls it
last_computed_at"When was this state last recomputed?"Provided by the caller (the engine) on every upsert.
state_changed_at"Since when has the commerce been in this visible state (stage, code)?"Managed by Reten automatically.

state_changed_at rules:

  • On create, it is initialized equal to last_computed_at.
  • On update, it only changes if the (stage, code) pair differs from the one that already existed. If only the label or the last_computed_at changes but (stage, code) stays the same, state_changed_at does not move.

Example of the evolution of a single commerce:

Eventstage / codelast_computed_atresulting state_changed_at
Computation 1 (creation)ENGAGEMENT / AT_RISK_MONTHLY2026-05-22T06:00Z2026-05-22T06:00Z (initialized)
Computation 2 (same state)ENGAGEMENT / AT_RISK_MONTHLY2026-06-01T06:00Z2026-05-22T06:00Z (unchanged)
Computation 3 (stage changed)RESURRECTION / WIN_BACK2026-06-10T06:00Z2026-06-10T06:00Z (updated)

Use last_computed_at to answer "how recent is this data?" and state_changed_at for "how long has the commerce been in this state?" (e.g. "it's been at risk for 19 days").

Behavior with unresolved or stale data

Unresolved

An identifier is unresolved when no snapshot exists for it. The API groups two cases together (intentionally — the result is unobtainable either way):

  • The commerce does not exist in the tenant.
  • The commerce exists but does not yet have a computed state.

How it manifests per endpoint:

EndpointBehavior with an unresolved identifier
Batch lookupThe identifier appears in the unresolved array. The request does not fail.
Look up by commerceResponds 404 Not Found.
ListSimply doesn't appear in data (the list only returns states that exist).

Stale (old data)

The API does not flag freshness or delete old states: a snapshot lives until the engine overwrites it. "Freshness" is decided by the consumer, comparing last_computed_at against the current moment and applying its own threshold (e.g. "I consider anything computed more than 24h ago stale").

To find recent states without walking everything, list accepts the state_changed_since filter, which returns only states whose state_changed_at is after a given date.

AGC identifier and setup flow

The lifecycle state is per (commerce, AGC) pair, not unique per commerce. An "Activity Generation Config" (AGC) is the configuration under which the engine computes states; the same commerce can have a different state under each AGC.

That's why the activity_generation_config_id parameter appears in every endpoint — read and write:

  • It is optional on reads. If you omit it, you get the states across all AGCs of the commerce/tenant.
  • It matters if you want to avoid "mixing" data. Filtering by a specific AGC ensures you read only the states computed under that configuration, without intermingling results from other AGCs.
  • It is required on writes (single and batch upsert), because a state is always written under a specific AGC.

How to obtain the available AGC IDs

If you want to filter by AGC, first discover the ones that exist in your tenant with:

GET /api/activity-generation-configs

Auth: requires the activity-generation-config:view permission. Responds with an array; take the id of the AGC you care about and use it as activity_generation_config_id:

[
  {
    "id": "a1b2c3d4-0000-4000-8000-000000000001",
    "name": "B2B Retention",
    "description": "Retention cycle for wholesale customers"
  },
  {
    "id": "a1b2c3d4-0000-4000-8000-000000000002",
    "name": "Q1 Resurrection",
    "description": "First-quarter win-back campaign"
  }
]

If your tenant operates with a single AGC, you can omit activity_generation_config_id on reads and work without it. The parameter becomes important when multiple AGCs coexist and you don't want to mix their states.