EmailCheckerEmailChecker

Guia técnico · 12 min de leitura

API de validação de email: guia técnico 2026

Dois endpoints, um fluxo: envia o lote, pola até completed, lê os resultados. Autenticação Bearer, rate limiting, retry exponencial e migração de mails.so — pra DEVs e ops que integram validação de email em produção, com o contrato real e nada de endpoint que não existe.

Neste guia

  1. 01O que é uma API de validação de email
  2. 02Os dois endpoints: enviar lote e consultar resultado
  3. 03Validação em tempo real — o padrão correto
  4. 04Autenticação Bearer e rotação de API keys
  5. 05Rate limiting (300/min, 10.000/h) e como respeitar
  6. 06Tratamento de erros e retry exponencial
  7. 07Polling eficiente: intervalo, backoff e estados terminais
  8. 08Migração de mails.so: o que mapeia e o que não existe
  9. 09Custos, plano Gold e monitoramento
  10. 10Perguntas frequentes

1. O que é uma API de validação de email

API de validação de email é um endpoint HTTP que recebe uma lista de endereços e retorna se cada um é entregável, sem mandar mensagem real. É a forma técnica de integrar validação em qualquer aplicação — signup form, ETL de CRM, fila de campanha — independente de linguagem ou framework.

Diferente de bibliotecas regex client-side (que só checam formato), uma API faz o trabalho completo: parse RFC 5322, resolução DNS/MX, conexão SMTP e negociação RCPT TO. Esse trabalho exige rede + estado + dados de reputação que não fazem sentido rodar no browser ou no app cliente. Pra entender as 4 camadas técnicas (sintaxe, DNS, SMTP, RCPT), veja a pillar conceitual de validação.

O EmailChecker expõe a API em https://app.emailchecker.email/api/v1. Contrato REST padrão: requests com JSON, autenticação Bearer, status codes HTTP convencionais, headers de rate limit. Sem SDK proprietário obrigatório — qualquer cliente HTTP serve. A API é exclusiva do plano Gold.

Uma coisa que muda o desenho da sua integração e é melhor você saber agora: a API é inteiramente assíncrona e baseada em lote. Não existe endpoint de validação única síncrona e não existe webhook de conclusão. São dois endpoints, e o fluxo é sempre o mesmo: envia o lote → pola até completed → lê os resultados.

2. Os dois endpoints: enviar lote e consultar resultado

É isso. Não tem terceiro endpoint.

2.1 POST /api/v1/batch — enviar lote

Recebe de 1 a 100.000 emails em um único request e devolve 201 com o id do lote. O campo name é opcional (rótulo pra você achar o lote no painel depois). O processamento roda em background — essa resposta não traz nenhum resultado de validação.

curl -X POST https://app.emailchecker.email/api/v1/batch \
  -H "Authorization: Bearer ec_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": ["joao@empresa.com.br", "maria@empresa.com.br"],
    "name": "leads-webinar-2026-05"
  }'

# 201 Created
{
  "data": {
    "id": "3f0c8a12-9d4e-4a7b-b1c2-8e5f6a7b9c01",
    "name": "leads-webinar-2026-05",
    "created_at": "2026-05-20T14:03:11.482Z",
    "user_id": "9a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d",
    "size": 2
  }
}

Persista o id antes de qualquer outra coisa. Ele é a única chave pra buscar o resultado depois — se você perder o id, perdeu o lote (e os créditos). name vem null quando você não manda.

2.2 GET /api/v1/batch/{id} — consultar resultado

Devolve o estado do lote. status assume pending, processing, completed, failed ou cancelled. O array emails só vem preenchido quando status === "completed" — antes disso ele vem vazio, o que não significa "nenhum email válido".

curl https://app.emailchecker.email/api/v1/batch/3f0c8a12-9d4e-4a7b-b1c2-8e5f6a7b9c01 \
  -H "Authorization: Bearer ec_live_xxxxxxxxxxxx"

