1. Home
  2. API docs
  3. Google Sheets

Verify emails and validate phone numbers in Google Sheets

Add the SimpleVerifier script to your sheet, save your API key, pick a column and click Verify. Each email gets a live mailbox check and comes back valid, risky or invalid with a reason. Each phone number comes back with its status, E.164 form, line type and, on the Email + Phone plan, carrier.

What it does

  • Emails: adds Status and Reason columns. Status is valid (the mail server confirmed the mailbox), risky (it may work, but delivery isn't confirmed, for example a catch-all domain) or invalid (it will bounce, or it's a disposable address).
  • Phone numbers: adds Status, E.164, Type and Carrier columns. Status says whether the number fits its country's numbering plan. Carrier is filled in only when a live lookup ran on the Email + Phone plan. It does not tell you whether a number is in service.
  • Long lists: rows are checked in batches that stay under the API rate limit and Google's script time limit, and progress is saved so you can resume.
  • Your key, your plan: the add-on calls the SimpleVerifier API with your own key, stored in your Google account and never written into the sheet.

1. Add the script to your sheet

Until the add-on is listed on the Google Workspace Marketplace, install it by copying three files into your sheet's Apps Script project.

  1. Open the Google Sheet that holds your list.
  2. Click Extensions > Apps Script. A new script project opens in another tab.
  3. Click the gear icon (Project Settings) and tick Show "appsscript.json" manifest file in editor.
  4. Back in the Editor, open Code.gs, delete what is there and paste the Code.gs file below.
  5. Click + next to Files, choose HTML, name it Sidebar and paste the Sidebar.html file below.
  6. Open appsscript.json and replace its contents with the appsscript.json file below.
  7. Click Save, then go back to the sheet and reload the page.
  8. Click Extensions > SimpleVerifier > Open. Google asks you to authorize the script the first time.

The script asks for three permissions: see and edit only the spreadsheet it is installed in, connect to simpleverifier.com, and show a sidebar. It cannot open your other files.

Code.gs — paste into Code.gs
/**
 * SimpleVerifier for Google Sheets.
 *
 * Verifies a column of email addresses or phone numbers with the SimpleVerifier
 * API, using the customer's own API key, and writes the results into new
 * columns next to it. Also adds SV_VERIFY and SV_PHONE for single cells.
 *
 * @OnlyCurrentDoc
 */

var API_BASE = 'https://simpleverifier.com/api/v1';
var SITE = 'https://simpleverifier.com';
var KEY_PROPERTY = 'SV_API_KEY';
var JOB_PREFIX = 'SV_JOB_';

/** 100 calls a minute, under the API's 120 so formulas running at the same time still fit. */
var MIN_MS_PER_CALL = 600;
var BATCH_SIZE = 10;
/** Each sidebar call stops starting new batches after this, far inside the 6-minute limit. */
var RUN_BUDGET_MS = 50 * 1000;
var RATE_LIMIT_PAUSE_MS = 15 * 1000;
var MAX_RETRIES = 4;
var CACHE_SECONDS = 6 * 60 * 60;

var HEADERS = {
  email: ['Status', 'Reason'],
  phone: ['Status', 'E.164', 'Type', 'Carrier'],
};

var MESSAGES = {
  noKey: 'Add your SimpleVerifier API key in Extensions > SimpleVerifier > Open first. Create one at ' + SITE + '/api-keys',
  badKey: 'Your API key was not accepted. Check it at ' + SITE + '/api-keys and save it again.',
  inactive: 'Your SimpleVerifier subscription is not active. Renew it at ' + SITE + '/pricing, then press Resume.',
  rateLimited: 'Too many checks at once: the limit is 120 a minute per key. Wait a minute and try again.',
  unavailable: 'SimpleVerifier is briefly unavailable. Wait a minute, then press Resume.',
};

function onInstall(e) {
  onOpen(e);
}

function onOpen() {
  SpreadsheetApp.getUi()
    .createAddonMenu()
    .addItem('Open', 'showSidebar')
    .addToUi();
}

