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:

  1. Create a draft. Pick channels, write content per channel.
  2. Update until ready. Audience can be lists, contact IDs, or segments.
  3. Send-test to verify rendering on one recipient.
  4. Send to fan out for real. Status flips to sending.
  5. The worker delivers each MarketingMessage. Status eventually settles to sent once 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 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:

  1. Union audience.contactIds, members of every audience.listIds, and contacts matched by every audience.segmentIds.
  2. 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 for email_* providers).
  3. Channels marked webhook_generic, discord_webhook, slack_webhook produce exactly one row with recipient: "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