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:
- Signup forms em tempo real — usuário digita email, espera 1–3s, sabe na hora se é válido.
- Validação eager em fluxos curtos — checkout, password reset, convite por email.
- Volume baixo — até ~100 emails/hora, não vale o overhead de batch+webhook.
- UX onde latência < 3s é tolerável — feedback imediato vale a espera.
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:
- Bulk import de CRM — limpeza periódica de listas, sync com SaaS.
- Validação pré-campanha — checar lista inteira antes do disparo.
- Lazy validation — usuário cadastra, valida em background, marca conta posteriormente.
- Volume alto — qualquer lista >100 emails é mais barato em batch.
3.3 Padrão híbrido: signup com fallback
Pra signup em alto volume, padrão eficaz:
- Sintaxe + DNS rápido no client/servidor (sem custo, <100ms).
- Single sync se DNS passou (1 crédito, <3s) — bloqueia signup se
undeliverable. - Se vier
riskyouunknown, 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:
- Local dev:
.env.localem.gitignore+ exemplo em.env.example. - Cloud (Vercel/Netlify/AWS): env vars no painel, scoped por ambiente.
- Self-hosted: AWS Secrets Manager, HashiCorp Vault, Doppler. NUNCA env var em image Docker pública.
- Frontend (browser): NUNCA expor key no client — proxy pelo seu backend.
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.
- No dashboard
/settings/api, gera nova key (mantém antiga ativa). - Deploy do app lendo a nova key do env var.
- Confirma uso da nova key via
GET /v1/creditsou logs (cada key temlast_used_aterequests_24h). - 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 replenisha5.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:
Bad Request
Payload inválido (JSON malformado, campo faltando, email malformado). NÃO retry — corrija o request.
Auth
Key inválida, revogada ou sem permissão. NÃO retry — verifique a key.
Not Found
job_id inexistente ou key não tem acesso ao recurso. NÃO retry.
Rate Limit
Bucket esgotado. Retry depois de Retry-After segundos.
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:
- Armazena
X-EC-Event-Idem tabela de eventos processados. - Antes de processar, checa se já viu esse
event_id— se sim, retorna 200 OK sem reprocessar. - Use
INSERT ... ON CONFLICT DO NOTHING(Postgres) ouSETNX(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.so | EmailChecker |
|---|---|
GET /v1/check?email=... | POST /v1/validate/single |
POST /v1/batch | POST /v1/validate/batch |
GET /v1/batch/{id} | GET /v1/validate/batch/{id} |
Header x-mails-api-key | Header Authorization: Bearer ... |
8.2 Mapping de response fields
| mails.so | EmailChecker | Nota |
|---|---|---|
data.result | result | Enum equivalente: deliverable / undeliverable / risky / unknown |
data.isv_format | format_valid | Boolean — sintaxe RFC 5322 |
data.isv_mx | mx_valid | Boolean — MX record resolve |
data.isv_role | is_role | Boolean — 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
- 1 crédito = 1 email validado. Não importa se foi via single ou batch.
- Emails rejeitados antes do processamento (4xx por payload inválido ou auth) não consomem.
- Emails que rodam o pipeline mas vêm
undeliverablepor sintaxe/DNS consomem — porque parse + DNS lookup aconteceu. - Erros 5xx do nosso lado não consomem — você só paga por trabalho real entregue.
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
- Cache local em formulários de signup — se você já validou
joao@x.comhoje, não revalida em retry de envio. - Pré-filtro de sintaxe client-side (regex básica) — não chama API pra emails obviamente quebrados.
- Dedupe antes do batch — listas com 30% de duplicatas custam 30% a mais sem ganho. O EC NÃO dedupa automático (decisão deliberada — você pode querer revalidar o mesmo email em momentos diferentes).
- Sample em listas suspeitas — antes de validar lista comprada de 100k, valide 1000 random pra ver bounce projection.
9.4 Observabilidade em produção
- Request ID: header
X-Request-IDem toda resposta — loge sempre, suporte pede pra debug. - Métricas RED: Rate (req/s), Errors (% 4xx + 5xx), Duration (p50, p95, p99) — Prometheus / Datadog / similar.
- Alertas: 5xx >1% por 5min, p95 >5s por 5min, saldo <7 dias.
- Webhook backlog: monitore tamanho da fila interna de eventos recebidos vs processados — se cresce, seu consumer não tá acompanhando.