Skip to main content

Program Messages

A program can carry a message plan: the emails and texts everyone on it receives, timed against each cohort's own dates. The plan lives on the program; every cohort inherits it and schedules from its own sessions, so "a week before we start" is written once and resolves differently for each intake.

Two resources:

  • Messages โ€” the plan itself, attached to a program.
  • Message runs โ€” one scheduled send for one cohort, produced from the plan.

Sends are resolved to recipients at send time from the cohort's live enrollments, so a late joiner is included and a cancellation is not.

Authenticationโ€‹

Reads require series:read; writes require series:write. Programs are a Growth-plan feature โ€” a plan without it gets 403 plan_feature_required.


List messagesโ€‹

GET /v1/series/{id}/messages

Returns the plan for the program, including paused messages. Calling it with a cohort id returns the plan the cohort inherits.

Exampleโ€‹

curl "https://app.equipdash.com/api/v1/series/11/messages" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": [
{
"id": 42,
"program_id": 11,
"position": 0,
"name": "Reminder โ€” you start next week",
"channel": "email",
"audience": "participants",
"timing": {
"anchor": "program_start",
"anchor_session_number": null,
"offset_value": 7,
"offset_unit": "days",
"offset_direction": "before",
"send_at_local_time": "09:00",
"fixed_at": null,
"summary": "7 days before the program starts"
},
"subject": "{{program_name}} starts {{start_date}}",
"email_blocks": [
{ "id": "b1", "type": "heading", "data": { "text": "Next week, {{first_name}}" } }
],
"sms_body": null,
"enabled": true,
"before_confirmation": false,
"created_at": "2026-08-08T10:00:00Z",
"updated_at": "2026-08-08T10:00:00Z"
}
],
"meta": { "request_id": "req_abc123", "timestamp": "2026-08-08T10:05:00Z" }
}

Create a messageโ€‹

POST /v1/series/{id}/messages
FieldTypeRequiredDescription
namestringYesInternal label โ€” the customer never sees it
channelstringYesemail or sms
audiencestringNoparticipants (default), booking_contact, or both
anchorstringNoWhat the timing is measured against โ€” see below
anchor_session_numberintegerNo1-based session position, for session_number
offset_valueintegerNo0 means "straight away"
offset_unitstringNominutes, hours, days (default), weeks
offset_directionstringNobefore or after
send_at_local_timestringNoHH:MM in the supplier's timezone; used for day/week offsets
fixed_atdatetimeNoOnly for the fixed_date anchor
subjectstringNoEmail only
email_blocksarrayNoThe visual editor's block list
sms_bodystringNoText only, max 640 characters
enabledbooleanNoDefaults to on

Anchorsโ€‹

AnchorSends
enrollmentOnce per booking, timed from when it was made (after only)
program_startOnce per cohort, from its first session
program_endOnce per cohort, from the end of its last session
each_sessionOnce per session in the cohort
session_numberOnce, from the session at anchor_session_number
fixed_dateOnce, at fixed_at (after only)

Day- and week-grained offsets land at send_at_local_time in the supplier's timezone, so a cohort spanning a daylight-saving change still sends at the same local hour. Minute- and hour-grained offsets keep the session's own clock time.

Merge tagsโ€‹

Usable in subject, sms_body and text blocks. A tag that does not exist is refused with 422 (field: tags), naming the tag and the one that was probably meant, and nothing is saved.

{{first_name}} {{last_name}} {{program_name}} {{cohort_name}} {{start_date}} {{start_time}} {{end_date}} {{session_date}} {{session_time}} {{session_number}} {{session_count}} {{location}} {{booking_reference}} {{balance_due}} {{supplier_name}}

Exampleโ€‹

curl -X POST "https://app.equipdash.com/api/v1/series/11/messages" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Kit list",
"channel": "sms",
"audience": "participants",
"anchor": "each_session",
"offset_value": 1,
"offset_unit": "days",
"offset_direction": "before",
"sms_body": "Hi {{first_name}}, {{program_name}} is on tomorrow at {{session_time}}, {{location}}."
}'

Returns 201 Created with the message.


Update a messageโ€‹

PATCH /v1/series/{id}/messages/{messageId}

Accepts any subset of the create fields. Changing the timing reschedules every future send across all cohorts; anything already sent is untouched.


Remove a messageโ€‹

DELETE /v1/series/{id}/messages/{messageId}

Returns 204 No Content. Future cohorts stop sending it; already-sent messages stay in each customer's history.


List a cohort's scheduleโ€‹

GET /v1/series/{id}/message-runs

Optional ?status= filter (scheduled, sent, skipped, failed, cancelled).

{
"data": [
{
"id": 900,
"cohort_id": 12,
"program_message_id": 42,
"message_name": "Reminder โ€” you start next week",
"channel": "email",
"booking_id": null,
"session_id": null,
"scheduled_at": "2026-09-08T09:00:00Z",
"status": "scheduled",
"status_label": "Scheduled",
"sent_at": null,
"recipients_count": 0,
"failure_reason": null
}
],
"meta": { "request_id": "req_abc123", "timestamp": "2026-08-08T10:05:00Z" }
}

Send one nowโ€‹

POST /v1/series/{id}/message-runs/{runId}/send

Sends immediately, ignoring the schedule but not the plan. Returns the run plus recipients_count. A run that has already sent, been skipped or been cancelled returns 409 already_resolved โ€” a run leaves scheduled exactly once, so this is safe to retry.

Skip oneโ€‹

POST /v1/series/{id}/message-runs/{runId}/skip

Skips it for this cohort only. Every other cohort is unaffected.


Errorsโ€‹

StatusCodeDescription
401authentication_requiredNo API key provided
403insufficient_scopeKey lacks series:read / series:write
403plan_feature_requiredThe plan does not include Programs
404resource_not_foundThe program, message or scheduled send does not exist for this business
409already_resolvedThe send is not waiting to go out
422validation_errorSee error.details for the offending fields