# 200 — ainda rodando: emails[] vazio
{
  "data": {
    "id": "3f0c8a12-9d4e-4a7b-b1c2-8e5f6a7b9c01",
    "status": "processing",
    "created_at": "2026-05-20T14:03:11.482Z",
    "finished_at": null,
    "size": 2,
    "emails": []
  }
}

# 200 — terminou: emails[] preenchido
{
  "data": {
    "id": "3f0c8a12-9d4e-4a7b-b1c2-8e5f6a7b9c01",
    "status": "completed",
    "created_at": "2026-05-20T14:03:11.482Z",
    "finished_at": "2026-05-20T14:03:19.907Z",
    "size": 2,
    "emails": [
      {
        "email": "joao@empresa.com.br",
        "result": "deliverable",
        "reason": "accepted_email",
        "score": 92,
        "isv_format": true,
        "isv_domain": true,
        "isv_mx": true,
        "isv_noblock": true,
        "isv_nocatchall": true,
        "isv_nogeneric": true,
        "is_free": false
      },
      {
        "email": "maria@empresa.com.br",
        "result": "undeliverable",
        "reason": "rejected_email",
        "score": 0,
        "isv_format": true,
        "isv_domain": true,
        "isv_mx": true,
        "isv_noblock": true,
        "isv_nocatchall": true,
        "isv_nogeneric": true,
        "is_free": false
      }
    ]
  }
}

2.3 Os campos de cada email

Code examples em Node, Python, PHP e Go disponíveis em /docs-api/exemplos.

2.4 Envelope de erro

Qualquer erro vem no mesmo formato, em qualquer status code:

{
  "data": null,
  "error": "Batch not found"
}

error é texto legível pra humano, não código estável. Sua lógica de tratamento deve olhar o status code HTTP; a string serve pra log e pra você entender o que aconteceu.

3. Validação em tempo real — o padrão correto

3.1 Não existe endpoint síncrono

Toda validação passa por lote, mesmo que o lote tenha 1 email. E o resultado leva alguns segundos, porque depende de DNS + handshake SMTP com o servidor do destinatário. Conclusão prática: não dá pra prender o request do seu formulário esperando a resposta. Se você tentar, vai acoplar o tempo de resposta do seu signup ao MX de terceiros — e um MX lento vira timeout no seu cadastro.

3.2 O padrão recomendado: validar em fila

  1. Sintaxe + resolução de MX no seu próprio backend (barato, <100ms, custo zero) — isso já barra erro de digitação na hora.
  2. Aceita o cadastro e enfileira o email pra validação.
  3. Um worker envia o lote (dá pra agrupar os cadastros dos últimos N segundos em um lote só) e guarda o id.
  4. O mesmo worker pola o GET; quando vier completed, grava result e score no lead.
  5. A ação de negócio (bloquear conta, exigir confirmação, marcar como suspeito) acontece nesse momento, não no submit.

3.3 Quando o usuário precisa ver o resultado

Se a UX exige feedback, faça o feedback ser assíncrono também: aceita o cadastro, mostra "verificando email…" e atualiza a tela via polling do seu backend ou websocket. Um lote de 1 email costuma terminar em poucos segundos, mas isso não é contrato — trate sempre como assíncrono e você nunca vai ter um formulário travado.

3.4 Volume alto e limpeza de lista

Aqui o modelo de lote joga a favor: 1 request submete até 100.000 emails. O anti-padrão é o inverso do de sempre — nunca quebre uma lista de 5.000 emails em 5.000 lotes de 1. É mais lento, gera 10.000 requests de polling e não melhora nada.

3.5 Código: enviar e aguardar

const BASE = 'https://app.emailchecker.email/api/v1';
const headers = { Authorization: `Bearer ${process.env.EC_API_KEY}` };

async function enviarLote(emails, name) {
  const r = await fetch(`${BASE}/batch`, {
    method: 'POST',
    headers: { ...headers, 'Content-Type': 'application/json' },
    body: JSON.stringify({ emails, name }),
  });
  const json = await r.json();
  if (!r.ok) throw new Error(json.error ?? `HTTP ${r.status}`);
  return json.data.id;   // guarde ISSO no seu banco, antes de qualquer outra coisa
}

