# Validate Emails in Typeform

URL: https://email-validation.org/docs/guides/typeform/
Updated: 2026-11-10

> Documentation index: https://email-validation.org/llms.txt

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](#before-submit): Typeform's Email question checks only the format of
  an address.

## Before you start

- **An API key.** Create one on the [API keys page](https://email-validation.org/app/api-keys/) 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](https://email-validation.org/docs/authentication/#limits) 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](https://email-validation.org/downloads/typeform/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](#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](https://email-validation.org/docs/test-addresses/) `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

| 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](https://email-validation.org/docs/eu-data-residency/) (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](https://email-validation.org/docs/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`.

```json
{
  "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:

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

```text
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](https://email-validation.org/downloads/typeform/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.

```bash
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](https://email-validation.org/docs/bulk/): 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](https://email-validation.org/docs/guides/signup-form-validation/).

## Costs

Each completed check costs 1 credit, as with any [validation request](https://email-validation.org/docs/api/validate/); `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](https://email-validation.org/docs/credits-and-billing/).
