Validação de e-mails
Confira se um endereço recebe e-mail antes de enviar: um por vez, em lote ou nos seus contatos, com créditos mensais do plano.
A Postal confere a sintaxe, o domínio e os servidores de e-mail de cada endereço, aponta endereços descartáveis, de função (como contato@) e com erro de digitação, tenta confirmar a caixa de entrada com o servidor do destinatário e cruza o resultado com o histórico de envios do seu time (bounces, reclamações e supressões). Há três jeitos de usar: um endereço por vez, uma lista em lote (um CSV, um segmento ou todos os contatos) e a prévia da saúde de uma lista. O último resultado de cada endereço vira o email_status do contato e pode filtrar a lista de contatos e segmentos dinâmicos. Todos os endpoints usam uma chave de API de acesso total e precisam de um plano que inclua a validação.
#Créditos
Cada endereço verificado usa 1 crédito. O plano dá créditos por mês (Grátis: 100, Profissional: 5.000, Escala: 50.000, Empresarial: sem limite) que renovam no dia 1º de cada mês (UTC); um plano com 0 não inclui a validação (403 plan_limit_reached). Os pacotes do Adicional de validação, comprados no painel (ou concedidos pela Postal), somam-se aos créditos do plano e aparecem em credits.grants. Cada pacote tem a validade definida para ele, em dias (validity_days, nos pacotes à venda), e alguns não têm validade: nesses, validity_days é 0 e, em credits.grants, expires_at é null. Os créditos do mês são usados primeiro; depois, os pacotes que vencem antes, e por último os que não vencem.
| O que | Custa |
|---|---|
| Um endereço que já tem resultado guardado de menos de 90 dias | Nada: o resultado é reaproveitado (cached: true ou already_verified), recalculado com o histórico atual do time. Com recheck_stale, os mais antigos são verificados de novo. |
Endereço mal formado ou falha de DNS (unknown) | Nada. Resultados unknown não ficam guardados, então a próxima verificação tenta de novo. |
| Prévia da saúde da lista | Grátis até 1.000 endereços amostrados por mês; acima disso, créditos normais. |
| Os demais | 1 crédito por endereço. |
Sem créditos, a resposta é 429 com verification_credits_exhausted. Os endpoints devolvem o saldo em credits: used e limit (créditos mensais), monthly_remaining, grant_credits (dos pacotes), remaining (a soma; null se for ilimitado), resets_at e free_preview_remaining.
#Entendendo o resultado
Todo resultado tem quatro leituras do mesmo endereço, da mais simples à mais detalhada:
| Campo | Valores |
|---|---|
status | valid (entregável, com risco baixo ou médio), invalid (não entregável ou que não deve receber), risky (catch_all, descartável, conta de função sem engajamento ou risco alto) e unknown (as verificações não terminaram). |
result | deliverable, undeliverable, do_not_send (reclamou ou está suprimido), catch_all (o domínio aceita qualquer destinatário) e unknown. |
risk | low, medium, high ou unknown. O engajamento do seu time ajusta o risco: quem recebeu, abriu ou clicou antes fica com risco menor; uma conta de função ou um erro de digitação que nunca recebeu nada sobe um nível. |
reason / reasons | O motivo principal (ok nos válidos) e todos os códigos que se aplicam, em ordem de importância. |
Código (reason) | Quer dizer |
|---|---|
complained, suppressed | O endereço reclamou de spam ou está na lista de supressão do time (do_not_send). |
previously_bounced | Já deu bounce permanente num envio do time. |
invalid_syntax, domain_not_found, no_mx | Mal formado, domínio que não existe e domínio sem servidor de e-mail. |
mailbox_not_found | O servidor do destinatário disse que a caixa não existe. |
dns_timeout, mailbox_unverifiable | Não foi possível concluir (unknown): o DNS falhou ou o provedor não permite confirmar a caixa. |
catch_all | O domínio aceita qualquer destinatário, então não dá para saber se a caixa existe. |
mailbox_full, disposable, typo_suspected, role_account | Sinais de risco: caixa cheia, endereço descartável, provável erro de digitação (veja suggestion) e conta de função (contato@, vendas@). |
free_provider | Informativo: provedor gratuito. Sozinho, nunca explica um status. |
has_engagement, no_history | O histórico do time: já enviou e o endereço recebeu, abriu ou clicou, ou nunca enviou nada para ele. |
checks traz as verificações como booleanos: syntax, mx (o domínio aceita e-mail; null se não deu para saber), smtp (a caixa existe; null se não confirmada), disposable, role, free e suppressed (reclamou, está suprimido ou já deu bounce). mailbox e smtp_code são o resultado da conversa com o servidor do destinatário (deliverable, undeliverable, catch_all, full ou unknown), ou null se a caixa não foi consultada. suggestion é a correção de um provável erro de digitação, como gmail.com para gmial.com.
#Validar um endereço
POST/email-verifications/check
Verifica um endereço na hora. Um resultado guardado e recente (menos de 90 dias) volta de graça, com cached: true; senão a verificação usa um crédito (os do mês primeiro, depois os pacotes). Endereços mal formados e falhas de DNS não gastam crédito. Para listas, use a validação em lote. Limite: 30 verificações por minuto por time (429 rate_limit_exceeded).
Corpo da requisição
emailstringobrigatórioO endereço, até 320 caracteres.
Erros
- 422
validation_errorAlgum campo é inválido. A mensagem diz qual. - 403
plan_limit_reachedO plano não inclui a validação de e-mails. - 429
verification_credits_exhaustedOs créditos acabaram. Os do plano renovam no dia 1º (UTC); o Adicional de validação libera novas validações na hora. - 429
rate_limit_exceededMais de 30 verificações em um minuto.
cURL
curl -X POST https://api.postal.so/email-verifications/check \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]"
}'
Node.js
const response = await fetch('https://api.postal.so/email-verifications/check', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: '[email protected]',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/email-verifications/check",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"email": "[email protected]",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('email-verifications/check', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'email' => '[email protected]',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email_verification",
"email": "[email protected]",
"normalized": "[email protected]",
"status": "valid",
"result": "deliverable",
"risk": "low",
"reason": "ok",
"reasons": ["has_engagement"],
"checks": {
"syntax": true,
"mx": true,
"smtp": true,
"disposable": false,
"role": false,
"free": false,
"suppressed": false
},
"mailbox": "deliverable",
"smtp_code": 250,
"suggestion": null,
"domain": "exemplo.com.br",
"cached": false,
"verified_at": "2026-10-24T14:00:00Z",
"credits": {
"used": 1240,
"limit": 5000,
"remaining": 4760,
"monthly_remaining": 3760,
"grant_credits": 1000,
"grants": [
{
"id": "019a1f00-4d5e-7f60-9a70-7c8d9e0f1a2b",
"source": "purchase",
"name": "Pacote de 500 créditos",
"credits": 500,
"remaining": 500,
"expires_at": "2027-10-20T12:00:00Z"
},
{
"id": "019a1f00-4d5f-7f61-8b71-8d9e0f1a2b3c",
"source": "purchase",
"name": "Pacote de 500 créditos sem validade",
"credits": 500,
"remaining": 500,
"expires_at": null
}
],
"resets_at": "2026-11-01T00:00:00Z",
"free_preview_remaining": 1000
}
}
#Estimar o custo de uma lista
POST/email-verifications/estimate
Lê a origem e diz quanto custaria verificá-la, sem verificar nem gastar nada: quantos endereços há (total), quantos são únicos (unique), quantos já têm resultado guardado (already_verified) e quantos seriam verificados (to_check, que é também credits_needed). enough_credits diz se o saldo cobre. preview mostra o tamanho da amostra da prévia e quanto ela custaria; max_addresses é o máximo por validação (200.000).
Corpo da requisição (JSON ou multipart/form-data)
sourcestringobrigatórioDe onde vêm os endereços:csv(um arquivo),segment(um segmento) oucontacts(todos os contatos).filearquivoSó comsource=csv: o CSV, até 20 MB, com uma linha de cabeçalho, enviado comomultipart/form-data. Não vale para as outras origens.email_columnstringSó comsource=csv: a coluna do cabeçalho com os e-mails. Se faltar, a Postal tenta adivinhar.segment_idstringObrigatório comsource=segment.recheck_stalebooleanVerifica de novo os endereços cujo resultado guardado tem mais de 90 dias. Padrão:false.
Erros
- 422
validation_errorOrigem inválida, arquivo ausente, grande demais ou que não é CSV, coluna que não está no cabeçalho, lista sem endereços ou com mais de 200.000. - 403
plan_limit_reachedO plano não inclui a validação de e-mails.
cURL
curl -X POST https://api.postal.so/email-verifications/estimate \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "segment",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a"
}'
Node.js
const response = await fetch('https://api.postal.so/email-verifications/estimate', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
source: 'segment',
segment_id: '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/email-verifications/estimate",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"source": "segment",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('email-verifications/estimate', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'source' => 'segment',
'segment_id' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email_verification_estimate",
"source": "segment",
"email_column": null,
"columns": null,
"total": 5200,
"unique": 5200,
"already_verified": 800,
"to_check": 4400,
"credits_needed": 4400,
"enough_credits": true,
"max_addresses": 200000,
"preview": {
"sample_size": 260,
"credits_needed": 0
},
"credits": {
"used": 1240,
"limit": 5000,
"remaining": 4760,
"monthly_remaining": 3760,
"grant_credits": 1000,
"grants": [
{
"id": "019a1f00-4d5e-7f60-9a70-7c8d9e0f1a2b",
"source": "purchase",
"name": "Pacote de 500 créditos",
"credits": 500,
"remaining": 500,
"expires_at": "2027-10-20T12:00:00Z"
},
{
"id": "019a1f00-4d5f-7f61-8b71-8d9e0f1a2b3c",
"source": "purchase",
"name": "Pacote de 500 créditos sem validade",
"credits": 500,
"remaining": 500,
"expires_at": null
}
],
"resets_at": "2026-11-01T00:00:00Z",
"free_preview_remaining": 1000
}
}
cURL
curl -X POST https://api.postal.so/email-verifications/estimate \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-F "[email protected]" \
-F "source=csv" \
-F "email_column=email"
Node.js
import { readFile } from 'node:fs/promises';
const form = new FormData();
form.append('file', new Blob([await readFile('clientes.csv')]), 'clientes.csv');
form.append('source', 'csv');
form.append('email_column', 'email');
const response = await fetch('https://api.postal.so/email-verifications/estimate', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.POSTAL_API_KEY}` },
body: form,
});
const verification = await response.json();
Python
import os
import requests
with open("clientes.csv", "rb") as csv_file:
response = requests.post(
"https://api.postal.so/email-verifications/estimate",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
files={"file": ("clientes.csv", csv_file, "text/csv")},
data={
"source": "csv",
"email_column": "email",
},
timeout=60,
)
verification = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('email-verifications/estimate', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'multipart' => [
['name' => 'file', 'contents' => fopen('clientes.csv', 'r'), 'filename' => 'clientes.csv'],
['name' => 'source', 'contents' => 'csv'],
['name' => 'email_column', 'contents' => 'email'],
],
]);
$verification = json_decode($response->getBody(), true);
#Prévia da saúde de uma lista
POST/email-verifications/preview
Verifica uma amostra aleatória da origem (5% da lista, de 50 a 500 endereços) para você ver a proporção de válidos, inválidos e arriscados antes de gastar créditos com tudo. A amostra é grátis até 1.000 endereços por mês; acima disso, usa créditos normais. O corpo é o mesmo da estimativa. Devolve a validação (preview: true), que roda em segundo plano: acompanhe em GET /email-verifications/{id}. Uma prévia não pode ser aplicada aos contatos.
Corpo da requisição (JSON ou multipart/form-data)
sourcestringobrigatórioDe onde vêm os endereços:csv(um arquivo),segment(um segmento) oucontacts(todos os contatos).filearquivoSó comsource=csv: o CSV, até 20 MB, com uma linha de cabeçalho, enviado comomultipart/form-data. Não vale para as outras origens.email_columnstringSó comsource=csv: a coluna do cabeçalho com os e-mails. Se faltar, a Postal tenta adivinhar.segment_idstringObrigatório comsource=segment.recheck_stalebooleanVerifica de novo os endereços cujo resultado guardado tem mais de 90 dias. Padrão:false.
Erros
- 422
validation_errorOrigem inválida, arquivo ausente, grande demais ou que não é CSV, coluna que não está no cabeçalho, lista sem endereços ou com mais de 200.000. - 403
plan_limit_reachedO plano não inclui a validação de e-mails. - 429
verification_credits_exhaustedOs créditos acabaram. Os do plano renovam no dia 1º (UTC); o Adicional de validação libera novas validações na hora. - 429
rate_limit_exceededJá há 2 validações em andamento. - 404
not_foundO segmento não existe.
cURL
curl -X POST https://api.postal.so/email-verifications/preview \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "segment",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a"
}'
Node.js
const response = await fetch('https://api.postal.so/email-verifications/preview', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
source: 'segment',
segment_id: '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/email-verifications/preview",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"source": "segment",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('email-verifications/preview', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'source' => 'segment',
'segment_id' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email_verification_job",
"id": "019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"source": "segment",
"preview": true,
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"filename": null,
"email_column": null,
"recheck_stale": false,
"status": "pending",
"total": null,
"processed": 0,
"counts": {
"valid": 0,
"invalid": 0,
"risky": 0,
"unknown": 0
},
"rates": {
"valid": null,
"invalid": null,
"risky": null,
"unknown": null
},
"reasons": {},
"already_verified": 0,
"checked": 0,
"credits_used": 0,
"free_checks": 0,
"credits_exhausted": false,
"population": 5200,
"estimated_credits": 0,
"applied": null,
"error": null,
"created_at": "2026-10-24T14:00:00.000000Z",
"started_at": null,
"completed_at": null,
"results_expire_at": null,
"results_filename": null,
"download_url": null
}
#Validar uma lista em lote
POST/email-verifications
Começa a validar todos os endereços da origem, em segundo plano. Antes de começar, a Postal confere o saldo contra o to_check da estimativa (429 verification_credits_exhausted se não cobrir). Cada time pode ter até 2 validações em andamento, e cada uma até 200.000 endereços. Acompanhe o andamento em GET /email-verifications/{id}; no fim, leia os resultados ou baixe o CSV em download_url (um link assinado, válido por 1 hora). Os resultados ficam 7 dias; a validação em si, 30. Com source=contacts ou segment, cada resultado traz o contact_id, e o resultado vira o email_status do contato.
Corpo da requisição (JSON ou multipart/form-data)
sourcestringobrigatórioDe onde vêm os endereços:csv(um arquivo),segment(um segmento) oucontacts(todos os contatos).filearquivoSó comsource=csv: o CSV, até 20 MB, com uma linha de cabeçalho, enviado comomultipart/form-data. Não vale para as outras origens.email_columnstringSó comsource=csv: a coluna do cabeçalho com os e-mails. Se faltar, a Postal tenta adivinhar.segment_idstringObrigatório comsource=segment.recheck_stalebooleanVerifica de novo os endereços cujo resultado guardado tem mais de 90 dias. Padrão:false.
Erros
- 422
validation_errorOrigem inválida, arquivo ausente, grande demais ou que não é CSV, coluna que não está no cabeçalho, lista sem endereços ou com mais de 200.000. - 403
plan_limit_reachedO plano não inclui a validação de e-mails. - 429
verification_credits_exhaustedOs créditos acabaram. Os do plano renovam no dia 1º (UTC); o Adicional de validação libera novas validações na hora. - 429
rate_limit_exceededJá há 2 validações em andamento. - 404
not_foundO segmento não existe.
cURL
curl -X POST https://api.postal.so/email-verifications \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": "segment",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a"
}'
Node.js
const response = await fetch('https://api.postal.so/email-verifications', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
source: 'segment',
segment_id: '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/email-verifications",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"source": "segment",
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('email-verifications', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'source' => 'segment',
'segment_id' => '019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email_verification_job",
"id": "019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"source": "segment",
"preview": false,
"segment_id": "019a1d90-3b4c-7d5e-9f6a-7b8c9d0e1f2a",
"filename": null,
"email_column": null,
"recheck_stale": false,
"status": "pending",
"total": null,
"processed": 0,
"counts": {
"valid": 0,
"invalid": 0,
"risky": 0,
"unknown": 0
},
"rates": {
"valid": null,
"invalid": null,
"risky": null,
"unknown": null
},
"reasons": {},
"already_verified": 0,
"checked": 0,
"credits_used": 0,
"free_checks": 0,
"credits_exhausted": false,
"population": 5200,
"estimated_credits": 4400,
"applied": null,
"error": null,
"created_at": "2026-10-24T14:00:00.000000Z",
"started_at": null,
"completed_at": null,
"results_expire_at": null,
"results_filename": null,
"download_url": null
}
cURL
curl -X POST https://api.postal.so/email-verifications \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-F "[email protected]" \
-F "source=csv" \
-F "email_column=email"
Node.js
import { readFile } from 'node:fs/promises';
const form = new FormData();
form.append('file', new Blob([await readFile('clientes.csv')]), 'clientes.csv');
form.append('source', 'csv');
form.append('email_column', 'email');
const response = await fetch('https://api.postal.so/email-verifications', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.POSTAL_API_KEY}` },
body: form,
});
const verification = await response.json();
Python
import os
import requests
with open("clientes.csv", "rb") as csv_file:
response = requests.post(
"https://api.postal.so/email-verifications",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
files={"file": ("clientes.csv", csv_file, "text/csv")},
data={
"source": "csv",
"email_column": "email",
},
timeout=60,
)
verification = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('email-verifications', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'multipart' => [
['name' => 'file', 'contents' => fopen('clientes.csv', 'r'), 'filename' => 'clientes.csv'],
['name' => 'source', 'contents' => 'csv'],
['name' => 'email_column', 'contents' => 'email'],
],
]);
$verification = json_decode($response->getBody(), true);
#Listar validações
GET/email-verifications
As validações em lote e as prévias do time, da mais recente para a mais antiga.
Parâmetros de consulta
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.previewbooleantruesó as prévias;falsesó as validações completas.
cURL
curl https://api.postal.so/email-verifications \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/email-verifications', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/email-verifications",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('email-verifications', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": false,
"data": [
{
"object": "email_verification_job",
"id": "019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"source": "csv",
"preview": false,
"segment_id": null,
"filename": "clientes.csv",
"email_column": "email",
"recheck_stale": false,
"status": "completed",
"total": 5200,
"processed": 5200,
"counts": {
"valid": 4410,
"invalid": 390,
"risky": 310,
"unknown": 90
},
"rates": {
"valid": 0.8481,
"invalid": 0.075,
"risky": 0.0596,
"unknown": 0.0173
},
"reasons": {
"mailbox_not_found": 210,
"invalid_syntax": 120,
"disposable": 140,
"role_account": 120,
"catch_all": 50
},
"already_verified": 800,
"checked": 4400,
"credits_used": 4390,
"free_checks": 0,
"credits_exhausted": false,
"population": 5200,
"estimated_credits": 4400,
"applied": null,
"error": null,
"created_at": "2026-10-24T14:00:00.000000Z",
"started_at": "2026-10-24T14:00:02.000000Z",
"completed_at": "2026-10-24T14:03:40.000000Z",
"results_expire_at": "2026-10-31T14:03:40.000000Z",
"results_filename": "email-verification-customers.csv",
"download_url": "https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/download?expires=1792850620&signature=…"
}
]
}
#Obter uma validação
GET/email-verifications/{id}
status: pending, processing, completed, canceled ou failed (com error). processed de total mostra o andamento; counts e rates dão a divisão por status, e reasons os motivos mais comuns. already_verified são os endereços reaproveitados, checked os verificados agora, credits_used e free_checks o que foi cobrado e o que foi grátis, e credits_exhausted avisa que a validação parou por falta de créditos. Aqui também vem contacts: quantos contatos o aplicar alteraria, para os resultados invalid e risky. Uma validação sem progresso por 30 minutos (por exemplo, um servidor que caiu) é retomada sozinha, até 3 vezes seguidas.
Parâmetros de caminho
idstringobrigatórioID da validação.
Erros
- 404
not_foundA validação não existe ou é de outro time.
cURL
curl https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email_verification_job",
"id": "019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"source": "csv",
"preview": false,
"segment_id": null,
"filename": "clientes.csv",
"email_column": "email",
"recheck_stale": false,
"status": "completed",
"total": 5200,
"processed": 5200,
"counts": {
"valid": 4410,
"invalid": 390,
"risky": 310,
"unknown": 90
},
"rates": {
"valid": 0.8481,
"invalid": 0.075,
"risky": 0.0596,
"unknown": 0.0173
},
"reasons": {
"mailbox_not_found": 210,
"invalid_syntax": 120,
"disposable": 140,
"role_account": 120,
"catch_all": 50
},
"already_verified": 800,
"checked": 4400,
"credits_used": 4390,
"free_checks": 0,
"credits_exhausted": false,
"population": 5200,
"estimated_credits": 4400,
"applied": null,
"error": null,
"created_at": "2026-10-24T14:00:00.000000Z",
"started_at": "2026-10-24T14:00:02.000000Z",
"completed_at": "2026-10-24T14:03:40.000000Z",
"results_expire_at": "2026-10-31T14:03:40.000000Z",
"results_filename": "email-verification-customers.csv",
"download_url": "https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/download?expires=1792850620&signature=…",
"contacts": {
"invalid": 360,
"risky": 280
}
}
#Cancelar uma validação
POST/email-verifications/{id}/cancel
Para uma validação que ainda está na fila ou rodando. O que já foi verificado fica nos resultados (e foi cobrado); o resto não é verificado. Uma validação que ainda não começou apaga o arquivo enviado.
Parâmetros de caminho
idstringobrigatórioID da validação.
Erros
- 422
validation_errorA validação não está mais em andamento. - 404
not_foundA validação não existe ou é de outro time.
cURL
curl -X POST https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/cancel \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/cancel', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/cancel",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/cancel', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email_verification_job",
"id": "019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"source": "csv",
"preview": false,
"segment_id": null,
"filename": "clientes.csv",
"email_column": "email",
"recheck_stale": false,
"status": "canceled",
"total": 5200,
"processed": 1800,
"counts": {
"valid": 4410,
"invalid": 390,
"risky": 310,
"unknown": 90
},
"rates": {
"valid": 0.8481,
"invalid": 0.075,
"risky": 0.0596,
"unknown": 0.0173
},
"reasons": {
"mailbox_not_found": 210,
"invalid_syntax": 120,
"disposable": 140,
"role_account": 120,
"catch_all": 50
},
"already_verified": 800,
"checked": 4400,
"credits_used": 4390,
"free_checks": 0,
"credits_exhausted": false,
"population": 5200,
"estimated_credits": 4400,
"applied": null,
"error": null,
"created_at": "2026-10-24T14:00:00.000000Z",
"started_at": "2026-10-24T14:00:02.000000Z",
"completed_at": "2026-10-24T14:01:10.000000Z",
"results_expire_at": "2026-10-31T14:03:40.000000Z",
"results_filename": "email-verification-customers.csv",
"download_url": "https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/download?expires=1792850620&signature=…"
}
#Resultados de uma validação
GET/email-verifications/{id}/results
Um resultado por endereço, na ordem do arquivo. Cada um traz as leituras de Entendendo o resultado, mais line (a linha do CSV, contando o cabeçalho como 1), already_verified e o contact_id do contato correspondente, quando há. Os resultados são apagados 7 dias depois do fim da validação.
Parâmetros de caminho
idstringobrigatórioID da validação.
Parâmetros de consulta
limitintegerDe 1 a 100. Padrão: 20.afterstringCursor da próxima página.beforestringCursor da página anterior.statusstringUm ou mais status, separados por vírgula:valid,invalid,risky,unknown.
Erros
- 422
validation_errorstatuscom um valor que não existe. - 404
not_foundA validação não existe ou é de outro time.
cURL
curl 'https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/results?status=invalid,risky&limit=50' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/results?status=invalid,risky&limit=50', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/results",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"status": "invalid,risky",
"limit": 50,
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/results', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'status' => 'invalid,risky',
'limit' => 50,
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "list",
"has_more": true,
"data": [
{
"object": "email_verification_result",
"id": "019a1f10-2b3c-7d4e-8f50-6b7c8d9e0f1a",
"email": "[email protected]",
"normalized": "[email protected]",
"status": "invalid",
"result": "undeliverable",
"risk": "high",
"reason": "mailbox_not_found",
"reasons": ["mailbox_not_found"],
"checks": {
"syntax": true,
"mx": true,
"smtp": false,
"disposable": false,
"role": false,
"free": false,
"suppressed": false
},
"mailbox": "undeliverable",
"smtp_code": 550,
"suggestion": null,
"domain": "exemplo.invalid",
"already_verified": false,
"contact_id": "019a1e00-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"line": 37
}
]
}
#Aplicar os resultados aos contatos
POST/email-verifications/{id}/apply
Limpa a audiência com o que a validação achou: descadastra ou exclui os contatos cujo endereço deu invalid (e, com include_risky, também risky). Roda em segundo plano e devolve a validação (202); applied mostra a ação, e count e applied_at ficam preenchidos quando termina. Só vale para uma validação completa (ou cancelada) cujos resultados ainda não expiraram; prévias não podem ser aplicadas. Os contatos descadastrados deixam de receber campanhas, e os excluídos saem da audiência.
Parâmetros de caminho
idstringobrigatórioID da validação.
Corpo da requisição
actionstringobrigatóriounsubscribe_invalid(descadastra) oudelete_invalid(exclui).include_riskybooleanInclui os contatos de resultadorisky. Padrão:false.
Erros
- 422
validation_errorA validação ainda não terminou, é uma prévia, os resultados expiraram, já está sendo aplicada ou nenhum contato corresponde. - 404
not_foundA validação não existe ou é de outro time.
cURL
curl -X POST https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/apply \
-H "Authorization: Bearer $POSTAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "unsubscribe_invalid",
"include_risky": false
}'
Node.js
const response = await fetch('https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/apply', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
action: 'unsubscribe_invalid',
include_risky: false,
}),
});
const data = await response.json();
Python
import os
import requests
response = requests.post(
"https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/apply",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
json={
"action": "unsubscribe_invalid",
"include_risky": False,
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->post('email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/apply', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'json' => [
'action' => 'unsubscribe_invalid',
'include_risky' => false,
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email_verification_job",
"id": "019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"source": "csv",
"preview": false,
"segment_id": null,
"filename": "clientes.csv",
"email_column": "email",
"recheck_stale": false,
"status": "completed",
"total": 5200,
"processed": 5200,
"counts": {
"valid": 4410,
"invalid": 390,
"risky": 310,
"unknown": 90
},
"rates": {
"valid": 0.8481,
"invalid": 0.075,
"risky": 0.0596,
"unknown": 0.0173
},
"reasons": {
"mailbox_not_found": 210,
"invalid_syntax": 120,
"disposable": 140,
"role_account": 120,
"catch_all": 50
},
"already_verified": 800,
"checked": 4400,
"credits_used": 4390,
"free_checks": 0,
"credits_exhausted": false,
"population": 5200,
"estimated_credits": 4400,
"applied": {
"action": "unsubscribe_invalid",
"include_risky": false,
"count": null,
"applied_at": null
},
"error": null,
"created_at": "2026-10-24T14:00:00.000000Z",
"started_at": "2026-10-24T14:00:02.000000Z",
"completed_at": "2026-10-24T14:03:40.000000Z",
"results_expire_at": "2026-10-31T14:03:40.000000Z",
"results_filename": "email-verification-customers.csv",
"download_url": "https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/download?expires=1792850620&signature=…"
}
#Visão geral da validação
GET/email-verifications/overview
O painel de validação numa chamada: o saldo e os pacotes à venda (credits), os totais do período (totals: validações, endereços verificados, reaproveitados, divisão por resultado e créditos usados), as taxas (rates), uma série por dia (series), os motivos mais comuns (reasons), os domínios com mais endereços verificados (domains) e as 5 validações mais recentes (recent_jobs). Os domínios só cobrem resultados ainda guardados (7 dias); o resto vem das validações e das cobranças, guardadas por mais tempo.
Parâmetros de consulta
start_datestringInício do período: uma data ou um horário ISO 8601. Padrão: 30 dias atrás.end_datestringFim do período. Uma data sozinha cobre o dia inteiro. Padrão: agora; nunca passa de agora.timezonestringFuso IANA, comoAmerica/Sao_Paulo. Padrão:UTC.
Erros
- 422
validation_errorData inválida,start_datedepois deend_date, fuso desconhecido ou período maior que 400 dias.
cURL
curl 'https://api.postal.so/email-verifications/overview?start_date=2026-10-01&end_date=2026-10-24&timezone=America/Sao_Paulo' \
-H "Authorization: Bearer $POSTAL_API_KEY"
Node.js
const response = await fetch('https://api.postal.so/email-verifications/overview?start_date=2026-10-01&end_date=2026-10-24&timezone=America/Sao_Paulo', {
headers: {
Authorization: `Bearer ${process.env.POSTAL_API_KEY}`,
},
});
const data = await response.json();
Python
import os
import requests
response = requests.get(
"https://api.postal.so/email-verifications/overview",
headers={"Authorization": f"Bearer {os.environ['POSTAL_API_KEY']}"},
params={
"start_date": "2026-10-01",
"end_date": "2026-10-24",
"timezone": "America/Sao_Paulo",
},
timeout=10,
)
data = response.json()
PHP
$client = new \GuzzleHttp\Client(['base_uri' => 'https://api.postal.so']);
$response = $client->get('email-verifications/overview', [
'headers' => ['Authorization' => 'Bearer '.getenv('POSTAL_API_KEY')],
'query' => [
'start_date' => '2026-10-01',
'end_date' => '2026-10-24',
'timezone' => 'America/Sao_Paulo',
],
]);
$data = json_decode($response->getBody(), true);
{
"object": "email_verification_overview",
"from": "2026-10-01T00:00:00-03:00",
"to": "2026-10-24T23:59:59-03:00",
"timezone": "America/Sao_Paulo",
"credits": {
"included": true,
"balance": {
"used": 1240,
"limit": 5000,
"remaining": 4760,
"monthly_remaining": 3760,
"grant_credits": 1000,
"grants": [
{
"id": "019a1f00-4d5e-7f60-9a70-7c8d9e0f1a2b",
"source": "purchase",
"name": "Pacote de 500 créditos",
"credits": 500,
"remaining": 500,
"expires_at": "2027-10-20T12:00:00Z"
},
{
"id": "019a1f00-4d5f-7f61-8b71-8d9e0f1a2b3c",
"source": "purchase",
"name": "Pacote de 500 créditos sem validade",
"credits": 500,
"remaining": 500,
"expires_at": null
}
],
"resets_at": "2026-11-01T00:00:00Z",
"free_preview_remaining": 1000
},
"packs": [
{
"object": "verification_credit_pack",
"id": "019a1f00-5e6f-7a70-8b80-8d9e0f1a2b3c",
"name": "Pacote de 10.000 créditos",
"credits": 10000,
"amount": 4900,
"currency": "BRL",
"prices": [],
"validity_days": 365,
"popular": true
},
{
"object": "verification_credit_pack",
"id": "019a1f00-5e70-7b71-9c81-9e0f1a2b3c4d",
"name": "Pacote de 50.000 créditos sem validade",
"credits": 50000,
"amount": 19900,
"currency": "BRL",
"prices": [],
"validity_days": 0,
"popular": false
}
]
},
"totals": {
"jobs": 3,
"addresses_checked": 9100,
"reused": 1300,
"by_outcome": {
"valid": 7720,
"invalid": 640,
"risky": 560,
"unknown": 180
},
"credits_used": 7800
},
"rates": {
"valid_rate": 0.8484,
"invalid_rate": 0.0703,
"risky_rate": 0.0615,
"unknown_rate": 0.0198
},
"series": [
{
"date": "2026-10-24T00:00:00-03:00",
"checked": 5200,
"by_outcome": {
"valid": 4410,
"invalid": 390,
"risky": 310,
"unknown": 90
},
"credits_used": 4390
}
],
"reasons": [
{
"reason": "mailbox_not_found",
"count": 410
},
{
"reason": "disposable",
"count": 260
}
],
"domains": [
{
"domain": "exemplo.com.br",
"checked": 820,
"invalid": 31,
"invalid_rate": 0.0378
}
],
"recent_jobs": [
{
"object": "email_verification_job",
"id": "019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"source": "csv",
"preview": false,
"segment_id": null,
"filename": "clientes.csv",
"email_column": "email",
"recheck_stale": false,
"status": "completed",
"total": 5200,
"processed": 5200,
"counts": {
"valid": 4410,
"invalid": 390,
"risky": 310,
"unknown": 90
},
"rates": {
"valid": 0.8481,
"invalid": 0.075,
"risky": 0.0596,
"unknown": 0.0173
},
"reasons": {
"mailbox_not_found": 210,
"invalid_syntax": 120,
"disposable": 140,
"role_account": 120,
"catch_all": 50
},
"already_verified": 800,
"checked": 4400,
"credits_used": 4390,
"free_checks": 0,
"credits_exhausted": false,
"population": 5200,
"estimated_credits": 4400,
"applied": null,
"error": null,
"created_at": "2026-10-24T14:00:00.000000Z",
"started_at": "2026-10-24T14:00:02.000000Z",
"completed_at": "2026-10-24T14:03:40.000000Z",
"results_expire_at": "2026-10-31T14:03:40.000000Z",
"results_filename": "email-verification-customers.csv",
"download_url": "https://api.postal.so/email-verifications/019a1f10-1a2b-7c3d-8e4f-5a6b7c8d9e0f/download?expires=1792850620&signature=…"
}
]
}