async function aguardarLote(id, { intervaloMs = 5000, tetoMs = 60_000, prazoMs = 1_800_000 } = {}) {
  const limite = Date.now() + prazoMs;
  let espera = intervaloMs;

  while (Date.now() < limite) {
    await new Promise((res) => setTimeout(res, espera));

    const r = await fetch(`${BASE}/batch/${id}`, { headers });
    const json = await r.json();
    if (!r.ok) throw new Error(json.error ?? `HTTP ${r.status}`);

    const { status, emails } = json.data;
    if (status === 'completed') return emails;            // só aqui emails[] vem preenchido
    if (status === 'failed' || status === 'cancelled') {
      throw new Error(`lote ${id} terminou como ${status}`);
    }
    espera = Math.min(Math.round(espera * 1.5), tetoMs);  // backoff até o teto
  }
  throw new Error(`prazo esgotado aguardando o lote ${id}`);
}

// uso
const id = await enviarLote(['joao@empresa.com.br'], 'signup');
const [resultado] = await aguardarLote(id);
if (resultado?.result === 'undeliverable') { /* bloqueia o lead */ }

4. Autenticação Bearer e rotação de API keys

4.1 Formato do header

Todas as requests precisam do header Authorization: Bearer ec_live_xxxxxxxx. A key é gerada no painel em app.emailchecker.email e vale pros dois endpoints.

# OK
Authorization: Bearer ec_live_a1b2c3d4e5f6

# Erros comuns
Authorization: ec_live_a1b2c3d4e5f6         # falta "Bearer "
Authorization: Bearer ec_live_xxx,Bearer y  # múltiplos tokens
authorization: bearer ec_live_xxx           # OK (HTTP headers são case-insensitive)

4.2 Storage seguro de keys

Nunca commit keys em repo, mesmo em .env. Padrões:

4.3 Rotação de keys sem downtime

Cenário: time de segurança detectou possível vazamento, precisa trocar a key sem quebrar produção.

  1. No painel, gera a nova key (mantém a antiga ativa).
  2. Deploy do app lendo a nova key do env var.
  3. Confirma nos seus próprios logs que 100% das chamadas já saem com a key nova — não existe endpoint que reporte uso por key.
  4. Revoga a antiga no painel.

Se o painel permitir mais de uma key ativa ao mesmo tempo, mantenha sempre duas em produção: a janela de troca simplesmente deixa de existir, e qualquer vazamento futuro tem rota de recuperação imediata.

5. Rate limiting (300/min, 10.000/h) e como respeitar

Limites reais: 300 requisições por minuto e 10.000 por hora, por API key (não por IP). São duas janelas independentes — você pode ter folga no minuto e estar no teto da hora.

5.1 Headers de rate limit

Toda resposta vem com dois headers que te dizem exatamente onde você está:

x-rate-limit-remaining-minute: 297
x-rate-limit-remaining-hour: 9863

Exporte os dois como gauge no seu Prometheus/Datadog. Ver o remaining-hour descendo em rampa é o aviso de que o seu polling vai virar 429 antes do fim do dia.

5.2 Quando você bate o limite (HTTP 429)

HTTP/1.1 429 Too Many Requests
x-rate-limit-remaining-minute: 0
x-rate-limit-remaining-hour: 8420

{
  "data": null,
  "error": "Too many requests"
}

Não existe header Retry-After — quem decide quanto esperar é você, com base nos dois headers acima. Se remaining-minute zerou mas remaining-hour tem folga, esperar a virada do minuto resolve. Se o de hora zerou, esperar 60 segundos não adianta nada: você precisa reduzir a frequência, não repetir mais rápido.

5.3 Client-side throttling

O jeito confiável de nunca ver 429 é passar todas as chamadas por um limitador. Em Node, com bottleneck:

import Bottleneck from 'bottleneck';

const limiter = new Bottleneck({
  reservoir: 300,                    // 300 requests
  reservoirRefreshAmount: 300,
  reservoirRefreshInterval: 60_000,  // por minuto
  maxConcurrent: 10,
});

