# Validate Emails in Segment

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

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

Check the email addresses your Segment sources collect, with a Segment Function you paste into your workspace. It
checks the address of identify calls and of the track events you name, and adds the result to the event, so the
destinations behind it receive the status. Optionally it sends the result back into Segment as traits, for your
profiles and your warehouse.

**Status: a setup guide, not a catalog destination.** email-validation.org is not in Segment's destination catalog
yet. The function on this page works today on any Segment workspace that has Functions. A catalog destination is
built and waiting to be submitted to Segment ([below](#catalog)).

## Before you start: what it costs

Every event the function checks is one validation, charged like any other check. These are free:

- Every result with status unknown
- Checks that exceed their time limit (10 seconds by default; unknown, reason timeout)
- API errors
- Malformed addresses rejected before any check
- Test addresses on the sandbox domain

**The function cannot tell whether an address changed.** Segment Functions keep no lasting state, so an identify call
that repeats an address already checked is checked, and charged, again. The function keeps results in Segment's
cache while its process runs, which saves some repeat checks, but Segment can empty that cache at any time, so do not
count on it. To keep the cost close to one check per new address:

- **Check the events that carry a new or changed address**, such as `Signed Up` or `Email Changed`: name them in the
  `trackEvents` setting, and set `checkIdentify` to false if your app sends identify calls on every visit.
- **Give the function its own API key with a [daily credit cap](https://email-validation.org/docs/authentication/#limits)**, so a busy source can
  never spend more than you expect.

## Where the result goes

The result is four fields, added to the identify call's traits or the track event's properties:

```json
{
  "ev_status": "valid",
  "ev_reason": "accepted",
  "ev_action": "send",
  "ev_processed_at": "2026-11-06T12:00:00.000Z"
}
```

The [statuses](https://email-validation.org/docs/statuses/) are `valid`, `invalid`, `catch_all`, `unknown` and `disposable`; the action says what
to do with the address (`send`, `send_with_caution`, `remove`, `retry_later`). What reaches your tools depends on how
you add the function:

- **As an insert function** (recommended): you attach it to a destination, and the destination receives each event
  with the fields above, for example to suppress invalid addresses in your email tool or CRM. Segment runs insert
  functions only in front of cloud-mode destinations, not storage destinations such as warehouses.
- **As a destination function**: it sees the events but cannot change them or the profile they came from. Its result
  reaches Segment only through the [write-back](#write-back) below. Without a write key, a destination function's
  checks are charged and the results are seen only in the function's logs, so always set one in this mode.
- **Write-back** (either mode): with the `writeKey` setting, each result is also sent to a Segment HTTP API source of
  yours as an identify call with the four fields as traits. From there it reaches your profiles, your warehouse and
  every destination connected to that source.

## Set it up

1. Create an API key for Segment on the [API keys page](https://email-validation.org/app/api-keys/), with a daily credit cap.
2. In Segment, go to **Connections → Catalog**, open the **Functions** tab and click **New Function**. Choose
   **Insert** (or **Destination**, see [above](#results)).
3. Replace the editor's code with the [function below](#code).
4. Add the settings in the function's **Settings** tab ([the list](#settings)): at least `apiKey`, marked as secret.
5. In the **Test** tab, send an identify call with a free [test address](https://email-validation.org/docs/test-addresses/), for example
   `accepted@sandbox.email-validation.org` or `no_mailbox@sandbox.email-validation.org` as `traits.email`. The output shows the event with
   the four fields added.
6. Save and deploy the function, then connect it: an insert function to the destination it should enrich, a
   destination function to the source whose events it should check.

### Settings

| Name | Type | Required | What it does |
| --- | --- | --- | --- |
| `apiKey` | String, secret | Yes | Your API key. |
| `region` | String | No | `eu` for [EU data residency](https://email-validation.org/docs/eu-data-residency/); anything else is the US endpoint. |
| `checkIdentify` | Boolean | No | False: identify calls are not checked. Default: checked. |
| `trackEvents` | String | No | Track event names to check, separated by commas, e.g. `Signed Up, Email Changed`. The address is the event's `properties.email`, else `context.traits.email`. |
| `writeKey` | String, secret | No | The write key of a Segment HTTP API source: each result is also sent there ([write-back](#write-back)). |
| `writeKeyRegion` | String | No | `eu` when that source's workspace is in Segment's EU region. |
| `retryLater` | Boolean | No | True: on a rate limit or an outage, Segment tries the event again later instead of passing it on unchecked. Only for a destination function ([details](#errors)). |

## The write-back source

1. In Segment, add a source of type **HTTP API** (for example named "Email validation results") and copy its write
   key into the function's `writeKey` setting.
2. Connect that source to the destinations that should receive the results: your warehouse, a CRM, an email tool.

Each result arrives there as an identify call with the user's `userId` (or `anonymousId`) and the four fields as
traits. The address is never sent back, so if the function also checks that source's events, it finds nothing to
check: there is no loop and no charge. Events with neither id get no write-back.

## Rate limits and errors

The function never stops or drops your events because of us. When a check cannot be done, the event goes on without
the four fields and the function's log says why, with the HTTP status, our error code and the request id, never the
address.

- **Rate limits and outages** (HTTP 429 or 503): the function sends no check until the `Retry-After` time is over;
  events in the meantime go on unchecked. With `retryLater` set to true it throws a retry error instead, and Segment
  tries the event again later; nothing was charged for it. Use that only in a destination function: in an insert
  function a retried event reaches its destination late.
- **No answer in time**: Segment stops a function after 5 seconds, so the function waits up to about 4 seconds for a
  check (it asks our API to answer within 5 seconds; a slower check comes back `unknown` with reason
  `timeout`, which is free). An event whose check got no answer is not checked again, because that check may have
  finished and been charged on our side.
- **Account problems** (an invalid key, no credits left, the key's daily cap): the events go on unchecked until you
  fix the cause. See [errors](https://email-validation.org/docs/errors/) for every code.

## The function

The whole function. Every name it adds starts with `ev`, so it does not clash with code of yours.

```js
// email-validation.org for Segment 1.0.0: a Segment Function (paste it as a destination function or an insert function).
// Docs: https://email-validation.org/docs/guides/segment/
//
// It checks the address of identify events (traits.email) and of the track events you name, and adds the result to
// the event as ev_status, ev_reason, ev_action and ev_processed_at. Each checked event costs one credit (results
// with status unknown and test addresses are free): Segment Functions keep no lasting state, so the function cannot
// tell whether an address changed. A cache inside the function saves repeat checks of the same address only while
// Segment keeps the function's process running.
//
// Settings (Settings tab of the function):
//   apiKey          Secret, required. Your email-validation.org API key (give it a daily credit cap).
//   region          Optional. "eu" for EU data residency; anything else is US.
//   checkIdentify   Optional boolean. false: identify events are not checked (default: checked).
//   trackEvents     Optional. Comma-separated track event names to check, e.g. "Signed Up, Email Changed".
//   writeKey        Secret, optional. A Segment HTTP API source's write key: the result is also sent there as an
//                   identify call with the ev_ traits (never the address), for your profiles and warehouse.
//   writeKeyRegion  Optional. "eu" when that source's workspace is in the EU.
//   retryLater      Optional boolean. true: a rate limit (429) or outage (503) throws RetryError, so Segment tries the
//                   event again later. Use it only in a destination function: in an insert function a retried
//                   event reaches its destination late. Default: the event goes on unchecked.
//
// A problem on our side never stops your events: the event goes on without the ev_ traits, and one log line says
// why (never the address).

const EV_VERSION = '1.0.0';
const EV_USER_AGENT = `email-validation-segment-function/${EV_VERSION}`;
const EV_ORIGINS = { us: 'https://api.email-validation.org', eu: 'https://api-eu.email-validation.org' };
const EV_SEGMENT_IDENTIFY = {
  us: 'https://api.segment.io/v1/identify',
  eu: 'https://events.eu1.segmentapis.com/v1/identify',
};
const EV_STATUSES = ['valid', 'invalid', 'catch_all', 'unknown', 'disposable'];
// Our API answers within this many seconds (its shortest wait; a slower check is unknown, reason timeout, free).
const EV_CHECK_TIMEOUT_S = 5;
// Segment stops a function after 5 seconds: the check and the write-back each get a share of that.
const EV_CHECK_LIMIT_MS = 3800;
const EV_WRITE_BACK_LIMIT_MS = 900;
// How long a result is kept in the function's cache (process-local; Segment may empty it at any time).
const EV_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
// After a 429 or 503 without Retry-After, no check is sent for this long.
const EV_DEFAULT_PAUSE_S = 30;

// Until when no check is sent, after our API asked to wait (Retry-After). Process-local.
let evPausedUntil = 0;

// biome-ignore lint/correctness/noUnusedVariables: Segment calls each handler by its name
async function onIdentify(event, settings) {
  if (settings.checkIdentify === false || settings.checkIdentify === 'false') return event;
  const email = evAddress(event.traits);
  if (!email) return event;
  const verdict = await evCheck(email, settings);
  if (!verdict) return event;
  event.traits = Object.assign({}, event.traits, verdict);
  await evWriteBack(event, verdict, settings);
  return event;
}

// biome-ignore lint/correctness/noUnusedVariables: Segment calls each handler by its name
async function onTrack(event, settings) {
  if (!evNamed(settings.trackEvents, event.event)) return event;
  const email = evAddress(event.properties) || evAddress(event.context?.traits);
  if (!email) return event;
  const verdict = await evCheck(email, settings);
  if (!verdict) return event;
  event.properties = Object.assign({}, event.properties, verdict);
  await evWriteBack(event, verdict, settings);
  return event;
}

// An insert function blocks every event type it has no handler for: these pass the others on unchanged.
// biome-ignore lint/correctness/noUnusedVariables: Segment calls each handler by its name
async function onPage(event) {
  return event;
}
// biome-ignore lint/correctness/noUnusedVariables: Segment calls each handler by its name
async function onScreen(event) {
  return event;
}
// biome-ignore lint/correctness/noUnusedVariables: Segment calls each handler by its name
async function onGroup(event) {
  return event;
}
// biome-ignore lint/correctness/noUnusedVariables: Segment calls each handler by its name
async function onAlias(event) {
  return event;
}
// biome-ignore lint/correctness/noUnusedVariables: Segment calls each handler by its name
async function onDelete(event) {
  return event;
}

function evAddress(traits) {
  const value = traits && typeof traits.email === 'string' ? traits.email.trim() : '';
  return value.includes('@') ? value : '';
}

function evNamed(list, name) {
  if (typeof list !== 'string' || typeof name !== 'string') return false;
  return list
    .split(',')
    .map((s) => s.trim())
    .filter(Boolean)
    .includes(name);
}

function evLog(message) {
  console.log(`email-validation.org: ${message}`);
}

function evIsTrue(value) {
  return value === true || value === 'true';
}

// The result as traits, or null when the address was not checked (the reason is logged).
async function evCheck(email, settings) {
  const apiKey = typeof settings.apiKey === 'string' ? settings.apiKey.trim() : '';
  if (!apiKey || /\s/.test(apiKey)) {
    evLog('not checked: the apiKey setting is missing.');
    return null;
  }
  if (Date.now() < evPausedUntil) return evLimited(settings, 'paused after a rate limit');
  const region = settings.region === 'eu' ? 'eu' : 'us';
  const load = () => evValidate(EV_ORIGINS[region], apiKey, email, settings);
  if (typeof cache === 'undefined' || !cache || typeof cache.load !== 'function') return load();
  const key = `ev:${region}:${email.toLowerCase()}`;
  const verdict = await cache.load(key, EV_CACHE_TTL_MS, load);
  // Only charged results are kept: unknown is free and always checked again; null means not checked.
  if (!verdict || verdict.ev_status === 'unknown') cache.delete(key);
  return verdict;
}

async function evValidate(origin, apiKey, email, settings) {
  let response;
  try {
    response = await fetch(`${origin}/v1/validate`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
        Accept: 'application/json',
        'User-Agent': EV_USER_AGENT,
      },
      body: JSON.stringify({ email: email, timeout: EV_CHECK_TIMEOUT_S }),
      redirect: 'manual',
      timeout: EV_CHECK_LIMIT_MS,
      signal: evSignal(EV_CHECK_LIMIT_MS),
    });
  } catch {
    // Never retried: the check may have run and been charged, and a retry would charge it again.
    evLog('not checked: no answer from the API in time (not retried).');
    return null;
  }
  let body = null;
  try {
    body = await response.json();
  } catch {
    body = null;
  }
  if (response.status === 200 && body && EV_STATUSES.includes(body.status)) {
    return {
      ev_status: body.status,
      ev_reason: typeof body.reason === 'string' ? body.reason : '',
      ev_action: typeof body.action === 'string' ? body.action : '',
      ev_processed_at: typeof body.processed_at === 'string' ? body.processed_at : '',
    };
  }
  const error = body?.error ? body.error : {};
  const code = typeof error.code === 'string' ? error.code : 'unexpected_response';
  const requestId = typeof error.request_id === 'string' ? error.request_id : response.headers.get('x-request-id');
  const where = ` (HTTP ${response.status}, ${code}${requestId ? `, request ${requestId}` : ''})`;
  if ((response.status === 429 || response.status === 503) && code !== 'key_daily_cap') {
    const after = evRetryAfter(response.headers.get('retry-after'));
    evPausedUntil = Date.now() + (after === null ? EV_DEFAULT_PAUSE_S : after) * 1000;
    return evLimited(settings, `rate limited${where}`);
  }
  evLog(`not checked${where}.`);
  return null;
}

// A rate limit: with retryLater, Segment tries the event again later (nothing was charged); else it goes on unchecked.
function evLimited(settings, why) {
  if (evIsTrue(settings.retryLater)) throw new RetryError(`email-validation.org: ${why}; Segment retries later.`);
  evLog(`not checked: ${why}.`);
  return null;
}

function evRetryAfter(value) {
  if (typeof value !== 'string' || value.trim() === '') return null;
  if (/^\d+$/.test(value.trim())) return Number(value.trim());
  const at = Date.parse(value);
  return Number.isNaN(at) ? null : Math.max(0, Math.ceil((at - Date.now()) / 1000));
}

function evSignal(ms) {
  return typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
    ? AbortSignal.timeout(ms)
    : undefined;
}

// The result to the customer's own Segment source: ids and ev_ traits only, never the address, so the call has
// nothing for this function to check if that source feeds it too (no loop, no charge). Never retried: the check was
// already charged.
async function evWriteBack(event, verdict, settings) {
  const writeKey = typeof settings.writeKey === 'string' ? settings.writeKey.trim() : '';
  if (!writeKey) return;
  const userId = typeof event.userId === 'string' && event.userId ? event.userId : undefined;
  const anonymousId = typeof event.anonymousId === 'string' && event.anonymousId ? event.anonymousId : undefined;
  if (!userId && !anonymousId) return;
  const url = settings.writeKeyRegion === 'eu' ? EV_SEGMENT_IDENTIFY.eu : EV_SEGMENT_IDENTIFY.us;
  try {
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        Authorization: `Basic ${btoa(`${writeKey}:`)}`,
        'Content-Type': 'application/json',
        'User-Agent': EV_USER_AGENT,
      },
      body: JSON.stringify({ userId: userId, anonymousId: anonymousId, traits: verdict }),
      redirect: 'manual',
      timeout: EV_WRITE_BACK_LIMIT_MS,
      signal: evSignal(EV_WRITE_BACK_LIMIT_MS),
    });
    if (response.status < 200 || response.status >= 300) evLog(`write-back failed (HTTP ${response.status}).`);
  } catch {
    evLog('write-back failed (no answer).');
  }
}
```

## The catalog destination (not available yet)

A destination for Segment's catalog is built and tested, in the shape Segment uses for its catalog destinations
(Destination Actions). It is not submitted to Segment yet, so it cannot be added from the catalog today. Once Segment
lists it, it will check the address of the events you subscribe it to (a preset subscribes it to `Signed Up` track
events) and, with a write key, send the result back the same way as the function. It will cost the same: one check
per event it receives. Unlike an insert function, it cannot add the result to the events your other destinations
receive, so the write-back is the only way its results reach Segment. This page will say when it is listed.

## Related

- [RudderStack](https://email-validation.org/docs/guides/rudderstack/): the same check as a RudderStack transformation
- [Sign-up form validation](https://email-validation.org/docs/guides/signup-form-validation/): check the address before it reaches Segment
- [Statuses and reasons](https://email-validation.org/docs/statuses/), [errors](https://email-validation.org/docs/errors/) and [rate limits](https://email-validation.org/docs/rate-limits/)
