docs/NOTIFICATION_TEMPLATES.mdNotification Templates
Last updated: May 19, 2026 at 4:17 PM ET
Canonical SMS + email lead-notification copy, managed in the admin and copy-pasted into Zapier today (with native delivery planned later). Zapier is still the delivery layer — these templates exist so the team has a single source of truth for what an "instant quote lead" notification reads like, regardless of which client or which Zapier step is sending it.
Related docs:
- INTEGRATIONS — Zapier delivery model + payload
- ADMIN_GUIDE — admin nav + general layout
Where to find it
Admin nav → Templates → /admin/templates.
Template types (V1)
Two types ship today; the system supports adding more without a schema migration.
| Type | Has subject? | Notes |
|---|---|---|
SMS lead alert (sms_lead_alert) |
No | Short text body. Soft limit 320 chars. |
Email lead alert (email_lead_alert) |
Yes | Subject + multi-line body. No length limit. |
Planned later (not built):
- Client-facing quote email
- Thank-you page copy override
- Landing page section presets
Variable system
Templates use {{section.field}} Mustache-style variables. The
data shape they render against is the same shape Zapier
receives from
lib/zapier.js#buildZapierPayload — so a token that resolves in
the preview will also resolve in your Zap.
Available variables
Grouped exactly as Zapier sees them. Click any token on the admin Variables panel to copy it.
Client
{{client.company_name}}{{client.slug}}
Lead
{{lead.first_name}}·{{lead.last_name}}·{{lead.full_name}}{{lead.phone}}·{{lead.email}}
Property
{{property.street_address}}·{{property.city}}·{{property.state}}·{{property.zip}}{{property.full_property_address}}
Quote
{{quote.quote_number}}{{quote.estimate_low}}·{{quote.estimate_high}}·{{quote.estimate_midpoint}}{{quote.monthly_financing_estimate}}{{quote.roof_square_feet}}·{{quote.selected_material}}{{quote.stories}}·{{quote.roof_age}}·{{quote.roof_complexity}}
Qualification
{{qualification.timeline}}·{{qualification.financing_interest}}
Tracking
{{tracking.source_url}}·{{tracking.utm_source}}·{{tracking.utm_medium}}{{tracking.utm_campaign}}·{{tracking.utm_content}}·{{tracking.utm_term}}{{tracking.gclid}}·{{tracking.fbclid}}
Submission
{{submission.id}}·{{submission.submitted_at}}{{submission.is_test}}·{{submission.status}}
Behaviour
- Missing values render as empty string — never
undefined. Zapier behaves the same way when a payload key is null, so the preview matches what the Zap will actually send. - Unknown tokens (e.g. typos like
{{lead.fullname}}instead of{{lead.full_name}}) render as empty string but the admin UI flags them with an amber "Unknown variables won't resolve at send time" warning so you can fix them before saving. - The renderer is intentionally simple — no conditionals, no loops, no formatters. Templates are short; complexity belongs in Zapier's downstream logic if you need it.
Editor + preview
- Edit the body (and subject for email) directly in the textarea. Variables update the preview live.
- Preview panel on the right renders the template against a
synthetic lead — same shape Zapier sees. Sample data is
generated by
lib/templates.js#sampleTemplatePayload()(which reusesbuildZapierPayload). - Copy SMS / Copy subject / Copy body buttons in the preview
card put the raw template text (with
{{vars}}intact) on the clipboard. Use these when pasting into Zapier actions — Zapier resolves the variables from the live Catch Hook payload. - Reset to default restores the in-code default for the active template. Doesn't save automatically; click Save template to persist.
How templates relate to Zapier
| Step | What you do | Where |
|---|---|---|
| 1 | Build the Zap and add a Catch Hook | Zapier |
| 2 | Paste the Catch Hook URL on the client's Integrations tab | /admin/clients/[id] → Integrations |
| 3 | Run one real (or test-mode) submission to populate Zapier's sample data | /<slug>/instant-quote?test=true |
| 4 | Add the SMS / email action to your Zap | Zapier |
| 5 | Copy the rendered text from /admin/templates and paste into the Zap action's body field |
This page |
| 6 | Map the {{var}} tokens to the Catch Hook payload fields inside Zapier |
Zapier |
| 7 | Turn the Zap on | Zapier |
Both steps 5 and 6 are essentially "paste plus map" — Zapier's
template syntax recognises {{var}} if you swap it for Zapier's
own field references, OR you can paste the rendered text and let
Zapier auto-replace as you map each variable.
Database
Migration 017 adds the templates table:
| Column | Type | Notes |
|---|---|---|
id |
uuid | Primary key. |
type |
text | Open-vocabulary (sms_lead_alert, email_lead_alert, future types). |
scope |
text | global today; client reserved for future per-client overrides. |
client_id |
uuid (nullable) | Always null for global rows; non-null for per-client. |
subject |
text (nullable) | Only used by email templates. |
body |
text | Required. |
updated_at, created_at |
timestamptz | Trigger-maintained. |
Two partial unique indexes enforce one row per (type, scope) combo:
idx_templates_global_unique—(type) WHERE scope='global' AND client_id IS NULLidx_templates_client_unique—(type, client_id) WHERE scope='client'
This lets client overrides ship later without a schema change —
just a new endpoint that writes scope='client' + client_id.
Migration tolerance
Like every other admin feature on this branch, the UI handles migration 017 not yet applied gracefully:
/admin/templatesrenders the in-code defaults fromlib/templates.js#DEFAULT_TEMPLATESso admins can still preview + copy.- The Save button is disabled with a tooltip explaining the missing migration.
- An amber banner at the top of the page names the migration file to run.
Endpoints
| Method | Path | Purpose |
|---|---|---|
GET |
/api/admin/templates |
Returns merged template list (DB rows + in-code defaults). |
PUT |
/api/admin/templates |
Upserts one template by { type, subject?, body }. |
Both gated by ADMIN_TOKEN bearer. Validation:
typemust be one of the known values.bodyis required.- Email templates require
subjecttoo. - Non-https / bad URLs aren't a concern here (no URL fields).
Future native delivery
When we build native SMS / email delivery (probably after the team stops doing day-to-day Zap maintenance), the same templates table will be the source of truth. The renderer is already generic — it takes the Zapier payload shape and a template string and returns the rendered output. Native delivery just needs:
- A sender (Twilio / Resend / SES / etc.)
- A per-client recipient field (already on
client_integrations:notification_emailandnotification_phone— currently dead). - A small queue / retry layer (mirror Zapier's
delivery_attemptstable or just extend it with a newprovidervalue).
Until then, Zapier is still the only delivery path and templates are read-only-from-the-system, copy-paste-from-admin.