// POST /batch e GET /batch/{id} passam pelo mesmo limitador —
// os dois consomem a MESMA cota de 300/min por key.
export const ecFetch = (path, init = {}) =>
  limiter.schedule(() =>
    fetch(`https://app.emailchecker.email/api/v1${path}`, {
      ...init,
      headers: {
        Authorization: `Bearer ${process.env.EC_API_KEY}`,
        ...init.headers,
      },
    })
  );

5.4 Estratégia: lote grande > muitos lotes pequenos

Enviar 100.000 emails custa 1 request do seu orçamento de rate limit. Enviar os mesmos 100.000 em lotes de 100 custa 1.000 requests de envio mais o polling de 1.000 lotes — e aí sim você estoura. Junte o máximo que der em cada lote: o rate limit conta requests, não emails.

6. Tratamento de erros e retry exponencial

6.1 Categorias de erro

Status codes HTTP têm semântica precisa — você deve tratar cada categoria diferente. O corpo sempre vem como { "data": null, "error": "..." }.

400

Bad Request

Payload inválido: JSON malformado, campo emails ausente ou vazio, lote acima de 100.000, email malformado. NÃO retry — corrija o request.

401 / 403

Auth

Key inválida, revogada ou conta sem acesso à API (ela é exclusiva do plano Gold). NÃO retry — verifique a key e o plano.

404

Not Found

Id de lote inexistente, digitado errado, ou pertencente a outra conta. NÃO retry.

429

Rate Limit

Passou de 300/min ou 10.000/h. Leia os headers x-rate-limit-remaining-* pra saber qual janela estourou e espere a janela virar.

500 / 502 / 503 / 504

Server

Erro nosso ou indisponibilidade temporária. Retry com exponential backoff — e apenas em GET (veja 6.3).

6.2 Exponential backoff pra 5xx + 429

// Use SOMENTE em GET /batch/{id}. Nunca em POST /batch — veja 6.3.
async function getComRetry(url, options, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const r = await fetch(url, options);

    // sucesso ou erro do cliente — retorna sem retry
    if (r.ok) return r;
    if (r.status >= 400 && r.status < 500 && r.status !== 429) return r;

    // 429: não há Retry-After. Espera a janela do minuto virar;
    // se foi a cota de HORA que zerou, desiste e deixa o caller reduzir a frequência.
    if (r.status === 429) {
      if (Number(r.headers.get('x-rate-limit-remaining-hour') ?? 1) === 0) {
        throw new Error('cota horária esgotada — reduza a frequência de polling');
      }
      await new Promise((res) => setTimeout(res, 60_000));
      continue;
    }

    // 5xx: exponential backoff com jitter
    const base = Math.min(1000 * 2 ** attempt, 30_000); // cap em 30s
    const jitter = Math.random() * base * 0.3;
    await new Promise((res) => setTimeout(res, base + jitter));
  }
  throw new Error('max retries exceeded');
}

6.3 POST /api/v1/batch não é idempotente

Não existe header de idempotência. Se você faz retry de um POST que já tinha completado do lado do servidor (resposta perdida na rede, timeout do seu cliente), você cria um segundo lote — e paga os créditos duas vezes. Regras que evitam isso:

7. Polling eficiente: intervalo, backoff e estados terminais

Como não existe webhook, callback ou qualquer notificação de saída — a plataforma nunca faz request pro seu servidor — o polling é o mecanismo de entrega. Vale a pena fazer direito.

7.1 Intervalo e backoff

7.2 A conta que evita o 429

O limite é por API key, e todos os seus lotes em voo dividem a mesma cota. A conta é direta:

requests_por_minuto = lotes_em_voo × (60 / intervalo_em_segundos)

# 50 lotes abertos, poll a cada 5s  → 50 × 12 = 600 req/min  ❌ estoura os 300
# 50 lotes abertos, poll a cada 30s → 50 × 2  = 100 req/min  ✅
# e cuidado com a janela de hora: 100 req/min × 60 = 6.000/h (teto 10.000)

