Broadcasts
A broadcast is one send (or a scheduled send) of a message to a defined audience, across one or more channels — email, SMS, WhatsApp, Telegram, or a webhook post to Discord/Slack. Broadcasts reference a contact list, an audience segment or a raw contact ID array for audience, and a channel integration per provider for delivery. This page covers ripllo.broadcasts on the Node SDK. For the HTTP surface, see API → Broadcasts.
ripllo.broadcasts, notripllo.marketingCampaigns. They are different resources.marketingCampaignsis the roll-up hub at/api/v1/marketing-campaigns— a container with a name, goal, budget and status that other objects attach to. It has nosend, nosendTestand notemplates; calling those on it throwsTypeError: … is not a function. See the hub namespace at the bottom of this page.
Namespace
ripllo.broadcasts.list()
ripllo.broadcasts.get(id)
ripllo.broadcasts.create(input)
ripllo.broadcasts.update(id, patch)
ripllo.broadcasts.send(id, input?)
ripllo.broadcasts.sendTest(id, input)
ripllo.broadcasts.templates.list()
ripllo.broadcasts.templates.create(input)
ripllo.broadcasts.templates.update(templateId, patch)
ripllo.broadcasts.templates.compile(input)
Two surfaces under one namespace: broadcast CRUD and templates. Templates are reusable content blocks; a broadcast wires content to an audience and a set of providers for a specific send.
Methods — broadcasts
create
Signature. ripllo.broadcasts.create(input): Promise<Broadcast>
Creates a broadcast in draft state — or in scheduled if you pass scheduledAt. Send it via .send(id).
const b = await ripllo.broadcasts.create({
name: 'May newsletter',
providers: ['email_resend'], // required, 1–10 channel slugs
content: {
email: {
subject: 'Your May update',
html: '<h1>Hello</h1>',
text: 'Hello',
},
},
audience: { listIds: ['<contact-list-id>'] }, // and/or segmentIds / contactIds
scheduledAt: '2026-05-20T09:00:00Z', // optional; omit for a draft
});
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string (1–120) | yes | |
description |
string | null | no | |
providers |
string[] (1–10) | yes | Channel slugs — email_resend, email_sendgrid, email_mailgun, email_postmark, email_ses, sms_twilio, sms_vonage, whatsapp_cloud, whatsapp_twilio, telegram_bot, line_business, discord_webhook, slack_webhook, push_onesignal, push_fcm, meta_business, linkedin, tiktok_business, twitter, youtube, pinterest, threads, webhook_generic. Omitting this 400s. |
content |
object | no (defaults {}) |
Keyed per channel kind: email, sms, whatsapp, telegram, discord, slack. Each key holds that channel's payload. |
audience |
object | no (defaults {}) |
{ listIds?, contactIds?, segmentIds? }. All three are resolved and unioned at send time. |
scheduledAt |
ISO 8601 | null | no | Sets status: 'scheduled' at create time. |
marketingCampaignId |
string | null | no | Attach to a hub campaign for roll-up. A campaign outside your workspace 400s. |
There is no channelId, templateId, audienceSegmentId, subject, fromName, fromAddress or replyTo field on a broadcast — sender identity lives on the channel integration, and the subject lives inside content.email.
list / get
const broadcasts = await ripllo.broadcasts.list(); // a bare array, not { broadcasts }
const single = await ripllo.broadcasts.get('<broadcast-id>');
list is unpaginated and newest-first; each row carries _count.messages. get additionally returns statusCounts — a map of MarketingMessage status to count for that broadcast ({ queued: 412, sent: 380, bounced: 2 }), which is how you track progress. There are no sent/opened/clicked counters on the broadcast row itself.
update
await ripllo.broadcasts.update('<broadcast-id>', {
content: { email: { subject: 'Your May update (updated)', html: '…', text: '…' } },
});
PATCH semantics over the same fields as create. You can edit while the broadcast is draft, scheduled or paused; once it is sending or sent the API returns 409 BAD_STATE — create a new broadcast instead.
send
Signature. ripllo.broadcasts.send(id, input?): Promise<{ id: string; queued: number }>
Resolves the audience, fans it out into MarketingMessage rows in queued, and flips the broadcast to sending. The SDK auto-generates an Idempotency-Key.
const { queued } = await ripllo.broadcasts.send('<broadcast-id>');
console.log(`${queued} messages queued`);
The response is { id, queued } — queued is the number of message rows written, not a recipient count, because a two-provider broadcast writes one row per contact per provider. There is no jobId and no estimatedRecipients. The actual delivery is asynchronous: a worker picks the queued rows up, so poll get(id).statusCounts to follow it.
Webhook-style providers (discord_webhook, slack_webhook, webhook_generic) post once per channel rather than once per contact, so they contribute exactly one queued row each regardless of audience size.
sendTest
await ripllo.broadcasts.sendTest('<broadcast-id>', {
provider: 'email_resend',
recipient: 'me@example.com',
});
Queues exactly one message through the same worker path a real send uses, bypassing audience resolution. Both provider and recipient are required; the provider must be one of the broadcast's own providers. Test sends are written with a null campaignId, so they never land in the broadcast's statusCounts. Resolves to { messageId }.
The SDK types this as
{ to: string }. That shape is wrong — the server readsproviderandrecipientand returns400 VALIDATION(provider and recipient required) for a{ to }body. Pass the real fields until the typing is corrected.
Cancelling
There is no cancel() on the Node SDK yet. Over HTTP, POST /api/v1/broadcasts/:id/cancel marks every still-queued message as skipped and puts the broadcast in paused.
Methods — templates
Templates are block documents, not MJML. A template holds a blocks array that Ripllo compiles to HTML and plain text server-side, so the dashboard preview and the delivered email come from one renderer.
templates.create
const tpl = await ripllo.broadcasts.templates.create({
name: 'May newsletter',
description: 'Monthly update',
providers: ['email_resend'],
blocks: {
subject: 'Your May update',
preheader: 'What shipped this month',
accentColor: '#0f766e',
blocks: [
{ kind: 'header', brandName: 'Brand' },
{ kind: 'hero', title: 'May at Brand', subtitle: 'Three new things' },
{ kind: 'text', body: 'Hello — here is what changed.' },
{ kind: 'cta', label: 'Shop now', url: 'https://brand.com' },
{ kind: 'footer', text: '© Brand', unsubscribeUrl: '…' },
],
},
});
Block kinds: header, hero, text, cta, image, divider, footer — up to 40 per document. When blocks is present the server compiles it and writes the result into content.email.{subject,html,text} for you; you can also skip blocks entirely and pass content yourself. There is no kind, bodyMjml, bodyHtml, bodyText or variables field. Template names are unique per workspace — a duplicate returns 409 NAME_EXISTS.
templates.list / templates.update
const { templates } = await ripllo.broadcasts.templates.list();
await ripllo.broadcasts.templates.update(tpl.id, { name: 'May newsletter v2' });
list resolves to { templates } (this one is wrapped), newest-updated first. Deleting a template is HTTP-only: DELETE /api/v1/broadcasts/templates/:id.
templates.compile
const { html, text } = await ripllo.broadcasts.templates.compile({
subject: 'Your May update',
preheader: 'What shipped this month',
blocks: [{ kind: 'text', body: 'Hello {{firstName}}' }],
});
Takes a blocks document directly (not a template ID) and returns both html and text. Useful for preview panes. The SDK's return type only names html; text is on the wire too.
Types
interface Broadcast {
id: string; // opaque cuid
accountId: string;
name: string;
description: string | null;
providers: string[];
content: Record<string, Record<string, unknown>>;
audience: { listIds?: string[]; contactIds?: string[]; segmentIds?: string[] };
status: 'draft' | 'scheduled' | 'sending' | 'sent' | 'paused' | 'archived';
scheduledAt: string | null;
startedAt: string | null;
completedAt: string | null;
marketingCampaignId: string | null;
createdAt: string;
updatedAt: string;
}
interface ComposeTemplate {
id: string; // opaque cuid
accountId: string;
name: string;
description: string | null;
providers: string[];
content: Record<string, Record<string, unknown>>;
blocks: BlocksDoc | null;
createdAt: string;
updatedAt: string;
}
Message rows created by a send move through queued → sending → sent → delivered, with bounced, failed, skipped, opened and clicked as the other terminal or engagement states.
Common patterns
Test → send flow
const b = await ripllo.broadcasts.create({ /* ...draft... */ });
await ripllo.broadcasts.sendTest(b.id, { provider: 'email_resend', recipient: 'me@example.com' });
// review test email...
await ripllo.broadcasts.send(b.id);
Follow a send to completion
No webhook fires when a broadcast finishes — poll instead:
const { statusCounts } = await ripllo.broadcasts.get(b.id);
console.log(`sent ${statusCounts.sent ?? 0}, bounced ${statusCounts.bounced ?? 0}, still queued ${statusCounts.queued ?? 0}`);
Segment targeting
await ripllo.broadcasts.create({
name: 'VIP early access',
providers: ['email_resend'],
content: { email: { subject: 'You first', html: '…', text: '…' } },
audience: { segmentIds: ['<segment-id>'] },
});
Segments are resolved at send time, so the audience is whoever matches then — not who matched at create time.
Errors
| Code | Status | Cause |
|---|---|---|
VALIDATION |
400 | Body shape wrong — missing providers, bad provider slug, or a marketingCampaignId outside your workspace. |
NO_PROVIDER |
400 | Send attempted with an empty providers array. |
NO_CHANNEL |
400 | No active channel integration matches the picked providers. |
EMPTY_AUDIENCE |
400 | The resolved audience has zero contacts. |
NO_RECIPIENTS |
400 | Contacts resolved, but none had a usable identifier for the chosen channels (e.g. no email address for an email send). |
BAD_PROVIDER |
400 | sendTest named a provider that isn't in the broadcast's providers. |
BAD_STATE |
409 | Edit or send attempted while status is sending or sent. |
NAME_EXISTS |
409 | A template with that name already exists in the workspace. |
NOT_FOUND |
404 | No such broadcast or template in this workspace. |
See Errors for handling.
The hub namespace: ripllo.marketingCampaigns
Separate resource, separate URL (/api/v1/marketing-campaigns), no sending:
ripllo.marketingCampaigns.list({ status: ['live'] })
ripllo.marketingCampaigns.get(id)
ripllo.marketingCampaigns.getFull(id) // hub + linked children + performance roll-up
ripllo.marketingCampaigns.selector() // lightweight dropdown payload
ripllo.marketingCampaigns.create({ name, goal, status, budgetIdr, startsAt, endsAt, notes })
ripllo.marketingCampaigns.update(id, patch)
ripllo.marketingCampaigns.delete(id) // soft-delete: sets status='archived'
goal is one of awareness (default), conversion, retention, launch, other; status is draft (default), live, paused, completed, archived. Attach a broadcast to a campaign with marketingCampaignId on create or update.
Next
- Channels — the delivery surface.
- Audience segments — dynamic targeting.
- API → Broadcasts — full HTTP reference.