function showSidebar() {
  var html = HtmlService.createHtmlOutputFromFile('Sidebar').setTitle('SimpleVerifier');
  SpreadsheetApp.getUi().showSidebar(html);
}

// ---------------------------------------------------------------------------
// API key
// ---------------------------------------------------------------------------

function getApiKey_() {
  return PropertiesService.getUserProperties().getProperty(KEY_PROPERTY) || '';
}

/** Checks a key without spending a verification: auth runs before the missing-email check. */
function checkKey_(key) {
  var response = UrlFetchApp.fetch(API_BASE + '/verify', {
    method: 'get',
    headers: { Authorization: 'Bearer ' + key },
    muteHttpExceptions: true,
  });
  var code = response.getResponseCode();
  if (code === 400 || code === 200) return;
  if (code === 401) throw new Error(MESSAGES.badKey);
  if (code === 402) throw new Error(MESSAGES.inactive);
  if (code === 429) throw new Error(MESSAGES.rateLimited);
  throw new Error(MESSAGES.unavailable);
}

function saveApiKey(rawKey) {
  var key = String(rawKey || '').trim();
  if (!/^sv_live_[A-Za-z0-9_-]+$/.test(key)) {
    throw new Error('That does not look like a SimpleVerifier key. Keys start with sv_live_.');
  }
  checkKey_(key);
  PropertiesService.getUserProperties().setProperty(KEY_PROPERTY, key);
  return getState();
}

function removeApiKey() {
  PropertiesService.getUserProperties().deleteProperty(KEY_PROPERTY);
  return getState();
}

// ---------------------------------------------------------------------------
// Sidebar state
// ---------------------------------------------------------------------------

function columnLetter_(index) {
  var letters = '';
  var n = index;
  while (n > 0) {
    var rem = (n - 1) % 26;
    letters = String.fromCharCode(65 + rem) + letters;
    n = Math.floor((n - 1) / 26);
  }
  return letters;
}

function describeSelection_() {
  var range = SpreadsheetApp.getActiveRange();
  if (!range) return null;
  var firstRow = Math.max(range.getRow(), 2);
  var lastRow = range.getLastRow();
  return {
    sheet: range.getSheet().getName(),
    column: range.getColumn(),
    letter: columnLetter_(range.getColumn()),
    firstRow: firstRow,
    lastRow: lastRow,
    rows: Math.max(lastRow - firstRow + 1, 0),
    multipleColumns: range.getNumColumns() > 1,
  };
}

function listColumns_(sheet) {
  var lastColumn = Math.min(sheet.getLastColumn(), 200);
  if (lastColumn < 1) return [];
  var headers = sheet.getRange(1, 1, 1, lastColumn).getDisplayValues()[0];
  return headers.map(function (header, i) {
    var letter = columnLetter_(i + 1);
    return { index: i + 1, label: header ? letter + ': ' + header : letter };
  });
}

function getState() {
  var key = getApiKey_();
  var sheet = SpreadsheetApp.getActiveSheet();
  return {
    hasKey: Boolean(key),
    keyEnding: key ? key.slice(-4) : '',
    selection: describeSelection_(),
    columns: listColumns_(sheet),
    sheet: sheet.getName(),
    job: jobSummary_(loadJob_()),
  };
}

// ---------------------------------------------------------------------------
// Jobs: a saved position in a column, so a long list survives the time limit
// ---------------------------------------------------------------------------

function jobKey_() {
  return JOB_PREFIX + SpreadsheetApp.getActiveSpreadsheet().getId();
}

function loadJob_() {
  var raw = PropertiesService.getUserProperties().getProperty(jobKey_());
  if (!raw) return null;
  try {
    return JSON.parse(raw);
  } catch (err) {
    return null;
  }
}

function saveJob_(job) {
  PropertiesService.getUserProperties().setProperty(jobKey_(), JSON.stringify(job));
}

function discardJob() {
  PropertiesService.getUserProperties().deleteProperty(jobKey_());
  return getState();
}