Padrão que escala melhor: um único worker varrendo os lotes abertos em ciclos, em vez de um timer independente por lote. Com um worker só, você controla a frequência total num lugar e o backoff vira propriedade do ciclo, não de cada lote.

7.3 Estados terminais: completed, failed, cancelled

Três dos cinco status são finais. Parar de pollar neles não é otimização, é correção — um loop que só sai no completed pola pra sempre um lote que falhou.

7.4 Checklist do consumer

  1. Gravou o id no banco assim que o 201 chegou.
  2. Um worker (não N timers) percorre os lotes com status não-terminal.
  3. Backoff com teto, prazo máximo e alerta ao estourar o prazo.
  4. Os cinco status tratados explicitamente — sem else genérico que engole failed.
  5. emails[] vazio só é lido quando status === "completed".

8. Migração de mails.so: o que mapeia e o que não existe

Se você já integra mails.so, metade da migração é troca de string. A outra metade depende de você estar usando o endpoint síncrono ou não — e vale ser honesto: se estiver, tem refatoração de verdade pela frente. Veja o comparativo técnico completo com tabela de feature matching.

8.1 Mapping de endpoints

mails.soEmailCheckerNota
POST /v1/batchPOST /api/v1/batchEquivalente. Body { emails, name }, de 1 a 100.000. Devolve data.id.
GET /v1/batch/{id}GET /api/v1/batch/{id}Equivalente. Resultados em data.emails[], só quando status = completed.
Header x-mails-api-keyAuthorization: Bearer ec_live_...Equivalente. Só muda o nome e o formato do header.
Checagem única síncrona (GET /v1/check?email=)Não existeSem equivalente. Mande um lote de 1 email e pole o id. Nenhum path /validate/... responde — dá 404.
Webhook / callback de conclusãoNão existeSem equivalente. A plataforma não faz requests pro seu servidor: o único jeito de saber que terminou é polling (seção 7).

8.2 Mapping de response fields

Aqui a migração é quase indolor: os nomes dos campos batem. O que muda é onde eles moram — no EmailChecker cada item vive dentro de data.emails[] do GET, não em data de uma resposta síncrona.

mails.soEmailCheckerNota
data.resultdata.emails[].resultMesmo enum: deliverable / undeliverable / risky / unknown
data.reasondata.emails[].reasonTexto do motivo
data.scoredata.emails[].score0 a 100
data.isv_format / isv_domain / isv_mxmesmos nomesBooleans — sintaxe, domínio, MX
data.isv_noblock / isv_nocatchall / isv_nogenericmesmos nomesBooleans — sem bloqueio, sem catch-all, não genérico
data.is_freedata.emails[].is_freeBoolean — provedor gratuito
Qualquer campo fora da lista acimaNão retornadoOs sete campos isv_*/is_free acima, mais result, reason e score, são tudo que vem por email.

8.3 Diff típico de migração

Quem já usava lote troca três linhas:

- const BASE = 'https://api.mails.so/v1';
+ const BASE = 'https://app.emailchecker.email/api/v1';

  const r = await fetch(`${BASE}/batch`, {
    method: 'POST',
-   headers: { 'x-mails-api-key': KEY },
+   headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ emails, name: 'minha-lista' }),
  });
  const { data } = await r.json();
  const id = data.id;

Quem usava a checagem única síncrona precisa trocar a chamada por um envio de lote de 1 + polling — e, junto, mover a validação pra fora do caminho síncrono do usuário (seção 3):

- const r = await fetch(`${BASE}/check?email=${encodeURIComponent(email)}`, {
-   headers: { 'x-mails-api-key': KEY },
- });
- const { data } = await r.json();
- if (data.result === 'deliverable') { /* ... */ }

+ // não há endpoint síncrono: lote de 1 + polling (enviarLote/aguardarLote da seção 3.5)
+ const id = await enviarLote([email], 'signup');
+ const [resultado] = await aguardarLote(id);
+ if (resultado?.result === 'deliverable') { /* ... */ }

9. Custos, plano Gold e monitoramento

9.1 O que conta como crédito

9.2 Saldo

