# Diamond automations

A Diamond is a marketing automation built around three lessons and an offer.
It follows a lead from sign-up to purchase, records the stage the lead is in,
and, on every stage transition, delivers an event to the trigger configured
for that stage (HTTP, WhatsApp, SMS, or email). The public term is always
`diamond`: no field, permission, or message uses `funnel`.

All endpoints live under `/automations/diamond`, because `diamond` is one of
the automation types an account can have. If Diamond automations are not
enabled for the account, every endpoint in this group returns
`409 feature_disabled`, never `200` with an empty list.

For the full schemas, see the [Diamonds reference](https://cademi.dev/api/reference/diamonds.md).

## Diamonds

`GET /automations/diamond` lists the account's Diamonds. It requires
`diamonds.read`, uses cursor pagination, and accepts the `product_id`,
`showcase_id`, `status`, and `deleted` filters.
`GET /automations/diamond/{diamond_id}` returns a single Diamond, with an
`ETag` for its current revision:

```json
{
  "object": "diamond",
  "id": "dmd_42",
  "type": "diamond",
  "name": "September launch",
  "status": "active",
  "product_id": "prd_501",
  "showcase_id": "shw_7",
  "delivery_id": "dlv_900",
  "lesson_ids": ["les_11", "les_12", "les_13"],
  "settings": {
    "steps_interval": {
      "class_1_not_initiated": 60,
      "class_2_not_initiated": 120,
      "class_3_not_initiated": 180,
      "checkout_not_initiated": 60,
      "checkout_abandoned": 60
    },
    "expected_delivery": "dlv_777"
  },
  "memberships_count": 318,
  "deleted": false,
  "revision": "...",
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-20T12:00:00Z"
}
```

`delivery_id` is the Diamond's internal delivery, which grants free access to
the three lessons. Do not confuse it with `settings.expected_delivery`: that
is the delivery of the actual offer, and purchasing it moves the lead to
`won`. `steps_interval` lists only the time-based stages, with the wait time
in minutes. A stage you never configured shows its default value.

`POST /automations/diamond` creates a complete Diamond from a showcase:
`{showcase_id, name?, type: "diamond"}`. Send an `Idempotency-Key` header.
Besides the Diamond itself, the operation creates its product, three
lessons, internal delivery, and eleven triggers. Credentials scoped to that
showcase can access all of them. Because the operation creates products and
deliveries, it requires `products.create` and `deliveries.create` in
addition to `diamonds.create`: permission to create automations alone does
not grant permission to create products or deliveries.

`PATCH /automations/diamond/{diamond_id}` requires `diamonds.update` and
accepts `name`, `settings.steps_interval`, and `status` (`active` or
`inactive`). Send the current `ETag` in `If-Match`. `steps_interval` is a
partial update: send only the time-based stages you want to change. Changing
`status` also requires `diamonds.activate`, because activating or
deactivating a Diamond changes what leads receive.

`DELETE /automations/diamond/{diamond_id}` requires `diamonds.delete` and
moves the Diamond to the trash. It returns `409 state_conflict` while the
Diamond has any membership, and there is no parameter to force deletion:
triggers and scheduled communications remain in place after deletion, so
deleting a Diamond that still has leads would keep sending messages from an
automation you consider removed.

## Stages

`GET /automations/diamond/{diamond_id}/stages` requires `diamonds.read` and
always returns the Diamond's eleven fixed stages, in order. If a stage's
trigger is missing, the stage is still returned, with `trigger_id` set to
`null`:

```json
{
  "object": "stage",
  "id": "class_1_not_initiated",
  "position": 1,
  "kind": "class",
  "lesson_id": "les_11",
  "interval_minutes": 60,
  "trigger_id": "trg_103",
  "memberships_count": 42
}
```

`id` is the stage's stable literal value, which automations already rely on.
Each stage belongs to one of four `kind` groups: `lead`, `class` (six
stages: not initiated and concluded for each of the three lessons),
`checkout` (not initiated, initiated, and abandoned), and `won`. The set of
stages is fixed: you cannot create or delete stages.

`PATCH /automations/diamond/{diamond_id}/stages/{stage_id}` is the only write
available on a stage. It requires `diamonds.update` and sets the waiting
interval, in minutes, of a time-based stage: `{"interval_minutes": 30}`.
Stages share the Diamond's revision, so send the **Diamond's** `ETag` in
`If-Match`. The time-based stages are `class_1_not_initiated`,
`class_2_not_initiated`, `class_3_not_initiated`, `checkout_not_initiated`,
and `checkout_abandoned`.

Only these interval values are accepted: `15`, `30`, `60`, `120`, `360`,
`720`, `1440`, and `2880`. The same values apply to
`settings.steps_interval`. Any other value returns `422 validation_failed`
instead of silently scheduling something other than what you asked for. The
change applies to future scheduling; communications already scheduled for
leads are not affected.

## Triggers

Each of the eleven stages has one trigger, created together with the
Diamond. A trigger defines where the communication for that stage is sent.

`GET /automations/diamond/{diamond_id}/triggers` requires `diamonds.read` and
lists all eleven. `GET .../triggers/{trigger_id}` returns one, with an `ETag`
for its current revision:

```json
{
  "object": "trigger",
  "id": "trg_103",
  "diamond_id": "dmd_42",
  "stage_id": "class_1_not_initiated",
  "event_type": "spyhub.diamond.class_1_not_initiated",
  "channel": "http",
  "destination": { "url": "https://automation.example.com/***" },
  "enabled": true,
  "revision": "...",
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-01T10:00:00Z"
}
```

`destination` is always masked: HTTP destinations show only the scheme and
host, and phone destinations show only the last four digits. Neither the list
nor the retrieve operation returns the full destination, and `email`
destinations are returned as `null`. `event_type` has the form
`spyhub.diamond.{stage}` and never changes, because external automations
already recognize it.

`POST /automations/diamond/{diamond_id}/triggers` requires
`diamonds.manage_triggers` and an `Idempotency-Key` header. It **ensures**
that the trigger for a stage exists: `{"stage_id": "lead"}`. It does not
create free-form triggers, because the set of stages is fixed. Normally the
response is `200` with the existing trigger; `201` is returned only if the
stage's trigger was missing and had to be created.

`PATCH .../triggers/{trigger_id}` requires `diamonds.manage_triggers`. Send
the trigger's current `ETag` in `If-Match`. It configures `enabled`,
`channel` (`http`, `whatsapp`, `sms`, or `email`), `destination` (`{url}`,
`{phone}`, or `{email}`, matching the channel), and `template`. HTTP
destinations go through the same public-host check as
[webhooks](https://cademi.dev/api/webhooks.md): private, loopback, and metadata addresses are
rejected with `422 validation_failed`. Triggers cannot be deleted: to turn a
stage off, set `enabled: false` or clear the destination. The change applies
to future communications with leads; nothing is sent when you update the
trigger.

## Memberships (leads)

A membership is a lead inside a Diamond.

`GET /automations/diamond/{diamond_id}/memberships` lists the leads. It
requires `diamonds.read`, uses cursor pagination, and accepts the `user_id`,
`stage_id`, and `updated_after` filters.
`GET .../memberships/{membership_id}` returns one, with an `ETag`:

```json
{
  "object": "membership",
  "id": "mbr_9001",
  "diamond_id": "dmd_42",
  "user_id": "usr_777",
  "stage_id": "class_1_not_initiated",
  "stage_position": 1,
  "progress": { "confirmed_stages": ["lead"] },
  "pending": { "next_check_at": "2026-09-23T14:00:00Z", "queued_events": 1 },
  "last_run": null,
  "enrollment_id": "enr_555",
  "revision": "...",
  "created_at": "2026-09-23T10:00:00Z",
  "updated_at": "2026-09-23T13:00:00Z"
}
```

`progress.confirmed_stages` is the lead's actual progress: a stage appears
there once its transition is confirmed, which is also when its
communication is sent. `pending` shows what is scheduled for the lead.
`last_run`, when not `null`, is the most recent failed communication for
this membership. Failed communications are not retried automatically; see
[Processing attempts](#processing-attempts). `enrollment_id` is the
enrollment that grants the lead access to the Diamond's internal delivery.

`POST /automations/diamond/{diamond_id}/memberships` adds a user to the
Diamond as a lead: `{"user_id": "usr_777"}`. It requires
`diamonds.manage_memberships` and `enrollments.create`, because the same
operation also enrolls the user in the internal delivery. Send an
`Idempotency-Key` header. The response is `201` for a new lead and `200` if
the user is already a member of this Diamond; in that case, no communication
is sent again. A user can belong to only one Diamond at a time: if the user
already belongs to **another** Diamond, the API returns
`409 state_conflict` with the `already_in_diamond` reason.

`PATCH .../memberships/{membership_id}` requires
`diamonds.manage_memberships` and moves the lead to another stage manually.
Send the membership's current `ETag` in `If-Match`:

```json
{ "target_stage_id": "checkout_initiated", "reason": "Reassigned by the sales team" }
```

`reason` is required and is included in the
`diamond_membership.stage_changed` event. Leads only move forward: a target
stage at or before the current one returns `409 state_conflict`, so
repeating a transition that already happened is rejected and does not send
the communication again. A manual move follows the same rules as the
automatic progression described in
[What happens automatically](#what-happens-automatically), and the
communication for the new stage is sent once.

## Processing attempts

A processing attempt is not a standalone resource: it is an item of a
`diamond.reprocess_membership` [operation](https://cademi.dev/api/operations.md), requested for a
membership.

`POST .../memberships/{membership_id}/processing-attempts` requires
`diamonds.reprocess` and an `Idempotency-Key` header. It requests that the
most recent failed communication for the lead be sent again, queuing the
communication for that stage. If the stage has already been confirmed,
nothing is sent again, which prevents duplicate messages. A membership with
no recorded failure returns `422 validation_failed`, never a `202` that does
nothing: accepting the request would promise a resend that cannot happen.
A successful request returns `202`:

```json
{
  "object": "processing_attempt",
  "id": "pat_88",
  "membership_id": "mbr_9001",
  "effects": { "stage_confirmed": false, "communication_sent": true },
  "operation_id": "op_01J8Z3...",
  "status": "queued",
  "error": null,
  "requested_by": { "kind": "credential", "id": "key_..." },
  "created_at": "2026-09-23T15:00:00Z"
}
```

`GET .../memberships/{membership_id}/processing-attempts` lists the attempts
already requested for the lead, all in a single response without
pagination. `GET .../processing-attempts/{attempt_id}` returns one. An
attempt that belongs to another membership returns `404` through this path,
even if it exists.

## Permissions

`diamonds.read`, `diamonds.create`, `diamonds.update`, `diamonds.delete`,
`diamonds.activate` (changing `status`), `diamonds.manage_triggers`
(triggers), `diamonds.manage_memberships` (adding and moving leads), and
`diamonds.reprocess`. Creating a Diamond also requires `products.create`
and `deliveries.create`; creating a membership also requires
`enrollments.create`.

Access to Diamonds can be scoped to specific showcases, specific products,
or the whole account. Access to memberships can additionally be scoped to
specific users: credentials delegated to a user's data see only that user's
memberships, without needing access to the whole Diamond. See
[Access control](https://cademi.dev/api/access-control.md).

## Events

`diamond.created`, `diamond.updated`, `diamond.deleted`,
`diamond_trigger.updated`, `diamond_membership.created`,
`diamond_membership.stage_changed`, `diamond_membership.run_failed`, and
`diamond_membership.reprocess_requested`. No payload includes the lead's
personal data or the destination configured on a trigger. See
[Events](https://cademi.dev/api/events.md) for the catalog.

## What happens automatically

Cademí always advances leads on its own, never through the API: after a
stage's waiting interval elapses, when the lead watches a lesson, opens the
offer link, or purchases the expected delivery. The API offers only two
state changes: adding a new lead (`POST .../memberships`, always starting at
`lead`) and moving an existing membership to a later stage
(`PATCH .../memberships/{id}`, with `reason`). The API never moves a lead
backward and does not bypass the progression rules.

## Errors

| Code | Status | When |
|---|---|---|
| `not_found` | 404 | The Diamond, stage, trigger, membership, or attempt does not exist or is not accessible with the current credentials (including showcase, product, or user scoping) |
| `permission_denied` | 403 | The credentials lack one of the permissions the operation requires |
| `feature_disabled` | 409 | Diamond automations are not enabled for the account |
| `state_conflict` | 409 | The target stage is at or before the current stage; the user already belongs to another Diamond; the Diamond still has memberships on `DELETE` |
| `revision_mismatch` | 412 | The `If-Match` revision does not match the current one |
| `validation_failed` | 422 | A required field is missing, `stage_id` is not a valid stage, `interval_minutes` is not an accepted value, the trigger destination is not a public host, or the membership has no recorded failure to reprocess |

See [Errors](https://cademi.dev/api/errors.md) for the error format.
