EmailCheckerEmailChecker

Guia técnico · 13 min de leitura

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

Endpoints, autenticação, rate limiting, retry exponencial, webhooks com HMAC e migração drop-in de mails.so. Pra DEVs e ops que integram validação de email em produção — código real, padrões de erro, idempotência.

Neste guia

  1. 01O que é uma API de validação de email
  2. 02Endpoints essenciais: single, batch, webhook
  3. 03Síncrono vs assíncrono — quando usar cada um
  4. 04Autenticação Bearer e rotação de API keys
  5. 05Rate limiting (60/min, 1000/h) e como respeitar
  6. 06Tratamento de erros e retry exponencial
  7. 07Webhooks: assinatura HMAC e idempotência
  8. 08Drop-in compatível com mails.so
  9. 09Custos: créditos vs requests 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 um endereço (ou lista) e retorna se ele é 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.

2. Endpoints essenciais: single, batch, webhook

Três endpoints cobrem 95% dos casos de uso:

2.1 POST /v1/validate/single — validação síncrona

Valida um único email e retorna o resultado na mesma resposta. Latência típica 1–3 segundos (depende do servidor MX do destinatário).

curl -X POST https://app.emailchecker.email/api/v1/validate/single \
  -H "Authorization: Bearer ec_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"email": "joao@empresa.com.br"}'

# Resposta
{
  "email": "joao@empresa.com.br",
  "result": "deliverable",
  "score": 92,
  "reason": "accepted_email",
  "is_disposable": false,
  "is_role": false,
  "is_catch_all": false,
  "mx_record": "aspmx.l.google.com",
  "credits_remaining": 4998
}

2.2 POST /v1/validate/batch — validação assíncrona em massa

Submete uma lista (até 100k emails por job) e retorna job_id imediato. O processamento roda em background; você consulta o resultado por polling ou recebe via webhook.

curl -X POST https://app.emailchecker.email/api/v1/validate/batch \
  -H "Authorization: Bearer ec_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": ["a@x.com", "b@y.com", "c@z.com"],
    "webhook_url": "https://seu-app.com/webhook/ec"
  }'

# Resposta
{
  "job_id": "job_01HZK8WX3Q",
  "status": "queued",
  "submitted_count": 3,
  "estimated_completion_seconds": 8
}

2.3 GET /v1/validate/batch/{job_id} — polling de resultado

Use polling quando webhook não é viável (firewall corporativo, ambiente local). Padrão sensato: polling com backoff — 5s, 10s, 20s, 30s — não mais agressivo que isso ou desperdiça rate limit.

curl https://app.emailchecker.email/api/v1/validate/batch/job_01HZK8WX3Q \
  -H "Authorization: Bearer ec_live_xxxxxxxxxxxx"

# Resposta (em progresso)
{
  "job_id": "job_01HZK8WX3Q",
  "status": "processing",
  "submitted_count": 1000,
  "processed_count": 423
}

# Resposta (completo)
{
  "job_id": "job_01HZK8WX3Q",
  "status": "completed",
  "submitted_count": 1000,
  "processed_count": 1000,
  "results": [
    { "email": "a@x.com", "result": "deliverable", "score": 92 },
    { "email": "b@y.com", "result": "undeliverable", "score": 0 },
    /* ... */
  ]
}

2.4 Webhook: callback assíncrono

Em vez de polling, registra uma URL no batch (webhook_url) e o EC manda POST quando o job termina. Detalhes de assinatura e idempotência na seção 7.

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

3. Síncrono vs assíncrono — quando usar cada um

3.1 Síncrono (single endpoint)

Use quando:

Anti-padrão: nuncaitere single em loop pra validar 5000 emails. Vai estourar rate limit (60/min), levar >1h, e custar igual ao batch.

3.2 Assíncrono (batch endpoint)

Use quando:

3.3 Padrão híbrido: signup com fallback

Pra signup em alto volume, padrão eficaz:

  1. Sintaxe + DNS rápido no client/servidor (sem custo, <100ms).
  2. Single sync se DNS passou (1 crédito, <3s) — bloqueia signup se undeliverable.
  3. Se vier risky ou unknown, libera signup mas marca lead como "pendente verificação" e revalida em batch noturno.

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. Keys têm prefixo de ambiente — ec_test_ pra sandbox (não consome créditos reais, retorna respostas fixturadas), ec_live_ pra produção.

# 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 dashboard /settings/api, gera nova key (mantém antiga ativa).
  2. Deploy do app lendo a nova key do env var.
  3. Confirma uso da nova key via GET /v1/credits ou logs (cada key tem last_used_at e requests_24h).
  4. Revoga a antiga no dashboard.

Recomendação: mantenha SEMPRE 2 keys ativas em produção. Keys são gratuitas e ilimitadas — o custo extra é zero, e qualquer compromisso futuro tem janela de recuperação imediata.

5. Rate limiting (60/min, 1000/h) e como respeitar

Limites padrão: 60 requests/minuto e 1000 requests/hora por API key (não por IP). Token bucket: capacidade 60, replenish 1 token/segundo. Pra limites maiores, contato comercial.

