Splashify RCS

Docs

Open panel
Panel

Recurring broadcasts

A recurring definition is a template + list + bot + cron cadence that creates a fresh broadcast (a child) every time the schedule comes round. Every child broadcast enters the normal preparing → ready → running → completed pipeline, so recurring runs show up in /broadcasts alongside your one-offs.

Access

Bot-scope from the caller's JWT bots claim applies, a definition on a bot the caller can't act on returns 403 bot_not_assigned on writes and is filtered out on reads.

The bot must be able to broadcast

A recurring definition is a broadcast generator, so create, update and resume take the same registration check as a one-off broadcast: a bot registered transactional or otp is refused with 403 bot_not_promotional. See Broadcasts for the response shape, and filter on can_broadcast from GET /rcs/bots to avoid it.

Pause and delete are never refused. A schedule you can create but cannot turn off would be worse than one you cannot create, so only the write paths that put messages on the carrier are gated.

The registration is checked again on every run, not just at save time. An administrator can correct a bot's traffic type long after a cron was set up; when that happens the schedule stops producing children and stays put for you to repoint or delete, rather than being silently deactivated.

Endpoints

http
GET    /api/v1/rcs/recurring-broadcasts
POST   /api/v1/rcs/recurring-broadcasts
PATCH  /api/v1/rcs/recurring-broadcasts/{id}
DELETE /api/v1/rcs/recurring-broadcasts/{id}
POST   /api/v1/rcs/recurring-broadcasts/{id}/pause
POST   /api/v1/rcs/recurring-broadcasts/{id}/resume
GET    /api/v1/rcs/recurring-broadcasts/{id}/children?limit=50

Create body

json
{
  "name":            "Weekly newsletter",
  "bot_id":          "3c9…",
  "template_id":     "48b…",
  "list_id":         "9ff…",
  "audience":        "rcs",
  "cron_expression": "0 9 * * 1",
  "tz":              "Asia/Kolkata",

  "ab_test":         false,
  "ab_template_a":   "",
  "ab_template_b":   "",
  "ab_split_ratio":  0,

  "active":          true
}

Set ab_test to true and both ab_template_a + ab_template_b (each must be an approved template) to run every child as an A/B test. Split defaults to 50/50 when ab_split_ratio is out of range.

audience is optional and says who each run sends to, with the same values as a one-off broadcast: rcs (the default, also when missing or ""), not_rcs or all. Any other value is refused with 400 invalid_audience. A PATCH without audience keeps the stored value. The list rows from GET include audience too (definitions saved before the field read rcs).

Each run copies audience to the child broadcast it makes, counts the list again for it when it runs, and holds the wallet for that count (all numbers that have not opted out while a check on the list is still running). Create and update are not refused when the list reaches nobody today: each run counts again. Opted-out numbers are never sent.

Cron syntax

Standard 5-field POSIX cron:

text
minute  hour  day-of-month  month  day-of-week
0-59    0-23  1-31          1-12   0-6 (0=Sunday)

Supported per field:

  • Literal integers (e.g. 9).
  • Comma-separated lists (e.g. 1,3,5).
  • * for "every value".

Ranges (1-5), steps (*/15), and named tokens (SUN) are not supported in v1, the panel's cadence picker emits only the subset the parser accepts. If you need richer syntax, use the "Advanced" tab in the panel modal and type the raw expression; the parser will reject anything it can't handle at create-time with a 400.

The New recurring broadcast dialog with the template weekly_digest_card, its variable mapped to the name column with the fallback there, and the Cadence set to Advanced with the schedule 30 10 1,15 * * typed in, above the line that explains the five parts
Advanced takes the raw expression. This one runs at 10:30 am on the 1st and 15th.

Examples

Timezone semantics

tz is any valid IANA name (Asia/Kolkata, America/New_York, …). The cron ticks in the given zone; next_run_at is always stored as UTC. next_run_at moves on after each child is created.

Pause / resume

Paused definitions stop ticking without losing history.

http
POST /api/v1/rcs/recurring-broadcasts/{id}/pause
POST /api/v1/rcs/recurring-broadcasts/{id}/resume

Resume recomputes next_run_at from the current cron so a definition paused during a peak doesn't fire a burst of catch-up broadcasts.

Children

Every child created is recorded against its definition, so the parent → child relationship can be read on its own. Each child broadcast also carries parent_recurring_id, which points back to the definition.

http
GET /api/v1/rcs/recurring-broadcasts/{id}/children?limit=50

Response carries each child's status + counters so the panel list can render terminal state without a second call.

Failure modes

  • Deleted list / empty list, the run is skipped and next_run_at advances. The definition stays active.
  • Nobody to send to for its audience, every number opted out, the list's check, made with any of your bots, found every number unreachable (rcs), or no number is marked unreachable (not_rcs, reason none_cannot_get). The same rule as no_reachable_recipients. The tick is skipped and next_run_at advances; no child broadcast is created. The definition stays active. With rcs, numbers the bot found not on RCS in the last 30 days count as nobody here too.
  • Daily spend limit reached, a run that falls due after today's spend limit was reached is skipped the same way: no child broadcast, next_run_at advances, the definition stays active. A child that was already created and falls due after the limit was reached does not start: it ends failed with status_reason: "spend_limit_reached" and nothing is sent. Creating, updating and resuming a recurring broadcast are not refused by the limit.
  • Cron parse failure, an update that lands an invalid expression pauses the definition (active = false). The panel shows the parse error inline; fix the expression + resume.
  • Wallet insufficient, the run's hold is taken when its child is created. A child that cannot be held ends failed and is never prepared or sent. The schedule carries on: next_run_at advances and the next run tries again.
  • Bot no longer registered for broadcasts, the run is skipped and next_run_at advances. The definition stays active and untouched; repoint it at a bot where can_broadcast is true, or delete it.

SSE

Each child created is announced as recurring.materialized on the account's SSE channel:

json
{
  "type": "recurring.materialized",
  "data": {
    "recurring_id":       "de…",
    "child_broadcast_id": "9c…",
    "name":               "Weekly newsletter",
    "materialized_at":    "2026-07-16T03:30:00Z"
  }
}

The panel's /broadcasts/recurring list keys off it to bump child_count without a re-fetch.