Copy and paste code to verify an email address with the MailTester Ninja API. Each example is the same GET request, wrapped for your language, with the verdict handling and the free tier pacing already done. Every address is checked in real time against the recipient’s mail server, and we keep no copy of the addresses you verify. The same examples are on GitHub: mailtester-ninja/mailtester-api-examples.

What you need

  • Nothing to try it: key=free gives 10 checks a day per IP, one request every 30 seconds.
  • A free API token for 50 checks a day, no card.
  • A paid key for full speed, from 100,000 checks a day.

Every example reads your key from the MAILTESTER_KEY environment variable. Branch on the code field: ok valid, ko invalid, mb uncertain, -- no verdict (key error or free limit, HTTP 429).

cURL

The API is a single GET request, so cURL is all you need to try it. verify.sh checks one address and prints what to do with it, using MAILTESTER_KEY when it is set and the free tier otherwise.

Run it

curl "https://happy.mailtester.ninja/ninja?email=info@apple.com&key=free"
MAILTESTER_KEY=your_key ./verify.sh someone@example.com

verify.sh

#!/usr/bin/env bash
# Verify one email with the MailTester Ninja API.
# Usage: ./verify.sh someone@example.com                    (free tier, key=free, 10 checks a day)
#        MAILTESTER_KEY=your_key ./verify.sh someone@example.com
# Free token (50 a day): https://mailtester.ninja/free-email-verification-api/
# Paid key (full speed):  https://mailtester.ninja/verify-emails-unlimited/
set -euo pipefail

EMAIL="${1:?Usage: ./verify.sh email@example.com}"
KEY="${MAILTESTER_KEY:-free}"

ENC=$(jq -rn --arg e "$EMAIL" '$e|@uri')
OUT=$(curl -s -w '\n%{http_code}' "https://happy.mailtester.ninja/ninja?email=${ENC}&key=${KEY}")
STATUS=$(echo "$OUT" | tail -n1)
RESP=$(echo "$OUT" | sed '$d')

if [ "$STATUS" = "429" ]; then
  # Free limit reached: this is not a verdict, the message says how long to wait.
  echo "$RESP" | jq -r '.message'
  exit 2
fi

echo "$RESP" | jq
CODE=$(echo "$RESP" | jq -r '.code')

case "$CODE" in
  ok) echo "-> valid, safe to send" ;;
  ko) echo "-> invalid, will bounce, drop it" ;;
  mb) echo "-> uncertain (catch-all or unprobeable), decide by risk" ;;
  *)  echo "-> unexpected response" ;;
esac

Python

Needs only requests. The script verifies one address, cleans a list at the right pace, and includes a CSV cleaner you can switch on.

Run it

pip install requests
export MAILTESTER_KEY=your_key   # optional: the free tier (key=free) is used when unset
python mailtester.py

mailtester.py

"""
MailTester Ninja, email verification in Python.

Verify one address or clean a whole list, over the real time API.
No dependencies beyond `requests`:  pip install requests

Keys:
  - none set      -> the free tier is used (key=free, 10 checks a day, one request every 30 s)
  - free token    -> 50 checks a day, one request every 30 s: https://mailtester.ninja/free-email-verification-api/
  - paid key      -> full speed: https://mailtester.ninja/verify-emails-unlimited/
Docs: https://mailtester.ninja/api/
"""

import os
import time
import csv
from urllib.parse import quote

import requests

ENDPOINT = "https://happy.mailtester.ninja/ninja"
FREE_KEY = "free"          # no signup, 10 checks a day per IP
FREE_DELAY = 30.0          # free tier: one request every 30 seconds


class FreeLimitError(RuntimeError):
    """The free tier refused the call (HTTP 429): wait, or use a paid key."""


def is_free(key: str) -> bool:
    """True for the keyless free tier and for free tokens (mtf_...)."""
    return key == FREE_KEY or key.startswith("mtf_")


def verify(email: str, key: str = FREE_KEY, timeout: int = 30) -> dict:
    """Verify a single email address. Returns the parsed API response.

    The useful field is `code`:
      "ok" -> valid and deliverable
      "ko" -> invalid, will bounce
      "mb" -> uncertain (catch-all, or the server could not be probed)
    `message` is a human readable label for the verdict.

    Raises FreeLimitError when a free limit is reached (HTTP 429). That
    response is not a verdict: its message says how long to wait.
    """
    resp = requests.get(
        ENDPOINT,
        params={"email": email, "key": key},
        timeout=timeout,
    )
    if resp.status_code == 429:
        raise FreeLimitError(resp.json().get("message", "Free API limit reached"))
    resp.raise_for_status()
    return resp.json()


def is_deliverable(email: str, key: str = FREE_KEY) -> bool:
    """Convenience helper: True only when the address is clearly valid."""
    return verify(email, key).get("code") == "ok"