5.1 Headers de rate limit

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

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1716221600    # epoch seconds quando o bucket replenisha

5.2 Quando você bate o limite (HTTP 429)

HTTP/1.1 429 Too Many Requests
Retry-After: 13

{
  "error": "rate_limit_exceeded",
  "message": "60 requests per minute exceeded",
  "retry_after_seconds": 13
}

Respeite Retry-After. Tentar antes só piora — você queima outro slot do bucket sem ganhar nada.

5.3 Client-side throttling

Implementação típica em Node (bottleneck):

import Bottleneck from 'bottleneck';

const limiter = new Bottleneck({
  reservoir: 60,                  // 60 tokens
  reservoirRefreshAmount: 60,
  reservoirRefreshInterval: 60_000, // refresh a cada 60s
  minTime: 50,                    // mínimo 50ms entre requests
});

const validate = (email) => limiter.schedule(() =>
  fetch(BASE_URL + '/validate/single', {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_KEY}` },
    body: JSON.stringify({ email }),
  })
);

5.4 Estratégia: batch > single em loop

Pra qualquer volume >100, batch é dramaticamente melhor. 1 batch de 1000 emails = 1 request (não conta nada significativo no rate limit). 1000 single = 1000 requests = 17 minutos só esperando o bucket replenishar.

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:

400

Bad Request

Payload inválido (JSON malformado, campo faltando, email malformado). NÃO retry — corrija o request.

401 / 403

Auth

Key inválida, revogada ou sem permissão. NÃO retry — verifique a key.

404

Not Found

job_id inexistente ou key não tem acesso ao recurso. NÃO retry.

429

Rate Limit

Bucket esgotado. Retry depois de Retry-After segundos.

500 / 502 / 503 / 504

Server

Erro nosso ou indisponibilidade temporária. Retry com exponential backoff.

6.2 Exponential backoff pra 5xx + 429

async function callWithRetry(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: respeita Retry-After
    if (r.status === 429) {
      const wait = Number(r.headers.get('retry-after') ?? 1) * 1000;
      await new Promise((res) => setTimeout(res, wait));
      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 Idempotência em POSTs

Se você retry um POST que talvez já tenha completado no servidor (resposta perdida na rede), você pode submeter o mesmo batch duas vezes. Solução: header Idempotency-Key com UUID gerado client-side. O EC dedupa por essa key durante 24h — segundo request com mesma key retorna o resultado do primeiro.

curl -X POST .../validate/batch \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Authorization: Bearer ec_live_xxx" \
  -d '{"emails": [...]}'

7. Webhooks: assinatura HMAC e idempotência

7.1 Como funciona

Você registra webhook_url no momento do batch (ou globalmente em /settings/api). Quando o job termina, o EC manda POST com o resultado. Retry policy do EC: 5 tentativas com backoff exponencial (1min, 5min, 30min, 2h, 6h).

7.2 Validação de assinatura (HMAC-SHA256)

Sempre valide a assinatura — sem isso, qualquer pessoa que descobrir sua URL pode injetar dados falsos. O EC envia:

POST /seu/webhook HTTP/1.1
X-EC-Signature: sha256=a1b2c3d4...
X-EC-Timestamp: 1716221600
X-EC-Event-Id: evt_01HZK8WX3Q
Content-Type: application/json

{"job_id":"job_01HZK8WX3Q","status":"completed","results":[...]}

Verificação correta (Node):

import crypto from 'node:crypto';

function verifyWebhook(rawBody, headers, secret) {
  const signature = headers['x-ec-signature']?.replace('sha256=', '');
  const timestamp = headers['x-ec-timestamp'];

  // rejeita replays >5min
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return false;
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + rawBody)
    .digest('hex');

  // timing-safe compare — NUNCA use === ou ==
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Pegadinha crítica: assine o raw body, não o JSON parseado. Frameworks como Express por padrão parseiam o body antes do handler — você perde a string original. Configure express.raw() ou capture o stream antes do parse.

7.3 Idempotência no consumer

O EC pode entregar o mesmo evento mais de uma vez (retry após timeout, queue duplicada). Você deve idempotar no seu lado:

  1. Armazena X-EC-Event-Id em tabela de eventos processados.
  2. Antes de processar, checa se já viu esse event_id — se sim, retorna 200 OK sem reprocessar.
  3. Use INSERT ... ON CONFLICT DO NOTHING (Postgres) ou SETNX (Redis) — atomicamente, pra evitar race em workers concorrentes.

8. Drop-in compatível com mails.so

Se você já integra mails.so, migração tem 3 passos. Em testes feitos em apps em produção: 30 minutos de trabalho, zero mudança na UX do usuário final. Veja o comparativo técnico completo com tabela de feature matching.

8.1 Mapping de endpoints

mails.soEmailChecker
GET /v1/check?email=...POST /v1/validate/single
POST /v1/batchPOST /v1/validate/batch
GET /v1/batch/{id}GET /v1/validate/batch/{id}
Header x-mails-api-keyHeader Authorization: Bearer ...

8.2 Mapping de response fields

mails.soEmailCheckerNota
data.resultresultEnum equivalente: deliverable / undeliverable / risky / unknown
data.isv_formatformat_validBoolean — sintaxe RFC 5322
data.isv_mxmx_validBoolean — MX record resolve
data.isv_roleis_roleBoolean — info@, contato@, etc.

8.3 Diff típico de migração

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

  const r = await fetch(`${BASE}/validate/single`, {
    method: 'POST',
-   headers: { 'x-mails-api-key': KEY },
+   headers: { 'Authorization': `Bearer ${KEY}` },
    body: JSON.stringify({ email }),
  });
  const json = await r.json();
- if (json.data.result === 'deliverable') { /* ... */ }
+ if (json.result === 'deliverable') { /* ... */ }

9. Custos: créditos vs requests e monitoramento

9.1 O que conta como crédito

9.2 Saldo e monitoramento

Checa saldo a qualquer momento:

curl https://app.emailchecker.email/api/v1/credits \
  -H "Authorization: Bearer ec_live_xxx"

{
  "balance": 12450,
  "lifetime_consumed": 87530,
  "last_purchase_at": "2026-04-15T10:32:00Z",
  "burn_rate_30d_avg": 380   // créditos/dia média 30d
}

Use balance / burn_rate_30d_avgpra calcular runway em dias. Padrão recomendado: alerta quando runway < 7 dias.

9.3 Estratégias de redução de custo

9.4 Observabilidade em produção

10. Perguntas frequentes

Qual a diferença entre API REST e SDK de validação?
A API REST é o contrato HTTP — endpoints, payloads, status codes. Funciona em qualquer linguagem com qualquer cliente HTTP (fetch, curl, requests, Guzzle). Um SDK é um wrapper opcional em uma linguagem específica que esconde detalhes (auth, retry, parsing). O EmailChecker prioriza API REST limpa — você não fica refém de SDK proprietário. Code examples em Node, Python, PHP e Go disponíveis em /docs-api/exemplos.
Quando devo usar o endpoint single vs batch?
Single (POST /validate/single) é síncrono, retorna em 1–3 segundos. Use em signup forms, validação real-time, baixo volume (até ~100/h). Batch (POST /validate/batch) é assíncrono, retorna job_id imediato e processa em background. Use em bulk import, CRM cleanup, listas grandes. Pra alto volume nunca itere single em loop — é mais lento e custa rate limit; use batch.
Como rotaciono API keys sem downtime?
Padrão zero-downtime: (1) gere nova key no dashboard /settings/api, (2) deploy app lendo NOVA_KEY do env, (3) confirme tráfego usando nova key via logs (/v1/credits mostra last_used_at por key), (4) revogue antiga. Mantenha sempre 2 keys ativas pra não ter janela de falha — keys são gratuitas e ilimitadas.
O que fazer quando recebo HTTP 429 (rate limited)?
Respeite o header Retry-After (segundos) antes de tentar de novo. Implementa client-side throttling: bottleneck no Node, aiohttp_retry no Python. Pra alto volume crônico, migra single → batch (1 request submete 1000 emails). Pra pico esporádico, exponential backoff: 1s, 2s, 4s, 8s, 16s — max 5 retries. Acima disso, falha pro caller.
Como verifico assinatura de webhook?
O EC envia header X-EC-Signature = HMAC-SHA256(webhook_secret, raw_body). No handler: compute o HMAC do body cru (NÃO do JSON parseado) com o secret armazenado, e use comparação timing-safe (crypto.timingSafeEqual no Node, hmac.compare_digest no Python). NUNCA use === ou == — vulnerável a timing attacks. Se assinatura não bater, retorna 401 e ignora.
Migrar de mails.so pro EmailChecker dá muito trabalho?
Não. O shape do JSON é quase idêntico — single endpoint retorna result/score/reason equivalentes. Migração típica: 30 minutos. Troca BASE_URL pra app.emailchecker.email/api/v1, ajusta header Authorization (Bearer em vez de x-mails-api-key) e mapeia 2-3 nomes de campos. Veja /comparativo/mails-so-vs-emailchecker pra tabela de mapeamento completa.
O que conta como crédito consumido?
Apenas emails efetivamente validados. Batch de 1000 emails consome 1000 créditos. Requests rejeitadas com 4xx (payload inválido, auth, rate limit) NÃO consomem crédito. Em batch, se 200 emails vierem inválidos por sintaxe (rejeitados antes do SMTP), são cobrados como result=undeliverable normal — porque o trabalho de parse + DNS aconteceu. Erros 5xx do nosso lado também não cobram.
Como monitoro saúde da integração em produção?
Track 4 métricas: (1) error rate por status code — 4xx persistente indica bug do seu lado, 5xx indica nosso, (2) latência p50/p95/p99 — single deve ficar <3s, batch submit <500ms, (3) saldo de créditos via GET /v1/credits, com alerta abaixo de 7 dias de runway, (4) backlog de webhooks não processados. Logue request_id (header X-Request-ID) em todas as chamadas pra correlação com suporte.

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.