function jobSummary_(job) {
  if (!job) return null;
  var total = job.endRow - job.startRow + 1;
  var done = Math.min(job.nextRow - job.startRow, total);
  return {
    mode: job.mode,
    sheet: job.sheetName,
    column: columnLetter_(job.inputCol),
    total: total,
    done: done,
    finished: job.nextRow > job.endRow,
    counts: job.counts,
  };
}

function sheetById_(sheetId) {
  var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
  for (var i = 0; i < sheets.length; i++) {
    if (sheets[i].getSheetId() === sheetId) return sheets[i];
  }
  return null;
}

function normaliseCountry_(country) {
  var code = String(country || 'US').trim().toUpperCase();
  if (!/^[A-Z]{2}$/.test(code)) throw new Error('Country must be a two-letter code, such as US or GB.');
  return code;
}

/**
 * options: { mode: 'email' | 'phone', source: 'selection' | 'column', column?: number,
 *            country?: string, live?: boolean }
 */
function startJob(options) {
  if (!getApiKey_()) throw new Error(MESSAGES.noKey);
  var mode = options.mode === 'phone' ? 'phone' : 'email';
  var sheet = SpreadsheetApp.getActiveSheet();
  var inputCol;
  var startRow;
  var endRow;

  if (options.source === 'selection') {
    var selection = describeSelection_();
    if (!selection || selection.rows < 1) {
      throw new Error('Select the cells to verify first. Row 1 is treated as the header row.');
    }
    inputCol = selection.column;
    startRow = selection.firstRow;
    endRow = selection.lastRow;
  } else {
    inputCol = Number(options.column);
    if (!inputCol || inputCol < 1) throw new Error('Choose a column to verify.');
    startRow = 2;
    endRow = sheet.getLastRow();
  }
  endRow = Math.min(endRow, sheet.getLastRow());
  if (endRow < startRow) throw new Error('There is nothing to verify below the header row.');

  var headers = HEADERS[mode];
  sheet.insertColumnsAfter(inputCol, headers.length);
  sheet.getRange(1, inputCol + 1, 1, headers.length).setValues([headers]).setFontWeight('bold');

  var job = {
    mode: mode,
    sheetId: sheet.getSheetId(),
    sheetName: sheet.getName(),
    inputCol: inputCol,
    outCol: inputCol + 1,
    startRow: startRow,
    endRow: endRow,
    nextRow: startRow,
    country: mode === 'phone' ? normaliseCountry_(options.country) : '',
    live: options.live !== false,
    retries: 0,
    counts: {},
  };
  saveJob_(job);
  return jobSummary_(job);
}

function requestFor_(job, value, key) {
  var url = job.mode === 'email'
    ? API_BASE + '/verify?email=' + encodeURIComponent(value)
    : API_BASE + '/phone?phone=' + encodeURIComponent(value) +
      '&country=' + encodeURIComponent(job.country) + '&live=' + (job.live ? 'true' : 'false');
  return {
    url: url,
    method: 'get',
    headers: { Authorization: 'Bearer ' + key },
    muteHttpExceptions: true,
  };
}

function parseBody_(response) {
  try {
    return JSON.parse(response.getContentText());
  } catch (err) {
    return {};
  }
}

function emailRow_(body) {
  return [body.status || '', body.reason || ''];
}

function phoneRow_(body) {
  var type = body.live_lookup && body.phone_line_type ? body.phone_line_type : body.phone_type;
  return [body.phone_status || '', body.phone_e164 || '', type || '', body.phone_carrier || ''];
}

function blankRow_(mode) {
  return HEADERS[mode].map(function () {
    return '';
  });
}

function errorRow_(mode, message) {
  var row = blankRow_(mode);
  row[0] = 'error';
  row[1] = message;
  return row;
}

/**
 * Runs one batch of up to BATCH_SIZE rows. Returns the rows to write and, when a
 * call failed in a way that stops the batch, the index of the first row that
 * must be retried and why.
 */
