Pharos
API reference.md

Email templates

Stored templates are the per-recipient path. Their HTML is carried onto a campaign and interpolated at send time, which is the only place {{tokens}} resolve.

Branding — logo, colours, name, mailing address — is baked into a template's HTML when the starter is seeded. It is not a runtime token.

GET /api/email-templates

Every active template in the project, most recently updated first.

Scope: templates:read

Parameter In Type Notes
projectId query string Required.
{
  "templates": [
    {
      "id": "tpl_…",
      "projectId": "acme-app",
      "name": "Monthly digest",
      "description": null,
      "category": "custom",
      "subjectEn": "What shipped in July",
      "htmlEn": "<html>…</html>",
      "textEn": "…",
      "subjectPt": null,
      "htmlPt": null,
      "textPt": null,
      "status": "active",
      "createdAt": "2026-07-02T11:00:00.000Z",
      "updatedAt": "2026-07-29T16:40:00.000Z"
    }
  ]
}

Archived templates are not returned.

POST /api/email-templates

Create a template. English is canonical and required; Portuguese is optional and falls back to English at render time.

Scope: templates:write

Field Type Notes
projectId string Required.
name string Required.
en object Required. { subject, html, text? }. text falls back to stripped HTML.
pt object Optional { subject, html, text? }.
description string
category string Defaults to custom.
curl -X POST https://pharosbase.com/api/email-templates \
  -H "Authorization: Bearer $PHAROS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "projectId": "acme-app",
    "name": "Monthly digest",
    "en": {
      "subject": "What shipped in July",
      "html": "<p>Hi {{contact.firstName|there}} — here is what changed.</p><p><a href=\"{{unsubscribeUrl}}\">Unsubscribe</a></p>"
    }
  }'

201 returns the created { "template": { … } } row. Invalid locale content returns 400 with the offending locale named (EN: … / PT: …).

GET /api/email-templates/{id}

One template in full.

Scope: templates:read

PATCH /api/email-templates/{id}

Update in place — iterate on copy without minting a new template row for every revision.

Scope: templates:write

Field Type Notes
name string
description string
category string
en object Partial — only the keys you pass change.
pt object | null Partial. Pass null to drop the Portuguese localization entirely.

DELETE /api/email-templates/{id}

Archive the template. It stops appearing in GET /api/email-templates; sends that already went out keep their recorded subject and template id.

Scope: templates:write

Tokens

These resolve in stored template HTML at send time. Unknown tokens resolve to an empty string rather than erroring.

Token Resolves to Always
{{unsubscribeUrl}} Per-recipient one-click unsubscribe URL Yes
{{project.unsubscribeUrl}} Alias of {{unsubscribeUrl}} Yes
{{contact.email}} Recipient's email address Yes
{{contact.firstName}} First name; empty when unknown Yes
{{contact.lastName}} Last name; empty when unknown Yes
{{subject}} Campaign subject Legacy
{{body}} Campaign body Legacy
{{project.name}} Project name Legacy
{{project.logo}} Project logo URL Legacy
{{project.primaryColor}} Primary brand colour Legacy

The legacy tokens exist for older templates; branding is baked in at seed time, so new templates should not reach for them.

A token may carry a fallback for when its value is empty or missing, written {{contact.firstName|there}}. Values are HTML-escaped in the HTML part and inserted raw in the plain-text part.

Tokens do not resolve in the content body passed to POST /api/send against a built-in React template. That copy is rendered literally, so {{contact.email}} written there ships as those exact characters. Per-recipient interpolation requires a stored template.

Every send needs a working unsubscribe path. Include {{unsubscribeUrl}} in any template that goes to a list.