def clean_list(emails, key, delay: float = None):
    """Verify a list of addresses, yielding (email, result) tuples.

    `delay` seconds between calls keeps you within the rate limit. By default:
    30 s on the free tier, 1 s on a paid key (Pro = 11 / 10s, Ultimate = 23 / 10s).
    """
    if delay is None:
        delay = FREE_DELAY if is_free(key) else 1.0
    for email in emails:
        email = email.strip()
        if not email:
            continue
        try:
            yield email, verify(email, key)
        except (requests.RequestException, FreeLimitError) as exc:
            yield email, {"code": "error", "message": str(exc)}
        time.sleep(delay)


if __name__ == "__main__":
    key = os.environ.get("MAILTESTER_KEY", FREE_KEY)
    if is_free(key):
        print("Free tier: one request every 30 s. Paid keys go full speed: "
              "https://mailtester.ninja/verify-emails-unlimited/\n")

    # 1) Verify one address
    result = verify("info@apple.com", key)
    print(f"{result['email']}: {result['code']} ({result['message']})")
    if is_free(key):
        time.sleep(FREE_DELAY)

    # 2) Clean a list and keep only deliverable addresses
    sample = ["info@apple.com", "test@gmail.com", "someone@example.com"]
    kept = []
    for email, res in clean_list(sample, key):
        verdict = res.get("code")
        label = res.get("message", "")
        print(f"  {email:<30} {verdict:<6} {label}")
        if verdict == "ok":
            kept.append(email)
    print(f"\nDeliverable: {kept}")

    # 3) Clean a CSV (expects a column named 'email'), writes clean.csv
    #    Uncomment to run against your own file.
    # with open("contacts.csv") as fin, open("clean.csv", "w", newline="") as fout:
    #     reader = csv.DictReader(fin)
    #     writer = csv.writer(fout)
    #     writer.writerow(["email", "code", "message"])
    #     for row in reader:
    #         res = verify(row["email"], key)
    #         writer.writerow([row["email"], res.get("code"), res.get("message")])
    #         time.sleep(FREE_DELAY if is_free(key) else 1.0)

Node.js

Uses the built-in fetch of Node.js 18+, nothing to install. Exports verify, isDeliverable and cleanList. Prefer a package? See the official SDK.

Run it

export MAILTESTER_KEY=your_key   # optional: the free tier (key=free) is used when unset
node mailtester.js

mailtester.js

/**
 * MailTester Ninja, email verification in Node.js.
 *
 * Uses the built in fetch (Node 18+), no dependencies.
 * Keys:
 *   none set    -> free tier (key=free, 10 checks a day, one request every 30 s)
 *   free token  -> 50 checks a day: https://mailtester.ninja/free-email-verification-api/
 *   paid key    -> full speed: https://mailtester.ninja/verify-emails-unlimited/
 * Docs: https://mailtester.ninja/api/
 */

const ENDPOINT = 'https://happy.mailtester.ninja/ninja';
const FREE_KEY = 'free';
const FREE_DELAY_MS = 30000;

/** True for the keyless free tier and for free tokens (mtf_...). */
const isFree = (key) => key === FREE_KEY || String(key).startsWith('mtf_');

/**
 * Verify a single email address. Resolves to the parsed API response.
 * The useful field is `code`: "ok" valid, "ko" invalid, "mb" uncertain.
 * Throws when a free limit is reached (HTTP 429): that answer is not a verdict.
 */
