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
contentbody passed toPOST /api/sendagainst 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.