Broadcasts
A broadcast is a blast send — one piece of content fanned out across one or more channels to a resolved audience. Email a launch announcement; SMS a flash-sale code; post to a Discord webhook and a Slack channel simultaneously. Broadcasts are the "what" and "to whom"; the actual delivery happens via per-message rows queued in MarketingMessage and dispatched by a worker.
Broadcasts are distinct from creator campaigns (/api/v1/campaigns), which are creator-marketplace briefs. The broadcast resource lives at /api/v1/marketing-campaigns (legacy URL kept for back-compat after the model was renamed) precisely to keep the namespaces separate.
The lifecycle is:
- Create a draft. Pick channels, write content per channel.
- Update until ready. Audience can be lists, contact IDs, or segments.
- Send-test to verify rendering on one recipient.
- Send to fan out for real. Status flips to
sending. - The worker delivers each
MarketingMessage. Status eventually settles tosentonce the queue drains.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/marketing-campaigns |
List campaigns |
POST |
/api/v1/marketing-campaigns |
Create a campaign |
GET |
/api/v1/marketing-campaigns/:id |
Retrieve a campaign |
PATCH |
/api/v1/marketing-campaigns/:id |
Update a campaign |
POST |
/api/v1/marketing-campaigns/:id/send |
Send the campaign |
POST |
/api/v1/marketing-campaigns/:id/send-test |
Send a test message |
POST |
/api/v1/marketing-campaigns/:id/cancel |
Cancel the campaign |
DELETE |
/api/v1/marketing-campaigns/:id |
Archive the campaign. |
POST |
/api/v1/marketing-campaigns/templates/compile |
Server-side compile of a blocks doc to HTML+text. |
GET |
/api/v1/marketing-campaigns/templates |
List templates |
POST |
/api/v1/marketing-campaigns/templates |
Create a template. |
PATCH |
/api/v1/marketing-campaigns/templates/:tid |
Update a template. |
DELETE |
/api/v1/marketing-campaigns/templates/:tid |
Delete a template. |
All endpoints require the merchant role.
Providers
A campaign carries a list of providers (1–10) that determine which channels to dispatch through. The full list:
| Category | Providers |
|---|---|
email_resend, email_sendgrid, email_mailgun, email_postmark, email_ses |
|
| SMS | sms_twilio, sms_vonage |
| Messaging | whatsapp_cloud, whatsapp_twilio, telegram_bot, line_business, discord_webhook, slack_webhook |
| Push | push_onesignal, push_fcm |
| Social | meta_business, linkedin, tiktok_business, twitter, youtube, pinterest, threads |
| Generic | webhook_generic |
A campaign can only use providers for which the merchant has an active channel integration. Sending a campaign with a provider that has no matching channel returns 400 NO_CHANNEL.
Create a campaign
POST /api/v1/marketing-campaigns
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string (1–120) | yes | Internal label. |
description |
string (≤2000) | null | no | |
providers |
enum[] (1–10) | yes | At least one provider. See Providers. |
content |
object | no | Per-channel content. Keys are channel-content keys: email, sms, whatsapp, telegram, discord, slack, plus generic webhook etc. Email content typically { subject, html, text }; SMS { body }. Use templates to standardise. |
audience |
object | no | { listIds?: string[], contactIds?: string[], segmentIds?: string[] }. Union semantics: a contact present in any source is included exactly once. |
scheduledAt |
ISO 8601 | null | no | Future-schedule the send. When set on create, the campaign starts in scheduled status; otherwise draft. |
Response — 201 Created
{
"data": {
"id": "mcp_01HX...",
"accountId": "acc_01HX...",
"name": "May launch announcement",
"description": null,
"providers": ["email_resend", "discord_webhook"],
"content": { "email": { "subject": "We're back", "html": "...", "text": "..." } },
"audience": { "listIds": ["cl_01HX..."], "segmentIds": ["seg_01HX..."] },
"scheduledAt": null,
"status": "draft",
"startedAt": null,
"createdAt": "2026-05-13T10:42:00.000Z",
"updatedAt": "2026-05-13T10:42:00.000Z"
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
List campaigns
GET /api/v1/marketing-campaigns
Returns every campaign in the workspace, newest first. Each row includes a _count.messages field for the total number of MarketingMessage rows fanned out so far.
Retrieve a campaign
GET /api/v1/marketing-campaigns/:id
Returns the campaign plus a statusCounts map summarising delivery state across MarketingMessage rows:
{
"data": {
"id": "mcp_01HX...",
"name": "May launch announcement",
/* ... */
"statusCounts": {
"queued": 3,
"sending": 8,
"sent": 487,
"failed": 2,
"skipped": 0
}
},
"error": null,
"meta": { "requestId": "...", "timestamp": "..." }
}
Update a campaign
PATCH /api/v1/marketing-campaigns/:id
Partial. Edits are rejected once the campaign has transitioned to sending or sent — the queue is already drained and partially modifying content would be confusing.
| Status | error.code |
When |
|---|---|---|
409 |
BAD_STATE |
Campaign is already sending or sent. Cancel first if you really need to re-edit. |
Send the campaign
POST /api/v1/marketing-campaigns/:id/send
Resolves the audience, builds per-(contact × provider) MarketingMessage rows, and transitions the campaign to sending. The worker picks the messages up from there.
The audience resolution is:
- Union
audience.contactIds, members of everyaudience.listIds, and contacts matched by everyaudience.segmentIds. - For each resolved contact, for each provider in
providers, build a row — provided the contact has a usable recipient identifier for that channel (e.g., contacts without email are skipped foremail_*providers). - Channels marked
webhook_generic,discord_webhook,slack_webhookproduce exactly one row withrecipient: "broadcast"per provider — webhook posts fan once per channel, not once per contact.
Errors
| Status | error.code |
When |
|---|---|---|
400 |
NO_PROVIDER |
Campaign has zero providers. Pick at least one. |
400 |
NO_CHANNEL |
No active channel integration for any of the picked providers. |
400 |
EMPTY_AUDIENCE |
Resolved audience came back empty. |
400 |
NO_RECIPIENTS |
Audience non-empty, but none of the contacts had a usable recipient for any of the picked channels. |
409 |
BAD_STATE |
Campaign already sending or sent. |
Response
{ "data": { "id": "mcp_01HX...", "queued": 487 }, "error": null, "meta": { ... } }
queued is the count of MarketingMessage rows inserted. Reach this number, and the worker has all the rows it needs to deliver everything.
const result = await ripllo.marketingCampaigns.send('mcp_01HX...');
console.log(`Queued ${result.queued} messages`);
Send a test message
POST /api/v1/marketing-campaigns/:id/send-test
Bypasses audience resolution. Queues exactly one MarketingMessage so the worker dispatches it through the same path a real send would — useful for verifying rendering before committing the full send.
Request body
| Field | Required | Notes |
|---|---|---|
provider |
yes | Must be one of the campaign's providers. |
recipient |
yes | The destination. For email_* an address; for sms_* a phone; for whatsapp_cloud and whatsapp_twilio a phone (Twilio routing auto-prefixes whatsapp:). |
| Status | error.code |
When |
|---|---|---|
400 |
BAD_PROVIDER |
Provider not in campaign's providers. |
400 |
NO_CHANNEL |
No active channel for that provider. |
Cancel the campaign
POST /api/v1/marketing-campaigns/:id/cancel
Marks any queued messages as skipped with lastError = "campaign canceled", and flips campaign status to paused. Messages already in sending or sent are left as-is — cancellation is forward-only.
Templates
Templates are saved drafts of campaign content. They live under the same resource (/marketing-campaigns/templates) because conceptually a template is a campaign without an audience or schedule.
Templates can carry a blocks document — a structured email layout the dashboard's builder produces. When set, the server compiles blocks to HTML+text on save and stamps the result into content.email.{html,text}. Send-time rendering reads content.email.{html,text} directly, so the compile is byte-identical between dashboard preview and actual delivery.
The blocks doc
{
"subject": "We're back",
"preheader": "Restocked and reorganized",
"accentColor": "#ff5722",
"blocks": [
{ "kind": "header", "logoKey": "s3://.../logo.png", "brandName": "ExampleCo" },
{ "kind": "hero", "title": "We're back", "subtitle": "Restocked and reorganized" },
{ "kind": "text", "body": "Hi {{ first_name }}, ..." },
{ "kind": "cta", "label": "Shop now", "url": "https://example.com" },
{ "kind": "footer", "text": "ExampleCo Inc.", "businessAddress": "Jakarta", "unsubscribeUrl": "..." }
]
}
Supported block kinds: header, hero, text, cta, image, divider, footer. Up to 40 blocks per document.
Compile preview
POST /api/v1/marketing-campaigns/templates/compile
Same compiler the save path uses. Send a blocks doc plus optional assetUrls map (S3 key → presigned-GET URL) and get back { html, text }. Lets the builder's preview iframe show the exact bytes the recipient will see.
Template CRUD
| Method | Path | Notes |
|---|---|---|
GET |
/templates |
List, most-recently-updated first. |
POST |
/templates |
Create. Field: { name, description?, providers?, content?, blocks? }. |
PATCH |
/templates/:tid |
Partial update. Setting blocks re-runs the compiler. |
DELETE |
/templates/:tid |
Hard delete. |
| Status | error.code |
When |
|---|---|---|
409 |
NAME_EXISTS |
Template name collision. |
The broadcast object
| Field | Type | Nullable | Notes |
|---|---|---|---|
id |
string | no | mcp_ + ULID. |
accountId |
string | no | |
name, description |
string | description yes | |
providers |
enum[] | no | At least one. |
content |
object | no | Per-channel content. |
audience |
object | no | listIds/contactIds/segmentIds. |
scheduledAt |
ISO 8601 | yes | Future-send. |
status |
enum | no | draft, scheduled, sending, sent, paused. |
startedAt |
ISO 8601 | yes | When /send ran. |
createdAt, updatedAt |
ISO 8601 | no |
The marketing message object (read-only)
The worker writes to MarketingMessage directly — you don't create these yourself. Listing them is on the roadmap; for now, the per-campaign statusCounts summary covers most monitoring needs.
| Field | Type | Notes |
|---|---|---|
id |
string | mmg_ + ULID. |
accountId, campaignId, contactId |
string | campaignId/contactId can be null for test sends. |
channelIntegrationId |
string | The channel actually used. |
provider |
enum | Which channel was selected. |
recipient |
string | Email/phone/handle/broadcast. |
content |
object | Snapshot at fan-out time. |
status |
enum | queued, sending, sent, failed, skipped. |
lastError |
string | null | Provider error if failed/skipped. |
sentAt, createdAt |
ISO 8601 |
Events
| Event type | Fires on | Status |
|---|---|---|
ripllo.marketing_campaign.sent.v1 |
All queued messages drained — campaign transitions to sent. |
Reserved — not currently emitted. |
ripllo.marketing_campaign.canceled.v1 |
/cancel succeeds. |
Reserved. |
ripllo.marketing_message.delivered.v1 |
Per-message success from the worker. | Reserved. |
ripllo.marketing_message.failed.v1 |
Per-message failure. | Reserved. |
Next
- Channels — the integrations campaigns dispatch through.
- Contact lists and Audience segments — the audience sources.