async function verify(email, key = FREE_KEY) {
  const url = `${ENDPOINT}?email=${encodeURIComponent(email)}&key=${encodeURIComponent(key)}`;
  const res = await fetch(url);
  if (res.status === 429) {
    const body = await res.json().catch(() => ({}));
    throw new Error(body.message || 'Free API limit reached (HTTP 429)');
  }
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

/** True only when the address is clearly valid and deliverable. */
async function isDeliverable(email, key = FREE_KEY) {
  const r = await verify(email, key);
  return r.code === 'ok';
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

/**
 * Verify a list, respecting the rate limit.
 * delayMs between calls: 30000 on the free tier, ~1000 on Pro, ~450 on Ultimate.
 */
async function cleanList(emails, key, delayMs = isFree(key) ? FREE_DELAY_MS : 1000) {
  const out = [];
  for (const raw of emails) {
    const email = String(raw).trim();
    if (!email) continue;
    try {
      out.push({ email, result: await verify(email, key) });
    } catch (err) {
      out.push({ email, result: { code: 'error', message: err.message } });
    }
    await sleep(delayMs);
  }
  return out;
}

// Run directly:  node mailtester.js   (or MAILTESTER_KEY=your_key node mailtester.js)
if (require.main === module) {
  (async () => {
    const key = process.env.MAILTESTER_KEY || FREE_KEY;
    if (isFree(key)) {
      console.log('Free tier: one request every 30 s. Paid keys go full speed: https://mailtester.ninja/verify-emails-unlimited/\n');
    }

    // 1) one address
    const one = await verify('info@apple.com', key);
    console.log(`${one.email}: ${one.code} (${one.message})`);
    if (isFree(key)) await sleep(FREE_DELAY_MS);

    // 2) clean a list
    const sample = ['info@apple.com', 'test@gmail.com', 'someone@example.com'];
    const rows = await cleanList(sample, key);
    const kept = [];
    for (const { email, result } of rows) {
      console.log(`  ${email.padEnd(30)} ${String(result.code).padEnd(6)} ${result.message || ''}`);
      if (result.code === 'ok') kept.push(email);
    }
    console.log('\nDeliverable:', kept);
  })();
}

module.exports = { verify, isDeliverable, cleanList, isFree };

PHP

Plain cURL, drops into any project or framework (Laravel, Symfony, plain PHP). Holds mailtester_verify, mailtester_is_deliverable for signup forms and mailtester_clean_list.

Run it

export MAILTESTER_KEY=your_key   # optional: the free tier (key=free) is used when unset
php mailtester.php

mailtester.php

<?php
/**
 * MailTester Ninja, email verification in PHP.
 *
 * Plain PHP with cURL, no dependencies, no framework.
 * Keys:
 *   none set    -> free tier (key=free, 10 checks a day, one request every 30 s)
 *   free token  -> 50 checks a day: https://mailtester.ninja/free-email-verification-api/
 *   paid key    -> full speed: https://mailtester.ninja/verify-emails-unlimited/
 * Docs: https://mailtester.ninja/api/
 */

const MAILTESTER_ENDPOINT = 'https://happy.mailtester.ninja/ninja';
const MAILTESTER_FREE_KEY = 'free';
const MAILTESTER_FREE_DELAY = 30.0;

/** True for the keyless free tier and for free tokens (mtf_...). */
function mailtester_is_free(string $key): bool
{
    return $key === MAILTESTER_FREE_KEY || str_starts_with($key, 'mtf_');
}

/**
 * Verify a single email address. Returns the decoded API response as an array.
 * The useful key is 'code': "ok" valid, "ko" invalid, "mb" uncertain.
 *
 * @throws RuntimeException on transport or HTTP error, and when a free limit is
 *         reached (HTTP 429): that answer is not a verdict, its message says how long to wait.
 */
function mailtester_verify(string $email, string $key = MAILTESTER_FREE_KEY, int $timeout = 30): array
{
    $url = MAILTESTER_ENDPOINT . '?' . http_build_query(['email' => $email, 'key' => $key]);

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_USERAGENT      => 'mailtester-php-example',
    ]);
    $body = curl_exec($ch);
    if ($body === false) {
        $err = curl_error($ch);
        curl_close($ch);
        throw new RuntimeException("Request failed: $err");
    }
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($status === 429) {
        $msg = json_decode($body, true)['message'] ?? 'Free API limit reached';
        throw new RuntimeException($msg);
    }
    if ($status < 200 || $status >= 300) {
        throw new RuntimeException("HTTP $status");
    }

    $data = json_decode($body, true);
    if (!is_array($data)) {
        throw new RuntimeException('Invalid JSON response');
    }
    return $data;
}

/** True only when the address is clearly valid and deliverable. */
function mailtester_is_deliverable(string $email, string $key = MAILTESTER_FREE_KEY): bool
{
    return (mailtester_verify($email, $key)['code'] ?? '') === 'ok';
}

/**
 * Verify a list. Returns [email => result]. $delay seconds between calls keeps you
 * within the rate limit: 30 s on the free tier, 1 s on a paid key (Pro 11/10s, Ultimate 23/10s).
 */
function mailtester_clean_list(array $emails, string $key, ?float $delay = null): array
{
    $delay = $delay ?? (mailtester_is_free($key) ? MAILTESTER_FREE_DELAY : 1.0);
    $out = [];
    foreach ($emails as $email) {
        $email = trim((string) $email);
        if ($email === '') {
            continue;
        }
        try {
            $out[$email] = mailtester_verify($email, $key);
        } catch (RuntimeException $e) {
            $out[$email] = ['code' => 'error', 'message' => $e->getMessage()];
        }
        usleep((int) ($delay * 1_000_000));
    }
    return $out;
}

// Run directly:  php mailtester.php   (or MAILTESTER_KEY=your_key php mailtester.php)
if (PHP_SAPI === 'cli' && isset($argv) && realpath($argv[0]) === __FILE__) {
    $key = getenv('MAILTESTER_KEY') ?: MAILTESTER_FREE_KEY;
    if (mailtester_is_free($key)) {
        echo "Free tier: one request every 30 s. Paid keys go full speed: https://mailtester.ninja/verify-emails-unlimited/\n\n";
    }

    // 1) one address
    $one = mailtester_verify('info@apple.com', $key);
    printf("%s: %s (%s)\n", $one['email'], $one['code'], $one['message']);
    if (mailtester_is_free($key)) {
        usleep((int) (MAILTESTER_FREE_DELAY * 1_000_000));
    }

    // 2) clean a list
    $sample = ['info@apple.com', 'test@gmail.com', 'someone@example.com'];
    $kept = [];
    foreach (mailtester_clean_list($sample, $key) as $email => $res) {
        printf("  %-30s %-6s %s\n", $email, $res['code'] ?? '?', $res['message'] ?? '');
        if (($res['code'] ?? '') === 'ok') {
            $kept[] = $email;
        }
    }
    echo "\nDeliverable: " . implode(', ', $kept) . "\n";
}

