All posts

How to Verify Email Addresses in Airtable: Formulas to Scripts

··7 min read

Airtable can trim, lower-case, deduplicate and syntax-check email addresses with formulas and extensions, but it cannot tell you whether a mailbox exists. For that, either call a verification API from an automation's "Run a script" action, one record at a time, or export the view as CSV, verify it with a bulk tool, and merge the results back with the CSV import extension.

Everything below about Airtable comes from its own support and developer documentation, read on 6 October 2026. The example verification API is ZeroBounce's, because its v2 validate endpoint is publicly documented; any verifier with an HTTP API slots in the same way.

What Airtable can and cannot do

Airtable handles text and records well and has no network access in formulas. The mailbox check needs a script or a trip outside.

Task Airtable feature Plan note
Normalise case and spaces Formula field: LOWER, TRIM All plans
Syntax check Formula field: REGEX_MATCH All plans
Find duplicates Dedupe extension Paid plans
Mailbox verification, per record Automation, Run a script Check your plan's automation limits
Mailbox verification, in bulk Scripting extension, or CSV round trip Paid plans

The plan notes come from Airtable's pages on the dedupe extension, the scripting extension and the CSV import extension, each of which lists "all paid plans".

Step 1: a normalised email field

Add a formula field that holds a trimmed, lower-cased copy of the address, and run every later check against it. Call it Email (clean):

LOWER(TRIM({Email}))

Most "different" addresses that are really the same differ only by capital letters or a trailing space from a copy-paste. Keep the original Email field untouched so every change is traceable.

Step 2: a syntax check that actually anchors

Use REGEX_MATCH with a pattern anchored at both ends; Airtable's own example matches text that merely contains an address. Airtable's regex guide says REGEX_MATCH() returns 1 or 0 and that the functions run on the RE2 engine, which does not support lookahead, lookbehind or backreferences. Its email example is:

REGEX_MATCH({Email address}, "(\W|^)[\w.\-]{0,25}@[A-Za-z0-9.-]+\.[A-Za-z]{2,}(\W|$)")

We ran that pattern with Python's re, which handles all of its syntax the same way. It matched contact me at a@b.co please, because nothing forces the address to fill the field, and it matched @example.com, because {0,25} allows an empty local part. It also rejected a perfectly legal 27-character local part. We go through why email regexes fail in general in email validation regex.

A tighter field, Syntax OK, run on the clean copy:

AND(
  REGEX_MATCH({Email (clean)}, "^[a-z0-9._%+'-]+@[a-z0-9-]+([.][a-z0-9-]+)*[.][a-z]{2,}$"),
  NOT(FIND("..", {Email (clean)}))
)

Using [.] instead of \. avoids any question of how backslashes are escaped inside a formula string. In our test it accepted jane.doe@example.com, j+tag@sub.example.co.uk and o'brien@example.ie, and rejected john..doe@example.com, jane@example, jane@@example.com, @example.com and the sentence above. It also rejects internationalised addresses such as user@münchen.de; if your audience uses them, review rejected rows by hand rather than deleting them.

A passing row means only that the text is shaped like an address. A made-up mailbox at a real domain passes too.

Step 3: remove duplicates

Run the dedupe extension on Email (clean) with exact matching. Airtable's dedupe extension lets you pick a table, optionally a view, and the fields to compare, with exact, similar or fuzzy matching for text. Exact matching on the clean field is what you want: similar and fuzzy matching can pair addresses like jon@ and jan@, which belong to different people. The extension can merge field values from duplicates into one record, so you do not lose notes attached to the copy you remove.

Step 4a: verify each new record with an automation

Trigger on records that have an email and no status, call the verifier with fetch, and write the status and reason back. Airtable's Run a script action documents the limits that shape this design:

  • each fetch times out after 30 seconds;
  • a script can make 50 fetch requests per run;
  • the script timeout is currently 120 seconds, which Airtable calls a temporary increase from 30.

One call per address and a 50-fetch ceiling make one record per run the natural unit. Use the When record matches conditions trigger with Email not empty and Status empty. Airtable notes that records already matching when you switch it on do not fire it; they must change into the matching state. So this handles new records, and the backlog needs Step 4b or 4c.

