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 fetch handler.

Set up the relay

  1. 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 of relay.js and deploy again.
  2. In the Worker's Settings → Variables and Secrets, add two secrets: EV_API_KEY (your API key) and TYPEFORM_WEBHOOK_SECRET (the webhook secret). Add any of the optional settings.
  3. 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 as TYPEFORM_WEBHOOK_SECRET, save, and turn the webhook on.
  4. 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, or npx wrangler tail) shows invalid with the reason no_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

SettingWhat it does
EV_API_KEYYour API key (required, a secret)
TYPEFORM_WEBHOOK_SECRETThe webhook's secret, at least 32 characters (required, a secret)
EV_REGIONeu to have addresses checked and stored in the EU (default us)
FORM_IDSComma-separated form ids to accept; deliveries of other forms are ignored
EMAIL_FIELDSComma-separated refs or ids of the Email questions to check (default: every Email question)
FORWARD_URLAn https URL of yours that receives each response's results as JSON
FORWARD_SECRETSigns the forward with the header X-EV-Signature (a secret)
SLACK_WEBHOOK_URLA Slack incoming webhook URL, starting https://hooks.slack.com/ (a secret)
NOTIFY_ONall (default), or problems to forward and post only results whose action is not send
INCLUDE_EMAILtrue to put the address in the forward and the Slack message (default: ids and results only)
EVENTSOptional: 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_URL receives a POST with the JSON above. With FORWARD_SECRET set, the header X-EV-Signature holds sha256= 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:

SituationRelay 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 response200, nothing checked
A delivery already answered, or a response submitted more than a day ago200, nothing checked
Rate limit, outage or timeout before any check answered503 with Retry-After: Typeform delivers it again
Key refused, no credits left, or the key's daily cap422 with the error code
Missing or wrong signature401
A setting is missing or wrong500

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
  1. In Typeform, open the menu next to your name → Account → Personal tokens → Generate a new token, with only the scope for reading responses.
  2. Download the clean script: clean.mjs, one file built from the tested source. It needs Node.js 22 or later and nothing else.
  3. 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.csv

Options: --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. 429 and 503 answers, which are never charged, are tried again after their Retry-After time; 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. 0 done; 1 a check failed or the run stopped early (run it again); 2 wrong arguments; 75 the 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.

Updated · Maintained by the email-validation.org team