Go

Standard library only, no modules to add. A typed Result and a Verify function you can copy into your package as is.

Run it

export MAILTESTER_KEY=your_key   # optional: the free tier (key=free) is used when unset
go run mailtester.go

mailtester.go

// MailTester Ninja, email verification in Go. Standard library only.
// Run:  go run mailtester.go   (free tier, key=free, 10 checks a day, one request every 30 s)
//       MAILTESTER_KEY=your_key go run mailtester.go
// Free token (50 a day): https://mailtester.ninja/free-email-verification-api/
// Paid key (full speed):  https://mailtester.ninja/verify-emails-unlimited/
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"
)

const endpoint = "https://happy.mailtester.ninja/ninja"

// freeKey uses the free tier without signup. Free calls are limited to one every 30 s.
const freeKey = "free"

// IsFree reports whether key is the keyless free tier or a free token (mtf_...).
func IsFree(key string) bool { return key == freeKey || strings.HasPrefix(key, "mtf_") }

// Result is the API response. The useful field is Code: "ok", "ko" or "mb".
type Result struct {
	Email   string `json:"email"`
	User    string `json:"user"`
	Domain  string `json:"domain"`
	MX      string `json:"mx"`
	Code    string `json:"code"`
	Message string `json:"message"`
}

// Verify checks a single address. A free limit (HTTP 429) is returned as an error:
// it is not a verdict, its message says how long to wait.
func Verify(email, key string) (*Result, error) {
	u, _ := url.Parse(endpoint)
	q := u.Query()
	q.Set("email", email)
	q.Set("key", key)
	u.RawQuery = q.Encode()

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Get(u.String())
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode == http.StatusTooManyRequests {
		var lim Result
		_ = json.Unmarshal(body, &lim)
		return nil, fmt.Errorf("free limit: %s", lim.Message)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
	}
	var r Result
	if err := json.Unmarshal(body, &r); err != nil {
		return nil, err
	}
	return &r, nil
}

func main() {
	key := os.Getenv("MAILTESTER_KEY")
	if key == "" {
		key = freeKey
	}
	delay := time.Second // respect the rate limit
	if IsFree(key) {
		delay = 30 * time.Second
		fmt.Println("Free tier: one request every 30 s. Paid keys go full speed: https://mailtester.ninja/verify-emails-unlimited/")
	}

	emails := []string{"info@apple.com", "test@gmail.com", "someone@example.com"}
	for _, email := range emails {
		r, err := Verify(email, key)
		if err != nil {
			fmt.Printf("  %-30s error: %v\n", email, err)
		} else {
			fmt.Printf("  %-30s %-6s %s\n", r.Email, r.Code, r.Message)
		}
		time.Sleep(delay)
	}
}

Ruby

Uses net/http, no gems required. Comes with verify and deliverable? helpers.

Run it

export MAILTESTER_KEY=your_key   # optional: the free tier (key=free) is used when unset
ruby mailtester.rb

mailtester.rb

# MailTester Ninja, email verification in Ruby. Standard library only.
# Run:  ruby mailtester.rb   (free tier, key=free, 10 checks a day, one request every 30 s)
#       MAILTESTER_KEY=your_key ruby mailtester.rb
# Free token (50 a day): https://mailtester.ninja/free-email-verification-api/
# Paid key (full speed):  https://mailtester.ninja/verify-emails-unlimited/

require "net/http"
require "uri"
require "json"
require "cgi"

ENDPOINT = "https://happy.mailtester.ninja/ninja"
FREE_KEY = "free"

# True for the keyless free tier and for free tokens (mtf_...).
def free?(key)
  key == FREE_KEY || key.start_with?("mtf_")
end

# Verify a single email address. Returns a Hash (the parsed API response).
# The useful key is "code": "ok" valid, "ko" invalid, "mb" uncertain.
# A free limit (HTTP 429) raises: it is not a verdict, the message says how long to wait.
def verify(email, key = FREE_KEY)
  uri = URI(ENDPOINT)
  uri.query = URI.encode_www_form(email: email, key: key)
  res = Net::HTTP.get_response(uri)
  raise "free limit: #{JSON.parse(res.body)["message"]}" if res.code == "429"
  raise "HTTP #{res.code}" unless res.is_a?(Net::HTTPSuccess)
  JSON.parse(res.body)
end

# True only when the address is clearly valid and deliverable.
def deliverable?(email, key = FREE_KEY)
  verify(email, key)["code"] == "ok"
end

