Guides
Broadcasts
Send campaigns to a segment with personalization variables, the {{{PIDGELY_UNSUBSCRIBE_URL}}} link and RFC 8058 one-click unsubscribe.
A broadcast sends the same content to every subscribed contact of a segment, personalized per contact. Broadcasts go out on the broadcast stream (separate IP pools from transactional mail) and every message carries one-click unsubscribe headers.
Create and send
import { Pidgely } from 'pidgely';
const pidgely = new Pidgely(process.env.PIDGELY_API_KEY);
const { data, error } = await pidgely.broadcasts.create({
segmentId: '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48',
name: 'October product update',
from: 'Acme <news@acme.com>',
subject: 'What shipped in October, {{{FIRST_NAME|friend}}}',
previewText: 'Five new features and a faster dashboard',
html: '<p>Hi {{{FIRST_NAME|there}}},</p><p>Your plan: {{{contact.plan|free}}}.</p><p><a href="{{{PIDGELY_UNSUBSCRIBE_URL}}}">Unsubscribe</a></p>',
});
console.log(data ?? error);import os
import pidgely
pidgely.api_key = os.environ["PIDGELY_API_KEY"]
result = pidgely.Broadcasts.create({
"segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48",
"name": "October product update",
"from": "Acme <news@acme.com>",
"subject": "What shipped in October, {{{FIRST_NAME|friend}}}",
"preview_text": "Five new features and a faster dashboard",
"html": "<p>Hi {{{FIRST_NAME|there}}},</p><p>Your plan: {{{contact.plan|free}}}.</p><p><a href=\"{{{PIDGELY_UNSUBSCRIBE_URL}}}\">Unsubscribe</a></p>",
})
print(result)<?php
require __DIR__ . '/vendor/autoload.php';
$pidgely = Pidgely::client(getenv('PIDGELY_API_KEY'));
$result = $pidgely->broadcasts->create([
'segment_id' => '0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48',
'name' => 'October product update',
'from' => 'Acme <news@acme.com>',
'subject' => 'What shipped in October, {{{FIRST_NAME|friend}}}',
'preview_text' => 'Five new features and a faster dashboard',
'html' => '<p>Hi {{{FIRST_NAME|there}}},</p><p>Your plan: {{{contact.plan|free}}}.</p><p><a href="{{{PIDGELY_UNSUBSCRIBE_URL}}}">Unsubscribe</a></p>',
]);
var_dump($result);package main
import (
"context"
"fmt"
"os"
"github.com/pidgely/pidgely-go"
)
func main() {
ctx := context.Background()
client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY"))
result, err := client.Broadcasts.Create(ctx, &pidgely.CreateBroadcastRequest{
SegmentId: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48",
Name: "October product update",
From: "Acme <news@acme.com>",
Subject: "What shipped in October, {{{FIRST_NAME|friend}}}",
PreviewText: "Five new features and a faster dashboard",
Html: "<p>Hi {{{FIRST_NAME|there}}},</p><p>Your plan: {{{contact.plan|free}}}.</p><p><a href=\"{{{PIDGELY_UNSUBSCRIBE_URL}}}\">Unsubscribe</a></p>",
})
if err != nil {
panic(err)
}
fmt.Println(result)
}require "pidgely"
Pidgely.api_key = ENV["PIDGELY_API_KEY"]
result = Pidgely::Broadcasts.create({
segment_id: "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48",
name: "October product update",
from: "Acme <news@acme.com>",
subject: "What shipped in October, {{{FIRST_NAME|friend}}}",
preview_text: "Five new features and a faster dashboard",
html: "<p>Hi {{{FIRST_NAME|there}}},</p><p>Your plan: {{{contact.plan|free}}}.</p><p><a href=\"{{{PIDGELY_UNSUBSCRIBE_URL}}}\">Unsubscribe</a></p>"
})
puts resultcurl -X POST 'https://api.pidgely.com/broadcasts' \
-H "Authorization: Bearer $PIDGELY_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"segment_id": "0199d7f5-9a3e-7d4b-a1c2-7e5f0b3d6c48",
"name": "October product update",
"from": "Acme <news@acme.com>",
"subject": "What shipped in October, {{{FIRST_NAME|friend}}}",
"preview_text": "Five new features and a faster dashboard",
"html": "<p>Hi {{{FIRST_NAME|there}}},</p><p>Your plan: {{{contact.plan|free}}}.</p><p><a href=\"{{{PIDGELY_UNSUBSCRIBE_URL}}}\">Unsubscribe</a></p>"
}'segment_id (or the legacy audience_id), from and subject are required. The broadcast is saved as a draft; then send it now or later:
import { Pidgely } from 'pidgely';
const pidgely = new Pidgely(process.env.PIDGELY_API_KEY);
const { data, error } = await pidgely.broadcasts.send('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19', {
scheduledAt: 'tomorrow at 9am',
});
console.log(data ?? error);import os
import pidgely
pidgely.api_key = os.environ["PIDGELY_API_KEY"]
result = pidgely.Broadcasts.send("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", {
"scheduled_at": "tomorrow at 9am",
})
print(result)<?php
require __DIR__ . '/vendor/autoload.php';
$pidgely = Pidgely::client(getenv('PIDGELY_API_KEY'));
$result = $pidgely->broadcasts->send('0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19', [
'scheduled_at' => 'tomorrow at 9am',
]);
var_dump($result);package main
import (
"context"
"fmt"
"os"
"github.com/pidgely/pidgely-go"
)
func main() {
ctx := context.Background()
client := pidgely.NewClient(os.Getenv("PIDGELY_API_KEY"))
result, err := client.Broadcasts.Send(ctx, "0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", &pidgely.SendBroadcastRequest{
ScheduledAt: "tomorrow at 9am",
})
if err != nil {
panic(err)
}
fmt.Println(result)
}require "pidgely"
Pidgely.api_key = ENV["PIDGELY_API_KEY"]
result = Pidgely::Broadcasts.send("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", {
scheduled_at: "tomorrow at 9am"
})
puts resultcurl -X POST 'https://api.pidgely.com/broadcasts/0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19/send' \
-H "Authorization: Bearer $PIDGELY_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"scheduled_at": "tomorrow at 9am"
}'To create and send in one call, pass "send": true (and optionally scheduled_at) to POST /broadcasts. Sending requires a segment, a non-empty subject, html or text, and a from on a verified domain. scheduled_at takes ISO 8601 or natural language, within 30 days.
Personalization
Variables use triple braces, with an optional fallback after |:
| Variable | Value |
|---|---|
{{{FIRST_NAME}}}, {{{contact.first_name}}} |
Contact's first name |
{{{LAST_NAME}}}, {{{contact.last_name}}} |
Contact's last name |
{{{EMAIL}}}, {{{contact.email}}} |
Contact's email address |
{{{contact.<property>}}} |
Any contact property, for example {{{contact.plan}}} |
{{{PIDGELY_UNSUBSCRIBE_URL}}} |
This recipient's unsubscribe link ({{{UNSUBSCRIBE_URL}}} is an alias) |
{{{FIRST_NAME|there}}}usestherewhen the value is missing or empty. A variable with no value and no fallback renders as an empty string.- Values are HTML-escaped in
htmlandpreview_text, inserted as is insubjectandtext. Line breaks in the subject become spaces. - Variables work in
subject,html,textandpreview_text.preview_textis added as hidden text at the top of the HTML.
Unsubscribe
Every broadcast message carries List-Unsubscribe and List-Unsubscribe-Post: List-Unsubscribe=One-Click headers (covered by DKIM), so Gmail, Yahoo and Apple Mail show their native unsubscribe button. Clicking it unsubscribes the contact without a confirmation step, as RFC 8058 requires.
The link opens a confirmation page (a GET never unsubscribes anyone, so link scanners are harmless) with a button that confirms. The link in the body and the header point to the same delivery. Unsubscribing adds an unsubscribe suppression and marks the contact unsubscribed: true.
Who receives it
- Contacts in the segment with
unsubscribed: false, processed in pages of 500. - Addresses with a bounce, complaint or manual suppression are skipped at delivery (
email.suppressed) and still appear inrecipients. - Each message is a normal email with the tag
broadcast_id, so delivery webhooks work as for any other email.
Lifecycle
draft ──send──► queued ──► sending ──► sent
└──send with scheduled_at──► scheduled ──(time)──► queued
scheduled / queued ──cancel──► canceled (errors) ──► failed| Action | Allowed when |
|---|---|
| Update | draft (otherwise 409 broadcast_not_draft) |
| Send | draft |
| Cancel | scheduled or queued; not once sending (409 broadcast_not_cancelable) |
| Delete | not scheduled, queued or sending (409 broadcast_in_progress) |
| Duplicate | always; creates a new draft without schedule |
Retrieve a broadcast to see its status, recipients and stats (deliveries by status).
Quotas
Broadcast messages count toward your sending quotas. When a quota runs out mid-send, the broadcast pauses and resumes automatically at 00:00 UTC the next day. If the account is paused by the reputation guard, the broadcast waits; if the account is suspended or validation fails, it ends as failed.