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.
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:
{
"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:
{
"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:
{
"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: 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:
{
"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. 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:
{ "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, 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, 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:
{
"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.
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 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 for the error format.