if __FILE__ == $PROGRAM_NAME
  key = ENV.fetch("MAILTESTER_KEY", FREE_KEY)
  delay = free?(key) ? 30 : 1 # respect the rate limit
  puts "Free tier: one request every 30 s. Paid keys go full speed: https://mailtester.ninja/verify-emails-unlimited/" if free?(key)

  ["info@apple.com", "test@gmail.com", "someone@example.com"].each do |email|
    begin
      r = verify(email, key)
      printf("  %-30s %-6s %s\n", r["email"], r["code"], r["message"])
    rescue => e
      printf("  %-30s error: %s\n", email, e.message)
    end
    sleep delay
  end
end

Java

Java 11+ with the built-in HTTP client and no dependencies. For production, parse the JSON with Jackson or Gson instead of the small field extractor.

Run it

export MAILTESTER_KEY=your_key   # optional: the free tier (key=free) is used when unset
java MailTester.java

MailTester.java

// MailTester Ninja, email verification in Java (11+). No dependencies, uses java.net.http.
// Run:  java MailTester.java   (single-file source, Java 11+; free tier, key=free, 10 checks a day)
//       MAILTESTER_KEY=your_key java MailTester.java
// Free token (50 a day): https://mailtester.ninja/free-email-verification-api/
// Paid key (full speed):  https://mailtester.ninja/verify-emails-unlimited/

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MailTester {

    static final String ENDPOINT = "https://happy.mailtester.ninja/ninja";
    static final String FREE_KEY = "free";

    // True for the keyless free tier and for free tokens (mtf_...). Free calls: one every 30 s.
    static boolean isFree(String key) {
        return FREE_KEY.equals(key) || key.startsWith("mtf_");
    }

    // Minimal field extractor so the example stays dependency free.
    // In a real project use Jackson or Gson to parse the JSON response.
    static String field(String json, String name) {
        Matcher m = Pattern.compile("\"" + name + "\"\\s*:\\s*\"([^\"]*)\"").matcher(json);
        return m.find() ? m.group(1) : "";
    }

    static String verify(String email, String key) throws Exception {
        String url = ENDPOINT
                + "?email=" + URLEncoder.encode(email, StandardCharsets.UTF_8)
                + "&key=" + URLEncoder.encode(key, StandardCharsets.UTF_8);
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest req = HttpRequest.newBuilder(URI.create(url)).GET().build();
        HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
        if (res.statusCode() == 429) {
            // Free limit reached: not a verdict, the message says how long to wait.
            throw new RuntimeException("free limit: " + field(res.body(), "message"));
        }
        if (res.statusCode() < 200 || res.statusCode() >= 300) {
            throw new RuntimeException("HTTP " + res.statusCode());
        }
        return res.body();
    }

    public static void main(String[] args) throws Exception {
        String key = System.getenv("MAILTESTER_KEY");
        if (key == null || key.isEmpty()) {
            key = FREE_KEY;
        }
        long delayMs = isFree(key) ? 30000 : 1000; // respect the rate limit
        if (isFree(key)) {
            System.out.println("Free tier: one request every 30 s. Paid keys go full speed: https://mailtester.ninja/verify-emails-unlimited/");
        }
        String[] emails = {"info@apple.com", "test@gmail.com", "someone@example.com"};
        for (String email : emails) {
            try {
                String json = verify(email, key);
                System.out.printf("  %-30s %-6s %s%n", email, field(json, "code"), field(json, "message"));
            } catch (RuntimeException e) {
                System.out.printf("  %-30s error: %s%n", email, e.getMessage());
            }
            Thread.sleep(delayMs);
        }
    }
}

C#

.NET 6+ with HttpClient and System.Text.Json. Create a project with dotnet new console and replace Program.cs.

Run it

dotnet new console
export MAILTESTER_KEY=your_key   # optional: the free tier (key=free) is used when unset
dotnet run

Program.cs

// MailTester Ninja, email verification in C# (.NET 6+). Uses HttpClient + System.Text.Json.
// Run:  dotnet run   (free tier, key=free, 10 checks a day, one request every 30 s)
//       MAILTESTER_KEY=your_key dotnet run
// Free token (50 a day): https://mailtester.ninja/free-email-verification-api/
// Paid key (full speed):  https://mailtester.ninja/verify-emails-unlimited/

using System.Net;
using System.Text.Json;
using System.Web;

string key = Environment.GetEnvironmentVariable("MAILTESTER_KEY") ?? "free";
bool isFree = key == "free" || key.StartsWith("mtf_");
int delayMs = isFree ? 30000 : 1000; // respect the rate limit
if (isFree) Console.WriteLine("Free tier: one request every 30 s. Paid keys go full speed: https://mailtester.ninja/verify-emails-unlimited/");

const string endpoint = "https://happy.mailtester.ninja/ninja";
using var http = new HttpClient();

