# Broadcasts

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

Source: https://docs.pidgely.com/guides/broadcasts/

A broadcast sends the same content to every subscribed contact of a [segment](https://docs.pidgely.com/guides/contacts/#segments), 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

```js tab="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 tab="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 tab="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 tab="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 tab="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
```

```bash tab="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](https://docs.pidgely.com/api-reference/broadcasts/send/) now or later:

```js tab="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 tab="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 tab="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 tab="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 tab="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
```

```bash tab="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](https://docs.pidgely.com/guides/domains/). `scheduled_at` takes ISO 8601 or [natural language](https://docs.pidgely.com/guides/sending/#scheduling), 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](https://docs.pidgely.com/guides/contacts/#contacts), 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](https://pidgely.com/blog/one-click-unsubscribe-rfc-8058/) requires.

> [!IMPORTANT]
> Pidgely does **not** add an unsubscribe link to your HTML. Include `{{{PIDGELY_UNSUBSCRIBE_URL}}}` in a visible link: it is required by law in many countries and by the bulk-sender rules of Gmail and Yahoo.

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](https://docs.pidgely.com/guides/suppressions/) 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](https://docs.pidgely.com/guides/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](https://docs.pidgely.com/api-reference/broadcasts/update/) | `draft` (otherwise `409 broadcast_not_draft`) |
| [Send](https://docs.pidgely.com/api-reference/broadcasts/send/) | `draft` |
| [Cancel](https://docs.pidgely.com/api-reference/broadcasts/cancel/) | `scheduled` or `queued`; not once `sending` (`409 broadcast_not_cancelable`) |
| [Delete](https://docs.pidgely.com/api-reference/broadcasts/delete/) | not `scheduled`, `queued` or `sending` (`409 broadcast_in_progress`) |
| [Duplicate](https://docs.pidgely.com/api-reference/broadcasts/duplicate/) | always; creates a new draft without schedule |

[Retrieve a broadcast](https://docs.pidgely.com/api-reference/broadcasts/retrieve/) to see its `status`, `recipients` and `stats` (deliveries by status).

## Quotas

Broadcast messages count toward your [sending quotas](https://docs.pidgely.com/rate-limits/#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](https://docs.pidgely.com/rate-limits/#reputation-guard), the broadcast waits; if the account is suspended or validation fails, it ends as `failed`.
