Broadcasts

Broadcasts send one email campaign to many recipients: newsletters, product updates, and announcements. You create a draft, then send it immediately or schedule it. Pingram builds the audience, throttles delivery, and tracks per-recipient results.

You can do this from the Broadcast tab in the dashboard or with the Broadcasts API.

Create a draft

POST /broadcasts creates a draft. Nothing is sent until you call send or schedule.

const broadcast = await pingram.broadcasts.create({
name: 'August product update',
type: 'product_updates',
channel: 'email',
audience: {
filter: {
$and: [
{ email: { $exists: true } },
{ 'properties.plan': { $eq: 'pro' } }
]
}
},
fromName: 'Acme',
fromAddress: 'updates@acme.com',
replyToAddress: 'hello@acme.com',
subject: 'What we shipped, {{ user.properties.firstname }}',
html: '<p>Hi {{ user.properties.firstname | default: "there" }},</p><p><a href="{{ pingram.unsubscribe }}">Unsubscribe</a></p>'
});
  • type is the notification type key. Recipients can unsubscribe from it independently of other types. It is created automatically if it does not exist yet.
  • channel is 'email'.
  • subject and html are Liquid templates rendered per recipient. Always include {{ pingram.unsubscribe }}.
  • Recipients who already unsubscribed from type are skipped automatically.

Audience

audience takes exactly one of:

filter — a Mongo-style query against your identified users. Best for segments and large audiences.

Allowed fields: email and properties.<key>. Allowed operators: $and, $or, $exists, $eq, $ne, $gt, $gte, $lt, $lte, $in. Nesting depth is at most 3, with at most 20 clauses.

Identify users first so property filters have data to match:

await pingram.user.identify('user-123', {
email: 'dana@acme.io',
properties: { plan: 'pro', firstname: 'Dana' }
});

emails — a raw list of addresses (max 10,000). Each address creates a user if one does not exist. For larger audiences, identify users and use filter.

Personalization

Subject and HTML are rendered with Liquid once per recipient. Custom fields stay under user.properties — use {{ user.properties.firstname }}, not {{ user.firstname }}.

Tag Source
{{ user.id }} Your user id. For audience.emails, this is the lowercased email.
{{ user.email }} Email address
{{ user.properties.<key> }} Custom properties from user.identify
{{ pingram.unsubscribe }} Per-recipient unsubscribe URL for this broadcast’s type

Missing properties render as empty. Use | default: when you need a fallback. audience.emails only guarantees user.id and user.email unless those users were identified first.

Send or schedule

await pingram.broadcasts.send(broadcast.broadcastId);
await pingram.broadcasts.schedule(broadcast.broadcastId, {
sendAt: '2026-10-01T15:00:00.000Z'
});

Send requires a non-empty subject and a type. Cancel a scheduled broadcast to return it to draft.

INFO

Verify your sending domain for full-speed delivery. Broadcasts from unverified domains, including the shared default sender, are heavily throttled. See Domain Verification.

Metrics

const metrics = await pingram.broadcasts.getMetrics(broadcast.broadcastId);
// status, total, sent, delivered, opened, clicked,
// bounced, complained, unsubscribed, skipped, failed

Status moves draft → sending → sent (or scheduled, paused, canceled). If a broadcast pauses itself, pausedReason explains why.