async Task<JsonElement> Verify(string email)
{
    var url = $"{endpoint}?email={HttpUtility.UrlEncode(email)}&key={HttpUtility.UrlEncode(key)}";
    using var res = await http.GetAsync(url);
    var body = await res.Content.ReadAsStringAsync();
    var json = JsonDocument.Parse(body).RootElement;
    if (res.StatusCode == HttpStatusCode.TooManyRequests)
    {
        // Free limit reached: not a verdict, the message says how long to wait.
        throw new InvalidOperationException("free limit: " + json.GetProperty("message").GetString());
    }
    res.EnsureSuccessStatusCode();
    return json;
}

string[] emails = { "info@apple.com", "test@gmail.com", "someone@example.com" };
foreach (var email in emails)
{
    try
    {
        var r = await Verify(email);
        var code = r.GetProperty("code").GetString();
        var message = r.GetProperty("message").GetString();
        Console.WriteLine($"  {email,-30} {code,-6} {message}");
    }
    catch (Exception e)
    {
        Console.WriteLine($"  {email,-30} error: {e.Message}");
    }
    await Task.Delay(delayMs);
}

Google Sheets

Verify a column right inside Google Sheets with Apps Script. In your sheet, open Extensions, Apps Script, paste the script below, then add MAILTESTER_KEY in Project Settings, Script Properties with a free token or a paid key. Use =MT_VERIFY(A2) in a cell, or run verifyColumn() for a whole column. Step by step: Google Sheets guide. Download Code.gs.

/**
 * MailTester Ninja for Google Sheets, via Apps Script.
 *
 * Setup:
 *   1. In your Sheet: Extensions > Apps Script, paste this file.
 *   2. Project Settings > Script Properties: add MAILTESTER_KEY = your key.
 *      To try it free, use a free token (50 checks a day, one request every 30 s):
 *        https://mailtester.ninja/free-email-verification-api/
 *      key=free does not suit Sheets: Google's servers share their IP addresses,
 *      and the keyless tier is counted per IP.
 *      Paid key (full speed): https://mailtester.ninja/verify-emails-unlimited/
 *   3. Back in the Sheet, use the custom function:  =MT_VERIFY(A2)
 *      or run verifyColumn() from the Apps Script editor for a whole column.
 */

var ENDPOINT = 'https://happy.mailtester.ninja/ninja';

function getKey_() {
  var key = PropertiesService.getScriptProperties().getProperty('MAILTESTER_KEY');
  if (!key) throw new Error('Set MAILTESTER_KEY in Script Properties: a free token from https://mailtester.ninja/free-email-verification-api/ or a paid key from https://mailtester.ninja/verify-emails-unlimited/');
  return key;
}

function verifyOne_(email) {
  var url = ENDPOINT + '?email=' + encodeURIComponent(email) + '&key=' + encodeURIComponent(getKey_());
  var resp = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  if (resp.getResponseCode() === 429) {
    var lim = JSON.parse(resp.getContentText());
    return { code: 'error', message: 'Free limit: ' + lim.message };
  }
  if (resp.getResponseCode() !== 200) return { code: 'error', message: 'HTTP ' + resp.getResponseCode() };
  return JSON.parse(resp.getContentText());
}

/**
 * Custom function:  =MT_VERIFY("someone@example.com")
 * Returns the human readable verdict (Accepted, Rejected, Catch-All, ...).
 * Note: Sheets caches custom function results; edit the cell to refresh.
 */
function MT_VERIFY(email) {
  if (!email) return '';
  return verifyOne_(email).message || '';
}

/**
 * Verify column A (from row 2), writing code to B and message to C.
 * Respects the rate limit with a pause between calls.
 */
function verifyColumn() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var last = sheet.getLastRow();
  for (var row = 2; row <= last; row++) {
    var email = sheet.getRange(row, 1).getValue();
    if (!email) continue;
    var r = verifyOne_(String(email).trim());
    sheet.getRange(row, 2).setValue(r.code || '');
    sheet.getRange(row, 3).setValue(r.message || '');
    Utilities.sleep(getKey_().indexOf('mtf_') === 0 ? 30000 : 1000); // 30 s on a free token, ~1 s on Pro
  }
}

n8n, Make, Zapier and Clay

No code needed: each tool calls the API with its HTTP module, and our guides give the exact fields to fill in.

  • n8n: one HTTP Request node. Ready-made workflow with the IF branch on the verdict: download workflow.json, then in n8n choose Workflows, Import from File and set your key.
  • Make: the HTTP, Make a request module, with a router on the verdict.
  • Zapier: Webhooks by Zapier, then a Filter or Paths step.
  • Clay: an HTTP API enrichment on the email column.
