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. 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 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.
3. Preview it
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 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
{ "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.
{
"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
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 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.
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.