function runBatch_(job, inputs, key) {
  var rows = [];
  var indexes = [];
  var requests = [];
  inputs.forEach(function (value, i) {
    var trimmed = String(value).trim();
    rows.push(blankRow_(job.mode));
    if (!trimmed) return;
    indexes.push(i);
    requests.push(requestFor_(job, trimmed, key));
  });
  if (!requests.length) return { rows: rows, stopAt: -1, stopReason: '' };

  var responses = UrlFetchApp.fetchAll(requests);
  for (var r = 0; r < responses.length; r++) {
    var i = indexes[r];
    var code = responses[r].getResponseCode();
    var body = parseBody_(responses[r]);
    if (code === 200) {
      rows[i] = job.mode === 'email' ? emailRow_(body) : phoneRow_(body);
      var status = rows[i][0] || 'unknown';
      job.counts[status] = (job.counts[status] || 0) + 1;
      continue;
    }
    if (code === 400) {
      rows[i] = errorRow_(job.mode, body.error || 'The API could not read this value.');
      job.counts.error = (job.counts.error || 0) + 1;
      continue;
    }
    var reason = code === 401 ? 'badKey' : code === 402 ? 'inactive' : code === 429 ? 'rateLimited' : 'unavailable';
    return { rows: rows, stopAt: i, stopReason: reason };
  }
  return { rows: rows, stopAt: -1, stopReason: '' };
}

function checkOutputColumns_(sheet, job) {
  var headers = HEADERS[job.mode];
  var current = sheet.getRange(1, job.outCol, 1, headers.length).getDisplayValues()[0];
  if (current.join('|') !== headers.join('|')) {
    throw new Error('The result columns have moved since this run started. Press Discard and start again.');
  }
}

/** Called repeatedly by the sidebar until the job reports finished. */
function processNext() {
  var job = loadJob_();
  if (!job) return null;
  var key = getApiKey_();
  if (!key) throw new Error(MESSAGES.noKey);
  var sheet = sheetById_(job.sheetId);
  if (!sheet) throw new Error('The sheet "' + job.sheetName + '" was deleted. Press Discard and start again.');
  checkOutputColumns_(sheet, job);

  var deadline = Date.now() + RUN_BUDGET_MS;
  while (job.nextRow <= job.endRow && Date.now() < deadline) {
    var count = Math.min(BATCH_SIZE, job.endRow - job.nextRow + 1);
    var started = Date.now();
    var inputs = sheet.getRange(job.nextRow, job.inputCol, count, 1).getDisplayValues().map(function (row) {
      return row[0];
    });
    var result = runBatch_(job, inputs, key);
    var written = result.stopAt === -1 ? count : result.stopAt;
    if (written > 0) {
      sheet.getRange(job.nextRow, job.outCol, written, HEADERS[job.mode].length)
        .setValues(result.rows.slice(0, written));
      job.nextRow += written;
    }

    if (result.stopAt !== -1) {
      if (result.stopReason === 'badKey' || result.stopReason === 'inactive') {
        saveJob_(job);
        throw new Error(MESSAGES[result.stopReason]);
      }
      job.retries += 1;
      saveJob_(job);
      if (job.retries > MAX_RETRIES) {
        job.retries = 0;
        saveJob_(job);
        throw new Error(MESSAGES[result.stopReason]);
      }
      Utilities.sleep(RATE_LIMIT_PAUSE_MS);
      continue;
    }

    job.retries = 0;
    saveJob_(job);
    var wait = started + count * MIN_MS_PER_CALL - Date.now();
    if (wait > 0) Utilities.sleep(wait);
  }

  return jobSummary_(job);
}

// ---------------------------------------------------------------------------
// Custom functions
// ---------------------------------------------------------------------------

function singleValue_(input, name) {
  if (Array.isArray(input)) throw new Error(name + ' takes one cell. For a column, use Extensions > SimpleVerifier > Open.');
  return String(input === null || input === undefined ? '' : input).trim();
}

function documentCache_() {
  try {
    return CacheService.getDocumentCache();
  } catch (err) {
    return null;
  }
}