Show the n8n workflow.json
{
  "name": "MailTester Ninja - verify email",
  "nodes": [
    {
      "parameters": {},
      "id": "a1b2c3d4-0001-4000-8000-000000000001",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [240, 300]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "set-email",
              "name": "email",
              "value": "info@apple.com",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "a1b2c3d4-0002-4000-8000-000000000002",
      "name": "Sample email",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [460, 300]
    },
    {
      "parameters": {
        "url": "https://happy.mailtester.ninja/ninja",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            { "name": "email", "value": "={{ $json.email }}" },
            { "name": "key", "value": "YOUR_KEY" }
          ]
        },
        "options": {}
      },
      "id": "a1b2c3d4-0003-4000-8000-000000000003",
      "name": "MailTester Ninja",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [680, 300],
      "notes": "Replace YOUR_KEY with your paid key (https://mailtester.ninja/verify-emails-unlimited/) or, to try it, a free token (https://mailtester.ninja/free-email-verification-api/): 50 checks a day, one request every 30 seconds."
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "typeValidation": "strict" },
          "conditions": [
            {
              "id": "is-ok",
              "leftValue": "={{ $json.code }}",
              "rightValue": "ok",
              "operator": { "type": "string", "operation": "equals" }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "a1b2c3d4-0004-4000-8000-000000000004",
      "name": "Valid?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [900, 300]
    }
  ],
  "connections": {
    "Manual Trigger": { "main": [[{ "node": "Sample email", "type": "main", "index": 0 }]] },
    "Sample email": { "main": [[{ "node": "MailTester Ninja", "type": "main", "index": 0 }]] },
    "MailTester Ninja": { "main": [[{ "node": "Valid?", "type": "main", "index": 0 }]] }
  },
  "settings": { "executionOrder": "v1" },
  "meta": { "templateId": "mailtester-ninja-verify-email" }
}

AI agents

Give an AI agent a verify_email tool so it checks addresses while it works. Keep one rule in your prompt: report the verdict the API returned, never invent one, and treat mb as uncertain. When a free limit is hit, these tools return code: "limit" with the API message, so the model reports the limit instead of a verdict.

OpenAI tool calling

Exposes a verify_email function to an OpenAI model and runs the call loop: the model asks for a check, your code calls the API and hands back {email, code, message}, the model answers with the real verdict.

Run it

pip install openai requests
export OPENAI_API_KEY=...
export MAILTESTER_KEY=your_key   # optional: the free tier (key=free) is used when unset
python verify_tool.py

verify_tool.py

"""
Give an OpenAI model a verify_email tool backed by MailTester Ninja.

pip install openai requests
export OPENAI_API_KEY=...
export MAILTESTER_KEY=...   optional: without it the free tier is used (key=free, 10 checks a day).
                            Free token (50 a day): https://mailtester.ninja/free-email-verification-api/
                            Paid key: https://mailtester.ninja/verify-emails-unlimited/
"""
import os, json, requests
from openai import OpenAI

client = OpenAI()
MT_KEY = os.environ.get("MAILTESTER_KEY", "free")

# The tool the model can call.
TOOLS = [{
    "type": "function",
    "function": {
        "name": "verify_email",
        "description": "Check whether an email address is valid and deliverable, in real time.",
        "parameters": {
            "type": "object",
            "properties": {
                "email": {"type": "string", "description": "The email address to verify."}
            },
            "required": ["email"],
        },
    },
}]

def verify_email(email: str) -> dict:
    r = requests.get("https://happy.mailtester.ninja/ninja",
                     params={"email": email, "key": MT_KEY}, timeout=30)
    if r.status_code == 429:
        # Free tier limit (one request every 30 s, daily quota): not a verdict.
        return {"email": email, "code": "limit", "message": r.json().get("message")}
    r.raise_for_status()
    d = r.json()
    # Hand the model a compact, honest result.
    return {"email": d.get("email"), "code": d.get("code"), "message": d.get("message")}

def run(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]
    resp = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=TOOLS)
    msg = resp.choices[0].message

    if msg.tool_calls:
        messages.append(msg)
        for call in msg.tool_calls:
            args = json.loads(call.function.arguments)
            result = verify_email(args["email"])
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": json.dumps(result),
            })
        resp = client.chat.completions.create(model="gpt-4o-mini", messages=messages, tools=TOOLS)
        msg = resp.choices[0].message

    return msg.content

if __name__ == "__main__":
    print(run("Is info@apple.com a real, deliverable address? Answer VALID, INVALID or UNCERTAIN."))

Claude tool use

The same verify_email tool for Claude, with its input_schema and the tool loop that runs until Claude stops asking for a call.

Run it

pip install anthropic requests
export ANTHROPIC_API_KEY=...
export MAILTESTER_KEY=your_key   # optional: the free tier (key=free) is used when unset
python verify_tool.py

verify_tool.py

"""
Give Claude a verify_email tool backed by MailTester Ninja.

pip install anthropic requests
export ANTHROPIC_API_KEY=...
export MAILTESTER_KEY=...   optional: without it the free tier is used (key=free, 10 checks a day).
                            Free token (50 a day): https://mailtester.ninja/free-email-verification-api/
                            Paid key: https://mailtester.ninja/verify-emails-unlimited/
"""
import os, requests
import anthropic

client = anthropic.Anthropic()
MT_KEY = os.environ.get("MAILTESTER_KEY", "free")

TOOLS = [{
    "name": "verify_email",
    "description": "Check whether an email address is valid and deliverable, in real time.",
    "input_schema": {
        "type": "object",
        "properties": {
            "email": {"type": "string", "description": "The email address to verify."}
        },
        "required": ["email"],
    },
}]

