# Sending a campaign

A campaign is one broadcast to many people. The path from draft to delivered
runs through a preview step that resolves *exactly* who would receive it, and
that step is separately scoped — so an agent can walk the whole flow up to the
last move and not make it.

For the endpoint contract, see
[Campaigns & sending](/docs/api-campaigns). This page is the flow.

## 1. Pick a rendering path

There are two, and choosing wrong is the single most common mistake.

**Built-in template + literal copy.** Pass a `template` name and a `content`
body. Fast, good for a one-off announcement. The copy is rendered **literally**
— `{{contact.firstName}}` written here ships as those exact characters.

**Stored template.** Create a template with
[`POST /api/email-templates`](/docs/api-templates#post-apiemail-templates) and
pass its rendered HTML. Tokens resolve per recipient at send time, so this is
the only path that can greet people by name.

> If your mail went out saying "Hi `{{contact.firstName}}`", you used the first
> path and wanted the second.

## 2. Decide who gets it

`recipientMode` takes one of four forms:

| Mode | Reaches |
| --- | --- |
| `all` | Every subscribed contact in the project |
| `list:<slug>` | Everyone subscribed to that list |
| `tag:<tag>` | Every subscribed contact carrying that tag |
| `test:<email>` | Exactly one address, no campaign created |

Unsubscribed and suppressed contacts are never included, whichever mode you
pick. List slugs come from
[`GET /api/lists`](/docs/api-lists#get-apilists).

## 3. Preview it

```bash
curl -X POST https://pharosbase.com/api/send \
  -H "Authorization: Bearer $PHAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "acme-app",
    "template": "newsletter",
    "subject": "What shipped in July",
    "content": "Here is what changed this month.",
    "recipientMode": "list:newsletter",
    "mode": "preview"
  }'
```

You get the recipient count, a real sample recipient, and the fully rendered
subject, HTML and text. Nothing is written: no campaign row, no queued batches,
no send events.

The render uses an actual recipient rather than a placeholder, so the preview
shows the language that person would really receive. That matters on a
bilingual list — see [Localization](#localization) below.

**Read the count before anything else.** `recipientCount` is the number of
people who will receive this. If it is larger than you expected, your
`recipientMode` is wider than you think, and preview is the last cheap moment
to find out.

A count of zero comes back with a `warning` instead of a render.

## 4. Send a test

```json
{ "recipientMode": "test:you@example.com" }
```

One real email, no campaign row. This is the only step that exercises the
sending credential end to end, so it is worth doing even when the preview
looked right — a preview cannot tell you your provider credential is wrong.

If the response says `"mock": true`, no credential is configured and nothing
left the building.

## 5. Send it

Drop `mode` and use a non-`test:` recipient mode. Add `scheduledAt` (ISO 8601)
to queue it for later instead.

```json
{
  "success": true,
  "campaignId": "cmp_…",
  "status": "queued",
  "recipientCount": 4102,
  "batchesQueued": 293
}
```

This requires `campaigns:send`, which agent keys deliberately lack.

`queued` means accepted, **not delivered**. Recipients are sent in batches
paced to your provider's rate limit — 14 per batch on SES, 2 on Resend, about a
second apart — so a large campaign lands over minutes, not instantly.

The campaign row walks `draft → scheduled → sending → sent`, or ends at
`failed`.

## 6. Watch what happened

```bash
curl "https://pharosbase.com/api/sends?projectId=acme-app&campaignId=cmp_…" \
  -H "Authorization: Bearer $PHAROS_API_KEY"
```

One row per recipient, each moving `queued → sent → delivered` and possibly
ending at `bounced` or `complained`.
[`GET /api/campaigns`](/docs/api-campaigns#get-apicampaigns) gives the same
thing rolled up per campaign.

If every row sits at `sent` and never advances, your provider's delivery
webhook is not configured — see
[Sending & deliverability](/docs/sending-providers#delivery-tracking).

## Localization

A campaign carries English and Portuguese variants side by side: `subject` and
`content` for English, `subjectPt` and `bodyPt` for Portuguese. Each recipient
gets the variant matching their contact language, falling back to English when
a Portuguese variant is missing.

One campaign, one send, two languages — rather than two campaigns and a
segmented list you have to keep in sync.

## What an agent can do here

With an agent key, a model can do steps 1 through 3 in full: write the copy,
create the template, resolve the recipients, and render the final HTML. It
cannot do step 4 or 5, because `campaigns:send` is not in its key.

That is the intended division of labour. The model drafts and shows its work;
a human looks at the preview and the recipient count, and presses send.