/**
 * Custom functions can read user properties but cannot ask for authorization or
 * show UI, so every failure is thrown as a message the cell shows on hover.
 */
function callForCell_(url, cacheKey) {
  var cache = documentCache_();
  var cached = cache ? cache.get(cacheKey) : null;
  if (cached) return JSON.parse(cached);

  var key = getApiKey_();
  if (!key) throw new Error(MESSAGES.noKey);

  var options = { method: 'get', headers: { Authorization: 'Bearer ' + key }, muteHttpExceptions: true };
  var response = UrlFetchApp.fetch(url, options);
  if (response.getResponseCode() === 429) {
    Utilities.sleep(2000 + Math.floor(Math.random() * 3000));
    response = UrlFetchApp.fetch(url, options);
  }

  var code = response.getResponseCode();
  var body = parseBody_(response);
  if (code === 401) throw new Error(MESSAGES.badKey);
  if (code === 402) throw new Error('Your SimpleVerifier subscription is not active. Renew it at ' + SITE + '/pricing');
  if (code === 429) throw new Error(MESSAGES.rateLimited + ' For long lists, use the sidebar.');
  if (code === 400) throw new Error(body.error || 'The API could not read this value.');
  if (code !== 200) throw new Error('SimpleVerifier is briefly unavailable. Try again in a minute.');

  if (cache) cache.put(cacheKey, JSON.stringify(body), CACHE_SECONDS);
  return body;
}

/**
 * Verifies one email address with SimpleVerifier. Returns the status and the
 * reason in two cells: valid, risky or invalid.
 *
 * @param {string} email The email address, or a cell that holds one.
 * @return The status and reason.
 * @customfunction
 */
function SV_VERIFY(email) {
  var value = singleValue_(email, 'SV_VERIFY');
  if (!value) return '';
  var body = callForCell_(API_BASE + '/verify?email=' + encodeURIComponent(value), 'e:' + value.toLowerCase());
  return [emailRow_(body)];
}

/**
 * Validates one phone number with SimpleVerifier. Returns the status, E.164
 * form, line type and carrier (carrier only on the Email + Phone plan) in four cells.
 *
 * @param {string} phone The phone number, or a cell that holds one.
 * @param {string} country Two-letter country for numbers written without +, such as "GB". Defaults to "US".
 * @return The status, E.164 number, type and carrier.
 * @customfunction
 */