def verify_email(email: str) -> dict:
    r = requests.get("https://happy.mailtester.ninja/ninja",
                     params={"email": email, "key": MT_KEY}, timeout=30)
    if r.status_code == 429:
        # Free tier limit (one request every 30 s, daily quota): not a verdict.
        return {"email": email, "code": "limit", "message": r.json().get("message")}
    r.raise_for_status()
    d = r.json()
    return {"email": d.get("email"), "code": d.get("code"), "message": d.get("message")}

def run(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]
    while True:
        resp = client.messages.create(
            model="claude-opus-5-5",  # or your current Claude model
            max_tokens=16000,
            tools=TOOLS,
            messages=messages,
        )
        if resp.stop_reason != "tool_use":
            return "".join(b.text for b in resp.content if b.type == "text")

        messages.append({"role": "assistant", "content": resp.content})
        tool_results = []
        for block in resp.content:
            if block.type == "tool_use" and block.name == "verify_email":
                result = verify_email(block.input["email"])
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result),
                })
        messages.append({"role": "user", "content": tool_results})

if __name__ == "__main__":
    print(run("Is info@apple.com a real, deliverable address? Answer VALID, INVALID or UNCERTAIN."))

LangChain

A ready-made tool built with the @tool decorator. Add verify_email_tool to your agent’s tool list; it returns a short verdict string.

Run it

pip install langchain-core requests
export MAILTESTER_KEY=your_key   # optional: the free tier (key=free) is used when unset
python mailtester_tool.py

mailtester_tool.py

"""
A LangChain tool that verifies an email with MailTester Ninja.

pip install langchain-core requests
export MAILTESTER_KEY=...   optional: without it the free tier is used (key=free, 10 checks a day).
                            Free token (50 a day): https://mailtester.ninja/free-email-verification-api/
                            Paid key: https://mailtester.ninja/verify-emails-unlimited/

Attach `verify_email_tool` to any LangChain agent. The agent decides when to
call it; it returns a short, honest verdict string.
"""
import os
import requests
from langchain_core.tools import tool

MT_KEY = os.environ.get("MAILTESTER_KEY", "free")

@tool
def verify_email_tool(email: str) -> str:
    """Verify whether an email address is valid and deliverable, in real time.
    Returns the verdict code (ok valid, ko invalid, mb uncertain) and a label."""
    r = requests.get(
        "https://happy.mailtester.ninja/ninja",
        params={"email": email, "key": MT_KEY},
        timeout=30,
    )
    if r.status_code == 429:
        # Free tier limit (one request every 30 s, daily quota): not a verdict.
        return f"free tier limit, no verdict: {r.json().get('message')}"
    r.raise_for_status()
    d = r.json()
    return f"{d.get('code')} ({d.get('message')}) for {d.get('email')}"

if __name__ == "__main__":
    # Call the tool directly (no agent) to check it works.
    print(verify_email_tool.invoke({"email": "info@apple.com"}))

Ready-to-use prompts

For ChatGPT, Claude or any agent framework that can call a URL.

System prompt: give the agent the skill

You can verify whether an email address is real and deliverable using the
MailTester Ninja API. To verify an address, make a GET request to:
  https://happy.mailtester.ninja/ninja?email=<EMAIL>&key=<YOUR_KEY>
URL encode the email. Read the JSON field "code":
  - "ok": the address is valid and deliverable.
  - "ko": the address is invalid and will bounce.
  - "mb": uncertain. The domain is catch-all or could not be probed; do not
    claim it is valid.
Report the verdict using the "message" field, and never invent a result:
only state what the API returned. If the HTTP status is 429, a free tier
limit was reached: say so, wait as the message says, and give no verdict.

One-shot prompt

Verify this email with MailTester Ninja and tell me if it is safe to send to:
<EMAIL>
Call the endpoint, then answer in one line: VALID, INVALID, or UNCERTAIN,
followed by the message.

Bulk clean prompt

Here is a list of emails. Verify each one with MailTester Ninja, then return
a table with columns: email, code, message, keep (yes if code is ok, no
otherwise). Verify them one at a time and respect the rate limit.
<PASTE LIST>
  • Tell the agent to report, not guess: an agent that fabricates a valid verdict defeats the purpose.
  • Treat mb as uncertain, not valid: catch-all domains accept everything.
  • On the free tier, an HTTP 429 answer is a limit, not a verdict.
  • Keep the key in an environment variable or the framework’s secret store, never in a prompt you log.

Official SDKs

  • JavaScript and TypeScript: npm install @mailtester-ninja/client, for Node.js 18+, Deno, Bun and browsers. npm package, source.
  • Python: pip install mailtester-ninja, Python 3.9+, standard library only. PyPI package, source.

Try it now

Get a free API token, or see the plans for up to 200,000 checks a day. Full reference: API documentation and OpenAPI specification. Back to all integrations.