Contact Form with Klaviyo
Introduction
This guide shows you how to build a contact form as a Weaverse section whose submissions are emailed to a notification inbox through a Klaviyo API-based transactional flow. Instead of sending mail directly from your storefront (which requires an SMTP provider, deliverability tuning, and secret management), the form fires a single Klaviyo event. A Klaviyo flow listens for that event’s metric and renders a transactional email. You get spam protection (hCaptcha), a tamper-resistant recipient, and a merchant-editable form — without running your own mail infrastructure.Architecture at a glance
profile.email is your notification inbox; the submitter’s details ride along as event properties that the flow email template renders.
Prerequisites
1
Klaviyo private API key
A private key with the
events:write scope (Full access also works but is broader than needed). Create it in Klaviyo under Settings → API keys → Create Private API Key → Custom Key. You only see the key once.2
hCaptcha keys
A site key and secret key from hCaptcha. The Pilot theme already uses hCaptcha for the newsletter form, so you can reuse the same pair.
3
Environment variables
Set the following on Oxygen (or in
.env for local dev, then npx shopify hydrogen env pull):Step 1 — Create the API route
The form posts to a resource route atapp/routes/api/contact.ts. The action does three things: verify the captcha, resolve a safe recipient, then POST a Klaviyo event. Only the parts that carry the integration logic are shown below — the rest (reading form fields, 400s for missing email/message/captcha) is routine.
Resolve a safe recipient. The section’s recipient arrives as a tamperable form field, so it’s only honored when it passes the optional allowlist (see the security model):
profile.email is the recipient (your inbox), and the submitter’s data goes in properties:
useFetcher:
Why the Klaviyo payload looks like this
A successful Create Event call returns HTTP 202 with an empty body.
Step 2 — Register the route
Add the route inside theapi prefix block in app/routes.ts:
POST /api/contact (and /:locale/api/contact).
Step 3 — Build the Weaverse section
The section atapp/sections/contact-form/index.tsx is a standard Weaverse component — a default-exported component plus a schema. Below are the integration-specific pieces; the rest is ordinary form markup (name / email / message / optional checkbox / dropdown).
Submit with useFetcher and gate on the captcha token:
metric and recipient as hidden fields plus the captcha token:
metricName and recipientEmail in the schema so merchants can point the form at the right Klaviyo flow and inbox without touching code (other inputs — labels, dropdown options, button text — follow the same pattern):
The hCaptcha site key is read from the root loader (
rootData.hCaptchaSiteKey, sourced from PUBLIC_HCAPTCHA_SITE_KEY). The Pilot theme already exposes this for its newsletter form. If your root loader does not return it yet, add it there — the section renders without a captcha when the key is absent, and the server rejects captcha-less submissions, so you’d be blocked until the key is wired up.Step 4 — Register the section
Add the section toapp/weaverse/components.ts so the Weaverse builder can discover it:
Step 5 — Declare the environment types
Add the new variables to theEnv interface in env.d.ts so context.env.* is typed:
Step 6 — Configure the Klaviyo flow
Code alone won’t deliver mail — the event needs a flow that produces a transactional email.1
Trigger the metric once
Submit the form once (or use the curl smoke test below) so the
Contact Form Submission metric appears in Analytics → Metrics. You can also pre-create it to build the flow first.2
Create the flow
Flows → Create Flow → Create from Scratch. Set the trigger to Metric → Contact Form Submission. Leave the trigger filter empty so every submission emails you. Drag a Flow Email action in right after the trigger.
3
Template the email with event variables
Reference the event properties the action sends:
4
Mark the email transactional
Edit email → Settings → Apply for transactional status / This is a transactional email → ON. This is what lets the email deliver regardless of marketing consent. Then set Smart Sending OFF — every submission goes to the same inbox, so smart sending would silently drop repeat notifications inside its window.
5
Go live
Save the flow and switch it from Draft → Live.
Event property → template variable map
Security model: the recipient allowlist
The “Notification recipient email” section setting is convenient for merchants but is a public, tamperable input — it travels as a normal form field, so anyone can POST/api/contact with an arbitrary recipient. Without a guard, your endpoint becomes an open mail relay.
resolveRecipient() resolves the address in this order:
- The section’s
recipientfield — only if it’s a valid email and (whenCONTACT_FORM_ALLOWED_RECIPIENTSis set) it appears in that allowlist. CONTACT_FORM_RECIPIENT_EMAIL(server-side env).- The hardcoded
FALLBACK_RECIPIENTconstant.
Step 7 — Test end to end
- Open the page on the storefront (or
npm run dev), fill the form, complete hCaptcha, submit. - The fetch to
/api/contactshould return 202; the form resets and the success banner shows. - In Klaviyo: Analytics → Metrics → Contact Form Submission shows the event within ~30s, and Flows → your flow → Analytics shows one recipient processed.
- Check the resolved recipient inbox.
Troubleshooting
Related
Creating Components
Weaverse section fundamentals — the default export + schema pattern used here.
Custom Routing
How resource routes like
/api/contact are defined and matched.Third-party Integrations
General patterns for wiring external APIs into Weaverse themes.
Input Settings
Every schema input type used in the contact form’s settings.
Reference links
- Klaviyo: Guide to setting up API-based transactional events
- Klaviyo API: Create Event
- Klaviyo: API overview and authentication
- Klaviyo: Help Center
- Klaviyo help: Use event variables in flow emails
- hCaptcha: Server-side verification
- Shopify: Hydrogen environments and variables