Skip to main content

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

The submitter is not the email recipient. The event’s 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 at app/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):
Verify hCaptcha server-side before sending anything to Klaviyo:
Build and POST the Klaviyo event — the core of the integration. Note profile.email is the recipient (your inbox), and the submitter’s data goes in properties:
The action exports a result type the section reads via 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 the api prefix block in app/routes.ts:
This makes the action reachable at POST /api/contact (and /:locale/api/contact).

Step 3 — Build the Weaverse section

The section at app/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:
The form posts to the route, carrying metric and recipient as hidden fields plus the captcha token:
Expose 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 to app/weaverse/components.ts so the Weaverse builder can discover it:

Step 5 — Declare the environment types

Add the new variables to the Env 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.
Branded sending domain required for production. Klaviyo will skip transactional sends whose from address is not on your verified branded sending domain (“Sender email addresses need to use your branded sending domain”). Set this up in Klaviyo under Settings → Domains and complete DNS (CNAME) verification before launch, or notifications will silently stop.

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:
  1. The section’s recipient field — only if it’s a valid email and (when CONTACT_FORM_ALLOWED_RECIPIENTS is set) it appears in that allowlist.
  2. CONTACT_FORM_RECIPIENT_EMAIL (server-side env).
  3. The hardcoded FALLBACK_RECIPIENT constant.
Always set CONTACT_FORM_ALLOWED_RECIPIENTS in production. With it unset, any well-formed email configured in the section is trusted — fine for local dev, unsafe for a public storefront. hCaptcha raises the cost of automated abuse, but the allowlist is what actually contains where mail can go.

Step 7 — Test end to end

  1. Open the page on the storefront (or npm run dev), fill the form, complete hCaptcha, submit.
  2. The fetch to /api/contact should return 202; the form resets and the success banner shows.
  3. In Klaviyo: Analytics → Metrics → Contact Form Submission shows the event within ~30s, and Flows → your flow → Analytics shows one recipient processed.
  4. Check the resolved recipient inbox.
Curl smoke test (bypasses hCaptcha — only meaningful if you temporarily comment out the captcha check while debugging; it hits Klaviyo directly):
A successful call returns HTTP 202 with an empty body.

Troubleshooting

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.