Currency

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, not ripllo.marketingCampaigns. They are different resources. marketingCampaigns is 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 no send, no sendTest and no templates; calling those on it throws TypeError: … 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 reads provider and recipient and returns 400 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

CurrencyRupiah is paid by transfer or QRIS; US dollars settle through PayPal.