Pharos
Guides.md

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.