Validate emails in Typeform
Check the email answers of your Typeform responses. A small relay that you run in your own Cloudflare account receives each new response from Typeform's webhook, checks the webhook's signature, validates every email answer and reports the result. A clean script checks the responses a form already has and writes a CSV of results. Both use your own API key and need no app to install.
What it can and cannot do
- After the submission, not before. Typeform has already accepted a response when its webhook fires, so the relay cannot block or undo a submission. The result arrives a few seconds later, for you to act on: in a log, at a URL of yours, or in Slack.
- Reported, not written onto the response. Typeform's public API has no call to tag or change a response, so the result is not stored in Typeform. The reports and the CSV carry the response id, so you can match them up.
- Before the form is submitted see below: Typeform's Email question checks only the format of an address.
Before you start
- An API key. Create one on the API keys page with the permission "Validate addresses" (add "Bulk lists" if the clean script will check more than 50 answers), and give it a daily credit cap to limit what a busy form can spend.
- A webhook secret. A random string of at least 32 characters, for example the output of
openssl rand -hex 32. You enter it in Typeform and in the relay. - A Cloudflare account for the relay (Workers' free plan runs it), or any server that can run a web
fetchhandler.
Set up the relay
- Download the relay: relay.js, one file built from the tested source. Deploy it with Wrangler:
npx wrangler deploy relay.js --name ev-typeform-relay --compatibility-date 2026-10-01. Or, in the Cloudflare dashboard, open Workers & Pages → Create → Create Worker, name it, for example,ev-typeform-relay, deploy it, choose Edit code, replace the code with the contents ofrelay.jsand deploy again. - In the Worker's Settings → Variables and Secrets, add two secrets:
EV_API_KEY(your API key) andTYPEFORM_WEBHOOK_SECRET(the webhook secret). Add any of the optional settings. - In Typeform, open the form, then Connect → Webhooks → Add a webhook. Paste the Worker's URL, for example
https://ev-typeform-relay.<your-subdomain>.workers.dev/. In the webhook's settings, set Secret to the same value asTYPEFORM_WEBHOOK_SECRET, save, and turn the webhook on. - Submit the form once with the test address
no_mailbox@sandbox.email-validation.org. It costs nothing and contacts no mail server. The Worker's log (Logs in the dashboard, ornpx wrangler tail) showsinvalidwith the reasonno_mailbox.
Typeform's Send test request button sends a sample response. The relay checks its email answer like any other, which can use a credit, so test with your own submission and a test address instead.
Settings
| Setting | What it does |
|---|---|
EV_API_KEY | Your API key (required, a secret) |
TYPEFORM_WEBHOOK_SECRET | The webhook's secret, at least 32 characters (required, a secret) |
EV_REGION | eu to have addresses checked and stored in the EU (default us) |
FORM_IDS | Comma-separated form ids to accept; deliveries of other forms are ignored |
EMAIL_FIELDS | Comma-separated refs or ids of the Email questions to check (default: every Email question) |
FORWARD_URL | An https URL of yours that receives each response's results as JSON |
FORWARD_SECRET | Signs the forward with the header X-EV-Signature (a secret) |
SLACK_WEBHOOK_URL | A Slack incoming webhook URL, starting https://hooks.slack.com/ (a secret) |
NOTIFY_ON | all (default), or problems to forward and post only results whose action is not send |
INCLUDE_EMAIL | true to put the address in the forward and the Slack message (default: ids and results only) |
EVENTS | Optional: a KV namespace binding; the relay remembers there each delivery it has answered, for a day |
What you get
Each result names the Email question (its field id and ref) and holds the check's status (one of the five result statuses), reason and action (send, send_with_caution, remove or retry_later). Spam traps and known complainers are valid with the action remove, so act on action rather than on status.
{
"event": "typeform.validated",
"form_id": "aBcD1234",
"response_id": "…",
"event_id": "…",
"results": [
{ "field_id": "…", "field_ref": "work_email", "status": "invalid", "reason": "no_mailbox", "action": "remove" }
]
}- The log. One line per response with the form id, the response id and the results, never the address.
- Your URL.
FORWARD_URLreceives aPOSTwith the JSON above. WithFORWARD_SECRETset, the headerX-EV-Signatureholdssha256=and the base64 HMAC-SHA256 of the body under that secret, the same scheme as Typeform's own signature: check it before you trust the body. - Slack. One message per response, listing each answer's status, reason and action.
How the relay answers Typeform
The relay accepts a delivery only when its Typeform-Signature header is the HMAC-SHA256 of the exact body under your secret, compared in constant time. Typeform tries again after some answers and turns the webhook off after others, so the relay answers like this:
| Situation | Relay answers |
|---|---|
| Checked (also when a report to your URL or to Slack failed) | 200 with the results |
| No email answer, another form, or not a response | 200, nothing checked |
| A delivery already answered, or a response submitted more than a day ago | 200, nothing checked |
| Rate limit, outage or timeout before any check answered | 503 with Retry-After: Typeform delivers it again |
| Key refused, no credits left, or the key's daily cap | 422 with the error code |
| Missing or wrong signature | 401 |
| A setting is missing or wrong | 500 |
Once one check of a response has answered, the relay answers 200, so Typeform does not deliver that response again and no check is paid twice; the same delivery sent again is answered 200 without a check (remembered by the relay, and across all its instances when EVENTS is bound). A delivery answered 503 is checked again when Typeform delivers it again. The relay never answers 404 or 410, which make Typeform turn a webhook off at once. If every delivery fails for a day, Typeform turns the webhook off too: turn it back on and run the clean script with --since to catch up.
Clean the responses you already have
The clean script reads a form's completed responses through Typeform's Responses API, checks every email answer and writes a CSV with one row per answer. It holds no addresses: open it next to Typeform's own export and match the rows on the response id.
response_id,field_id,field_ref,status,reason,action,checked_at- In Typeform, open the menu next to your name → Account → Personal tokens → Generate a new token, with only the scope for reading responses.
- Download the clean script: clean.mjs, one file built from the tested source. It needs Node.js 22 or later and nothing else.
- Run it with the keys in the environment. The form id is the part after
/to/in the form's URL.
export EV_API_KEY=… TYPEFORM_TOKEN=…
node clean.mjs --form aBcD1234 --dry-run # counts the email answers it would check
node clean.mjs --form aBcD1234 # writes typeform-aBcD1234-results.csvOptions: --field REF (only that Email question; repeat for more), --since and --until (a date-time such as 2026-11-01T00:00:00Z), --out FILE, --region eu, and --typeform-api https://api.eu.typeform.com (or https://api.typeform.eu) for an account in Typeform's EU Data Center.
- Small or large. Up to 50 answers are checked one by one. More go into one bulk list: created without starting it, then started, which is when its credits are held, and its results downloaded when it is done.
- Never paid twice. A run skips every answer already in the output file, so running it again checks only new responses. A bulk list's id is saved next to the output file as soon as the list is created, and the next run resumes that list instead of creating a second one, for example after you buy credits or when it was still running when the script stopped waiting.
429and503answers, which are never charged, are tried again after theirRetry-Aftertime; a check that got no answer at all is left for the next run rather than sent again at once. - Typeform's limits. It reads at most two pages a second, Typeform's limit. Typeform's Responses API may leave out responses from roughly the last 30 minutes; the relay covers those, or the next run.
- Exit codes.
0done;1a check failed or the run stopped early (run it again);2wrong arguments;75the bulk list was still running (run it again later to collect the results).
Before the form is submitted
Typeform's Email question accepts any address that looks like one, and Typeform cannot call another service while someone fills in the form. Its logic jumps can branch on the text of an answer, for example to send answers that end with a domain you do not accept to a screen asking for another address, but that compares the text against your own list and checks nothing. To check an address before you accept it, check it in a sign-up form of your own: see Validate emails on sign-up forms.
Costs
Each completed check costs 1 credit, as with any validation request; unknown results, malformed addresses and test addresses are free. A bulk list costs 1 credit per unique address with a result other than unknown, and duplicates are free. See Credits and billing.