Não existe endpoint de saldo — o consumo e o que sobra você acompanha no painel, em app.emailchecker.email. Se você precisa de alerta automático de saldo baixo, o caminho é contabilizar do seu lado: some os size dos lotes que você enviou no período e compare com a cota que contratou. É informação que você já tem no seu banco, já que está gravando o id e o size de cada envio.

9.3 Estratégias de redução de custo

9.4 Observabilidade em produção

10. Perguntas frequentes

A API tem endpoint de validação única (síncrona)?
Não. A API inteira são dois endpoints: POST /api/v1/batch pra enviar o lote e GET /api/v1/batch/{id} pra consultar. Pra validar um único email você manda um lote de 1 e faz polling do id. O resultado leva alguns segundos, então o padrão recomendado é validar de forma assíncrona (fila/worker) e nunca prender o submit do formulário esperando resposta.
Como eu sei que o lote terminou? Tem webhook?
Não existe webhook, callback ou notificação de saída — a plataforma não faz requests pro seu servidor. O único jeito de saber que terminou é polling do GET /api/v1/batch/{id}. Enquanto status for pending ou processing o array emails vem vazio; ele só vem preenchido quando status é completed. failed e cancelled também são estados terminais: pare de pollar.
Qual o rate limit real da API?
300 requisições por minuto e 10.000 por hora, por API key. Toda resposta traz os headers x-rate-limit-remaining-minute e x-rate-limit-remaining-hour — use os dois como métrica no seu monitoramento. Na prática o limite raramente aperta, porque 1 request submete até 100.000 emails; quem estoura é quem faz polling agressivo de muitos lotes ao mesmo tempo.
Como rotaciono API keys sem downtime?
Padrão zero-downtime: (1) gere a nova key no painel em app.emailchecker.email, mantendo a antiga ativa, (2) faça deploy do app lendo a key nova do env var, (3) confirme nos SEUS logs que 100% das chamadas já usam a nova, (4) só então revogue a antiga. Se o painel permitir mais de uma key ativa, mantenha sempre duas — a janela de troca simplesmente deixa de existir.
O que fazer quando recebo HTTP 429?
Pare de mandar request e espere a janela virar (o limite por minuto reseta a cada minuto). Olhe x-rate-limit-remaining-minute e x-rate-limit-remaining-hour pra saber qual dos dois estourou: se o de hora zerou, esperar 60 segundos não resolve. Regra dura: repetir automaticamente só GET. POST /api/v1/batch não é idempotente — retry cego cria um lote novo e consome créditos de novo.
Migrar de mails.so pro EmailChecker dá muito trabalho?
Depende do que você usava. O fluxo de lote é quase drop-in: os campos de resultado têm os mesmos nomes (result, reason, score, isv_format, isv_domain, isv_mx, isv_noblock, isv_nocatchall, isv_nogeneric, is_free) e só muda a URL base, o header de auth e o fato de os resultados virem dentro de data.emails[]. Quem usava o endpoint síncrono de checagem única precisa refatorar pra fila: não há equivalente, nem webhook de conclusão.
Quantos emails cabem em um lote e quanto isso custa?
De 1 a 100.000 emails por lote, num único request. A resposta 201 devolve size — é exatamente quantos emails entraram e, portanto, quantos créditos aquele lote consome. Não há endpoint de saldo: o consumo você acompanha no painel. A API é exclusiva do plano Gold.
Como monitoro a saúde da integração em produção?
Quatro métricas: (1) erros por status code — 4xx persistente é bug do seu lado, 5xx é do nosso; (2) tempo entre created_at e finished_at por lote, pra detectar fila crescendo; (3) proporção de lotes que terminam em failed; (4) os headers x-rate-limit-remaining-minute e x-rate-limit-remaining-hour como gauge, pra ver o polling apertando antes de virar 429. Logue sempre o id do lote — é a única chave pra recuperar o resultado depois.

Continue lendo

Próximos passos

Comece agora

Pronto pra parar de mandar email pra endereço morto?

Comece grátis com 500 créditos. Sem cartão, sem compromisso.