Add two input variables to the script, recordId (the triggering record's ID) and email, and create text fields Status and Verification reason:

const API_KEY = "YOUR_ZEROBOUNCE_API_KEY";
const { recordId, email } = input.config();
const table = base.getTable("Contacts");

const url = "https://api.zerobounce.net/v2/validate"
  + "?api_key=" + encodeURIComponent(API_KEY)
  + "&email=" + encodeURIComponent(String(email).trim())
  + "&timeout=20";

let status = "risky";
let reason = "no response";
try {
  const res = await fetch(url);
  const body = await res.json();
  if (body.error) {
    reason = "api error: " + body.error;
  } else {
    reason = body.sub_status ? body.status + " / " + body.sub_status : body.status;
    if (body.status === "valid") status = "valid";
    if (["invalid", "spamtrap", "abuse", "do_not_mail"].includes(body.status)) status = "invalid";
  }
} catch (err) {
  reason = "request failed: " + err.message;
}

await table.updateRecordAsync(recordId, { "Status": status, "Verification reason": reason });
output.set("status", status);

Why these choices:

  • timeout=20 is ZeroBounce's own parameter, which accepts 3 to 60 seconds and returns unknown when reached. Twenty seconds keeps the vendor inside Airtable's 30-second fetch limit, so you get an answer rather than an exception.
  • The error check is there because ZeroBounce documents its bad-key and out-of-credits reply as a JSON body with an error field, not as a status value.
  • Everything not clearly valid or invalid is risky, including catch-all and unknown. On a catch-all domain nobody outside the company can confirm a mailbox. The raw vendor status goes into the reason so you can filter on it later; our verification statuses guide explains each one.

The API key sits in the script, so anyone who can edit the automation can read it. Use a key you can rotate.

Step 4b: verify the backlog with the scripting extension

For existing records, the scripting extension can loop through a view, but it must use remoteFetchAsync and write in batches of 50. Extension scripts run in your browser, so a plain fetch can be blocked by CORS. Airtable's fetch documentation provides remoteFetchAsync, which "makes the request from Airtable's servers instead of your browser". Updates are limited to 50 records per updateRecordsAsync call, per the table API.

const API_KEY = "YOUR_ZEROBOUNCE_API_KEY";
const BATCH = 200;
const table = base.getTable("Contacts");
const query = await table.selectRecordsAsync({ fields: ["Email", "Status"] });

const todo = query.records
  .filter((r) => r.getCellValueAsString("Email").trim() && !r.getCellValueAsString("Status"))
  .slice(0, BATCH);

const updates = [];
for (const record of todo) {
  const email = record.getCellValueAsString("Email").trim();
  const url = "https://api.zerobounce.net/v2/validate?api_key=" + encodeURIComponent(API_KEY)
    + "&email=" + encodeURIComponent(email) + "&timeout=20";
  let status = "risky";
  let reason = "no response";
  try {
    const body = await (await remoteFetchAsync(url)).json();
    reason = body.error ? "api error: " + body.error : body.status;
    if (body.status === "valid") status = "valid";
    if (["invalid", "spamtrap", "abuse", "do_not_mail"].includes(body.status)) status = "invalid";
  } catch (err) {
    reason = "request failed: " + err.message;
  }
  updates.push({ id: record.id, fields: { "Status": status, "Verification reason": reason } });
}

while (updates.length > 0) {
  await table.updateRecordsAsync(updates.splice(0, 50));
}
output.text(`Checked ${todo.length} records`);

It processes 200 records per run so you can watch progress and stop it; run it again for the next batch. Airtable's scripting overview also cites a rate limit of about 15 requests a second, which the sequential loop stays well under.

Step 4c: the CSV round trip

For a large backlog, export the view, verify it in bulk, and merge the results back on the email field. Airtable's base management guide puts Download CSV in the menu next to the view name; filters on the view limit what is exported.

Upload the file to a bulk verifier. SimpleVerifier, for example, takes CSVs of up to 100,000 rows and returns a results file with email, status and reason columns. Then import with the CSV import extension, switch on Merge with existing records and choose Email as the merge field. Three documented details matter:

  • Matching is case-sensitive, though leading and trailing whitespace is ignored. Check that the results file writes addresses exactly as you exported them.
  • Blank merge values create new records, so remove empty rows first.
  • The extension is limited to 25,000 rows and 5 MB per import, so split bigger results files.

The general version of this export-and-rejoin process is in removing invalid emails from a CSV.

The short version

Use formulas for the text: LOWER(TRIM()), an anchored REGEX_MATCH, and dedupe on the clean field. Use a per-record automation for new contacts, keeping each verifier call under the 30-second fetch limit. Clear the backlog with the scripting extension in batches, or with a CSV round trip that merges on an exactly matching email field. To check a single address before building any of it, the free email checker shows the result and reason.

Common questions

Can Airtable check if an email address is real?

Not on its own. Formulas such as REGEX_MATCH only check that the text is shaped like an address. Confirming the mailbox exists needs a DNS lookup and an SMTP check, so you either call a verification API from an Airtable script or export a CSV, verify it elsewhere and import the results.

Is Airtable's example email regex good enough?

It is loose. Airtable's documented pattern is not anchored to the start and end of the field, so text such as 'contact me at a@b.co please' matches, and it allows an empty part before the @. Anchor the pattern with ^ and $ and run it on a trimmed, lower-cased copy of the address.

How many emails can an Airtable automation script verify per run?

Airtable documents a limit of 50 fetch requests per automation script run, and each fetch times out after 30 seconds. One verification API call per address means at most 50 addresses per run, so a per-record automation that verifies one address each time is the simpler design.

Why did my CSV import create duplicates instead of updating records?

Airtable's CSV import extension matches the merge field case-sensitively, so Jane@Example.com and jane@example.com are treated as different values. Leading and trailing whitespace is ignored. Make sure the email values in the results file are written exactly as they are in the table.

Verify unlimited addresses for $29.99/month

Real SMTP mailbox checks. No credits, no per-email fees.

Get Started