function SV_PHONE(phone, country) {
  var value = singleValue_(phone, 'SV_PHONE');
  if (!value) return '';
  var region = normaliseCountry_(country);
  var url = API_BASE + '/phone?phone=' + encodeURIComponent(value) + '&country=' + region;
  var body = callForCell_(url, 'p:' + region + ':' + value);
  return [phoneRow_(body)];
}
Sidebar.html — paste into Sidebar (an HTML file)
<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
    <style>
      body { padding: 12px; }
      section { margin-bottom: 18px; }
      h2 { font-size: 13px; font-weight: bold; margin: 0 0 8px; }
      label { display: block; margin: 6px 0; }
      .hint { color: #5f6368; font-size: 11px; line-height: 1.4; }
      .row { display: flex; gap: 6px; align-items: center; margin-top: 6px; }
      .row input[type=text], .row input[type=password], select { flex: 1; width: 100%; box-sizing: border-box; }
      .bar { background: #e8eaed; border-radius: 3px; height: 8px; margin: 8px 0; overflow: hidden; }
      .bar > div { background: #1a73e8; height: 100%; width: 0; transition: width .3s; }
      .message { border-radius: 4px; font-size: 12px; line-height: 1.4; margin-top: 10px; padding: 8px; }
      .message.error { background: #fce8e6; color: #a50e0e; }
      .message.ok { background: #e6f4ea; color: #137333; }
      .hidden { display: none; }
      a { color: #1a73e8; }
    </style>
  </head>
  <body>
    <section id="key-section">
      <h2>API key</h2>
      <div id="key-saved" class="hidden">
        <div>Key saved, ending in <b id="key-ending"></b>.</div>
        <div class="row">
          <button id="change-key">Change</button>
          <button id="remove-key">Remove</button>
        </div>
      </div>
      <div id="key-form" class="hidden">
        <div class="row">
          <input type="password" id="key-input" placeholder="sv_live_..." autocomplete="off">
          <button class="action" id="save-key">Save</button>
        </div>
        <p class="hint">
          Create a key on your <a href="https://simpleverifier.com/api-keys" target="_blank">API keys page</a>.
          It is stored in your Google account for this add-on only and is never shown again.
        </p>
      </div>
    </section>

    <section id="verify-section" class="hidden">
      <h2>What is in the column?</h2>
      <label><input type="radio" name="mode" value="email" checked> Email addresses</label>
      <label><input type="radio" name="mode" value="phone"> Phone numbers</label>
      <div id="phone-options" class="hidden">
        <label for="country">Country for numbers written without +</label>
        <div class="row"><input type="text" id="country" value="US" maxlength="2" style="text-transform:uppercase"></div>
        <label><input type="checkbox" id="live" checked> Look up carrier and line type (Email + Phone plan)</label>
      </div>

      <h2 style="margin-top:14px">Which cells?</h2>
      <label><input type="radio" name="source" value="selection" checked> My selection</label>
      <p class="hint" id="selection-text"></p>
      <label><input type="radio" name="source" value="column"> A whole column</label>
      <div class="row hidden" id="column-row"><select id="column"></select></div>
      <p class="hint">Row 1 is treated as the header. Results go into new columns to the right.</p>
      <div class="row">
        <button class="action" id="start">Verify</button>
        <button id="refresh">Refresh selection</button>
      </div>
    </section>

    <section id="job-section" class="hidden">
      <h2 id="job-title"></h2>
      <div class="bar"><div id="bar"></div></div>
      <div id="job-progress"></div>
      <p class="hint" id="job-counts"></p>
      <p class="hint" id="job-keep-open">Keep this sidebar open while it runs. If you close it, open it again and press Resume.</p>
      <div class="row">
        <button class="action hidden" id="resume">Resume</button>
        <button class="hidden" id="pause">Pause</button>
        <button id="discard">Discard</button>
      </div>
    </section>

    <div id="message" class="message hidden"></div>

    <script>
      var state = null;
      var running = false;
      var paused = false;

      function $(id) { return document.getElementById(id); }
      function show(id, visible) { $(id).classList.toggle('hidden', !visible); }

      function linkify(text) {
        var escaped = String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
        return escaped.replace(/https:\/\/simpleverifier\.com\/[\w\-\/]*/g, function (url) {
          return '<a href="' + url + '" target="_blank">' + url + '</a>';
        });
      }

      function say(text, kind) {
        var box = $('message');
        if (!text) { show('message', false); return; }
        box.className = 'message ' + (kind || 'ok');
        box.innerHTML = linkify(text);
      }

      function fail(err) {
        running = false;
        setBusy(false);
        say(err && err.message ? err.message : String(err), 'error');
        renderJob(state && state.job);
      }

      function call(name, arg) {
        return new Promise(function (resolve, reject) {
          var runner = google.script.run.withSuccessHandler(resolve).withFailureHandler(reject);
          if (arg === undefined) runner[name](); else runner[name](arg);
        });
      }

      function setBusy(busy) {
        ['save-key', 'start', 'refresh', 'discard', 'resume'].forEach(function (id) { $(id).disabled = busy; });
      }

      function mode() { return document.querySelector('input[name=mode]:checked').value; }
      function source() { return document.querySelector('input[name=source]:checked').value; }

      function render(next) {
        state = next;
        show('key-saved', state.hasKey);
        show('key-form', !state.hasKey);
        $('key-ending').textContent = state.keyEnding;
        show('verify-section', state.hasKey && !state.job);

        var sel = state.selection;
        if (!sel || sel.rows < 1) {
          $('selection-text').textContent = 'Select the cells to verify, then press Refresh selection.';
        } else {
          $('selection-text').textContent = sel.rows + ' rows in column ' + sel.letter + ' of ' + sel.sheet +
            ' (rows ' + sel.firstRow + ' to ' + sel.lastRow + ')' +
            (sel.multipleColumns ? '. Only the first column is used.' : '.');
        }

        var select = $('column');
        var current = select.value;
        select.innerHTML = '';
        state.columns.forEach(function (col) {
          var option = document.createElement('option');
          option.value = col.index;
          option.textContent = col.label;
          select.appendChild(option);
        });
        if (current) select.value = current;

        renderJob(state.job);
      }

      function renderJob(job) {
        show('job-section', Boolean(job) && state && state.hasKey);
        if (!job) return;
        var noun = job.mode === 'phone' ? 'phone numbers' : 'emails';
        $('job-title').textContent = job.finished
          ? 'Done: ' + noun + ' in column ' + job.column
          : 'Verifying ' + noun + ' in column ' + job.column;
        $('bar').style.width = (job.total ? Math.round(job.done / job.total * 100) : 100) + '%';
        $('job-progress').textContent = job.done.toLocaleString() + ' of ' + job.total.toLocaleString() + ' rows checked';
        $('job-counts').textContent = Object.keys(job.counts || {}).map(function (k) {
          return k + ': ' + job.counts[k].toLocaleString();
        }).join(' · ');
        show('job-keep-open', !job.finished);
        show('resume', !job.finished && !running);
        show('pause', !job.finished && running);
        $('discard').textContent = job.finished ? 'Verify another column' : 'Discard';
      }

      function loop() {
        if (paused) {
          running = false;
          setBusy(false);
          say('Paused. Press Resume to carry on where it stopped.', 'ok');
          renderJob(state.job);
          return;
        }
        call('processNext').then(function (job) {
          state.job = job;
          renderJob(job);
          if (!job || job.finished) {
            running = false;
            setBusy(false);
            renderJob(job);
            say('Finished. Results are in the new columns next to column ' + (job ? job.column : '') + '.', 'ok');
            return;
          }
          loop();
        }).catch(fail);
      }

      function run() {
        running = true;
        paused = false;
        setBusy(true);
        say('');
        renderJob(state.job);
        loop();
      }

      function refresh() {
        return call('getState').then(render).catch(fail);
      }

      $('save-key').onclick = function () {
        setBusy(true);
        say('Checking your key...');
        call('saveApiKey', $('key-input').value).then(function (next) {
          $('key-input').value = '';
          setBusy(false);
          say('Key saved.', 'ok');
          render(next);
        }).catch(fail);
      };

      $('change-key').onclick = function () {
        show('key-saved', false);
        show('key-form', true);
        $('key-input').focus();
      };

      $('remove-key').onclick = function () {
        call('removeApiKey').then(function (next) { say('Key removed.', 'ok'); render(next); }).catch(fail);
      };

      document.querySelectorAll('input[name=mode]').forEach(function (input) {
        input.onchange = function () { show('phone-options', mode() === 'phone'); };
      });
      document.querySelectorAll('input[name=source]').forEach(function (input) {
        input.onchange = function () { show('column-row', source() === 'column'); };
      });

      $('refresh').onclick = refresh;

      $('start').onclick = function () {
        var options = {
          mode: mode(),
          source: source(),
          column: Number($('column').value),
          country: $('country').value,
          live: $('live').checked,
        };
        setBusy(true);
        say('');
        call('startJob', options).then(function (job) {
          state.job = job;
          show('verify-section', false);
          run();
        }).catch(fail);
      };

      $('resume').onclick = run;
      $('pause').onclick = function () {
        paused = true;
        say('Pausing after the current batch...');
      };
      $('discard').onclick = function () {
        call('discardJob').then(function (next) { say(''); render(next); }).catch(fail);
      };

      refresh();
    </script>
  </body>
</html>
appsscript.json — paste into appsscript.json
{
  "timeZone": "Etc/UTC",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets.currentonly",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/script.container.ui"
  ],
  "urlFetchWhitelist": [
    "https://simpleverifier.com/api/v1/"
  ]
}

2. Verify a column

  1. Create an API key on your SimpleVerifier API keys page and copy it.
  2. Paste the key into the sidebar and click Save. The key is checked before it is saved.
  3. Choose whether the column holds email addresses or phone numbers. For phones, set the country used for numbers written without +.
  4. Select the cells to verify, or pick a whole column. Row 1 is treated as the header.
  5. Click Verify. Results are written into new columns to the right of your data as each batch finishes.

Keep the sidebar open while it runs. Create your key on the API keys page.

3. Or use a formula for one cell

Once your key is saved, two formulas work anywhere in the sheet. Each fills cells to its right, so leave them empty.

=SV_VERIFY(A2)
→ valid | Mailbox exists

=SV_PHONE(A2, "GB")
→ valid | +447911123456 | mobile | <carrier, on the Email + Phone plan>

The country in SV_PHONE is used only for numbers written without +; it defaults to US. Results are cached in the sheet for six hours so recalculating doesn't repeat the check. For more than a few dozen rows, use the sidebar: Google limits each formula to 30 seconds.

Limits and errors

  • The API allows 120 calls a minute per key. The sidebar runs at up to 100 a minute.
  • "Your API key was not accepted" means the key is wrong or was revoked. Create a new one on the API keys page and save it again.
  • "Your subscription is not active" means the plan behind the key has lapsed. Renew it on the pricing page, then click Resume.
  • Google also caps how many web requests a script can make per day (20,000 on a personal account, 100,000 on Google Workspace). For bigger lists, upload a CSV in the dashboard.

Common questions

How do I verify emails in Google Sheets?
Add the SimpleVerifier script to your sheet (Extensions > Apps Script), save your API key in the sidebar, choose the email column and click Verify. Two new columns, Status and Reason, are filled in: valid, risky or invalid for every address. For one cell, use =SV_VERIFY(A2).
How do I validate phone numbers in Google Sheets?
Open the SimpleVerifier sidebar, choose Phone numbers, set the default country and click Verify. Four new columns are added: Status, E.164, Type and Carrier. For one cell, use =SV_PHONE(A2, "GB"), where the second value is the country for numbers written without a + prefix.
Does it tell me whether a phone number is in service?
No. It checks that the number fits its country's numbering plan, returns the E.164 form and the line type from the numbering plan, and on the Email + Phone plan adds the carrier and real line type from a live lookup. No check can confirm someone is using a number today.
What do I need to use it?
An active SimpleVerifier plan and an API key. Pro ($29.99/month) covers email verification and phone format checks. Email + Phone ($49.99/month) adds 5,000 live carrier lookups a month. API access is included in both, with no per-call credits.
How long does a column take?
The add-on runs up to 100 checks a minute, under the API limit of 120 a minute per key, so 1,000 rows take about ten minutes. Most checks finish in a second or two; a slow mail server can take up to about 30 seconds. For lists of tens of thousands of rows, upload a CSV in the dashboard instead.
What happens with a long list and Google's 6-minute script limit?
The sidebar runs the list in short batches and saves its position after each one, so no single run comes near the limit. If you close the sidebar, open it again and click Resume to carry on from the last saved row.
Where is my API key stored?
In your Google account's user properties for this script, which other users of the sheet cannot read. It is not written into the sheet or logged. In a shared spreadsheet, Google runs formulas with the spreadsheet owner's saved key, so other editors should use the sidebar.
Why does a formula show #ERROR?
Hover the cell to read the message. The usual causes are no saved key, a key that is not accepted (create a new one on the API keys page), an inactive subscription (see pricing), or more than 120 checks in a minute. Google stops a formula after 30 seconds, so a very slow mail server can also time out; the sidebar does not have that limit.

Start verifying in your sheet

Pick a plan, create an API key and paste it into the sidebar. Full endpoint details are in the API docs.

See plans

Ready to clean your email and phone lists?

Start verifying emails and phone numbers today and boost your deliverability.

Get Started

Cancel anytime • No setup required