Skip to content
Pidgely Docs

Guides

Broadcasts

Send campaigns to a segment with personalization variables, the {{{PIDGELY_UNSUBSCRIBE_URL}}} link and RFC 8058 one-click unsubscribe.

View as Markdown

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

Node.js
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);
Python
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
<?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);
Go
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)
}
Ruby
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 result
cURL
curl -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:

Node.js
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);
Python
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
<?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);
Go
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)
}
Ruby
require "pidgely"

Pidgely.api_key = ENV["PIDGELY_API_KEY"]

result = Pidgely::Broadcasts.send("0199d7f6-4c8d-7e1a-9b3f-2d6a5c0e7f19", {
  scheduled_at: "tomorrow at 9am"
})
puts result
cURL
curl -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}}} uses there when the value is missing or empty. A variable with no value and no fallback renders as an empty string.
  • Values are HTML-escaped in html and preview_text, inserted as is in subject and text. Line breaks in the subject become spaces.
  • Variables work in subject, html, text and preview_text. preview_text is 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 in recipients.
  • Each message is a normal email with the tag broadcast_id, so delivery webhooks work as for any other email.

Lifecycle

Text
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.