# Validate Emails in Klaviyo

URL: https://email-validation.org/docs/guides/klaviyo/
Updated: 2026-11-06

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

Check the email address of each new Klaviyo profile and save the result on the profile, so your segments and flow
splits can use it. Two small pieces do the work, and you can use either or both:

- **A flow-webhook relay** checks a profile within seconds, when a Klaviyo flow sends it there.
- **A sync script** checks every profile created after a time that has no result yet: for a backfill, to catch up,
  or on a schedule instead of the relay.

Both run in your own accounts with your own keys: the relay as a Cloudflare Worker, the script with Node. Nothing
needs to be installed from the Klaviyo App Marketplace.

## 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". A
  [daily credit cap](https://email-validation.org/docs/authentication/#limits) limits what a busy flow can spend.
- **A Klaviyo private API key.** In Klaviyo: **Settings → API keys → Create Private API Key → Custom scopes**, with
  **Profiles: Read** (for the sync script) and **Profiles: Write**. Klaviyo does not let you change a key's scopes
  later.
- **Two-step authentication on your Klaviyo login**, which Klaviyo requires for webhook actions in flows.
- **A Cloudflare account** for the relay, and **Node 22.18 or later** for the sync script.
- **The files**, which are built from our tested code:
  - [relay.js](https://email-validation.org/downloads/klaviyo/relay.js): the flow-webhook relay, one Cloudflare Worker module;
  - [sync.mjs](https://email-validation.org/downloads/klaviyo/sync.mjs): the sync script, one Node module;
  - [flow-webhook.json](https://email-validation.org/downloads/klaviyo/flow-webhook.json): the webhook action's settings, for reference.

## What is saved on the profile

Four custom profile properties, which Klaviyo creates on the first write:

```text
email_validation_status       valid, invalid, catch_all, unknown or disposable
email_validation_reason       why, such as accepted or no_mailbox
email_validation_action       send, send_with_caution, remove or retry_later
email_validation_checked_at   when the check ran (ISO 8601, UTC)
```

The status is one of the five [result statuses](https://email-validation.org/docs/statuses/). To leave out every profile you should not mail,
build a segment on the action property equal to `remove`: spam traps and complainers are `valid` with the action
`remove`.

## Check new profiles with a flow

1. Deploy the relay to your Cloudflare account and give it its secrets:

   ```bash
   npx wrangler deploy relay.js --name ev-klaviyo-relay --compatibility-date 2026-10-01
   npx wrangler secret put EV_API_KEY --name ev-klaviyo-relay        # your API key
   npx wrangler secret put KLAVIYO_API_KEY --name ev-klaviyo-relay   # the Klaviyo private key
   npx wrangler secret put RELAY_SECRET --name ev-klaviyo-relay      # 32 or more random characters
   ```

   For the relay secret, use for example the output of `openssl rand -hex 32`. Your keys stay in the Worker's
   secret store. For [EU data residency](https://email-validation.org/docs/eu-data-residency/), add `--var EV_REGION:eu` to the deploy command.
2. In Klaviyo, create a flow with the trigger **Added to List** for your signup list, or any trigger that starts for
   each new profile. Add the flow filter "email_validation_status is not set", so a profile that enters the flow
   again is not checked, and charged, again.
3. Add a **Webhook** action:
   - **Destination URL**: the relay's address, such as `https://ev-klaviyo-relay.<your-subdomain>.workers.dev/`. It
     must be HTTPS and must not redirect.
   - **Headers**: `X-Relay-Secret` with the same value as the relay secret.
   - **JSON body**:

     ```json
     { "email": "{{ email }}" }
     ```

   The relay finds the profile by its email. If your trigger's preview shows the profile's Klaviyo ID as a
   variable, you can add it to the body as the key profile_id; the relay then updates exactly that profile.
4. Turn the flow on, on a test list first.

## Check it once

Add a test profile to the test list with the address `no_mailbox@sandbox.email-validation.org`, a
[test address](https://email-validation.org/docs/test-addresses/) that costs nothing, and give it one custom property of your own. Keep it off
lists that send mail: the test domain receives none, so a message to it would bounce. When the flow has run, the
profile's status property is `invalid`, its reason `no_mailbox`, and your own property is unchanged. Then delete the
test profile.

## How the relay answers Klaviyo

Klaviyo looks only at a webhook's HTTP status. It delivers again after a `429` or `5xx` for about 24 hours, and puts
any other failure on the flow's **Skipped** list.

| Situation | Relay answers | What to do |
|---|---|---|
| Saved | `200` | Nothing |
| The profile has no email (for example an SMS-only profile) | `200`, skipped | Nothing: nothing was checked or charged |
| A rate limit, outage or timeout at our API | `503` | Nothing: Klaviyo delivers again |
| A relay setting is missing, or the relay secret is under 32 characters | `500` | Fix the secret; Klaviyo keeps trying meanwhile |
| Wrong or missing `X-Relay-Secret` | `401` | Check the webhook's header |
| A body without the email key, or over 16 KB | `400` | Check the webhook's JSON body |
| The key was refused, no credits, or the key's daily cap | `422` with the [error code](https://email-validation.org/docs/errors/) | Fix the cause, then run the sync script for that period |
| The check ran but Klaviyo refused the write | `422` | Run the sync script for that period |

A failed Klaviyo write is never answered with `503`, because a new delivery would pay for the check again. A `503`
delivery that Klaviyo repeats checks the address again, and a repeated check uses a credit like the first one. The
relay's answers never contain the address or a key, and the relay logs nothing.

## Check existing profiles

```bash
export EV_API_KEY=…  KLAVIYO_API_KEY=…
node sync.mjs --since 2026-11-01T00:00:00Z --dry-run   # counts what it would check
node sync.mjs --since 2026-11-01T00:00:00Z             # checks and saves
```

The script checks every profile created after `--since` that has an email and no status yet. Options:
`--region eu` (or the variable `EV_REGION=eu`), `--revalidate` to check profiles that already have a status again,
`--limit N`, and `--concurrency N` (4 by default).

- Rate limits and outages get up to four tries per call, after the wait the service asks for (at most 60 seconds). A
  check that got no answer at all is not sent again, because it may already have used a credit: the profile still
  has no status, so the next run picks it up.
- The run stops at once if our API refuses the key, the credits run out or the key reaches its daily cap, or if
  Klaviyo refuses its key.
- It prints profile IDs and statuses, never addresses. The exit code is `0` when every check succeeded, `1` when a
  check failed or the run stopped early, and `2` when the arguments were wrong.

On a schedule, such as a cron job, set `--since` to a little before the previous run started.

## Costs and limits

- Each checked profile is one check. Results with the status `unknown` are free, and so are malformed addresses and
  the [test addresses](https://email-validation.org/docs/test-addresses/).
- The flow filter on the status property keeps a profile that enters the flow again from being checked twice.
- Each sync run reads profiles through Klaviyo's API, which has its own rate limits; the script waits when Klaviyo
  asks it to.
