Instant Quote · Admin

Docs

Project documentation rendered straight from /docs.

docs/NOTIFICATION_TEMPLATES.md

Notification 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:

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 reuses buildZapierPayload).
  • 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 NULL
  • idx_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/templates renders the in-code defaults from lib/templates.js#DEFAULT_TEMPLATES so 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:

  • type must be one of the known values.
  • body is required.
  • Email templates require subject too.
  • 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:

  1. A sender (Twilio / Resend / SES / etc.)
  2. A per-client recipient field (already on client_integrations: notification_email and notification_phone — currently dead).
  3. A small queue / retry layer (mirror Zapier's delivery_attempts table or just extend it with a new provider value).

Until then, Zapier is still the only delivery path and templates are read-only-from-the-system, copy-paste-from-admin.