Entrar Criar conta
Audiência

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 queCusta
Um endereço que já tem resultado guardado de menos de 90 diasNada: 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 listaGrátis até 1.000 endereços amostrados por mês; acima disso, créditos normais.
Os demais1 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:

CampoValores
statusvalid (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).
resultdeliverable, undeliverable, do_not_send (reclamou ou está suprimido), catch_all (o domínio aceita qualquer destinatário) e unknown.
risklow, 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 / reasonsO 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, suppressedO endereço reclamou de spam ou está na lista de supressão do time (do_not_send).
previously_bouncedJá deu bounce permanente num envio do time.
invalid_syntax, domain_not_found, no_mxMal formado, domínio que não existe e domínio sem servidor de e-mail.
mailbox_not_foundO servidor do destinatário disse que a caixa não existe.
dns_timeout, mailbox_unverifiableNão foi possível concluir (unknown): o DNS falhou ou o provedor não permite confirmar a caixa.
catch_allO domínio aceita qualquer destinatário, então não dá para saber se a caixa existe.
mailbox_full, disposable, typo_suspected, role_accountSinais de risco: caixa cheia, endereço descartável, provável erro de digitação (veja suggestion) e conta de função (contato@, vendas@).
free_providerInformativo: provedor gratuito. Sozinho, nunca explica um status.
has_engagement, no_historyO 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

Chave de acesso total

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ório
    O endereço, até 320 caracteres.

Erros

  • 422validation_error
    Algum campo é inválido. A mensagem diz qual.
  • 403plan_limit_reached
    O plano não inclui a validação de e-mails.
  • 429verification_credits_exhausted
    Os créditos acabaram. Os do plano renovam no dia 1º (UTC); o Adicional de validação libera novas validações na hora.
  • 429rate_limit_exceeded
    Mais de 30 verificações em um minuto.
POST /email-verifications/check

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);
200Resposta
{
  "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

Chave de acesso total

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ório
    De onde vêm os endereços: csv (um arquivo), segment (um segmento) ou contacts (todos os contatos).
  • filearquivo
    Só com source=csv: o CSV, até 20 MB, com uma linha de cabeçalho, enviado como multipart/form-data. Não vale para as outras origens.
  • email_columnstring
    Só com source=csv: a coluna do cabeçalho com os e-mails. Se faltar, a Postal tenta adivinhar.
  • segment_idstring
    Obrigatório com source=segment.
  • recheck_staleboolean
    Verifica de novo os endereços cujo resultado guardado tem mais de 90 dias. Padrão: false.

Erros

  • 422validation_error
    Origem 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.
  • 403plan_limit_reached
    O plano não inclui a validação de e-mails.
POST /email-verifications/estimate

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);
200Resposta
{
  "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
  }
}
POST /email-verifications/estimate (CSV)

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

Chave de acesso total

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ório
    De onde vêm os endereços: csv (um arquivo), segment (um segmento) ou contacts (todos os contatos).
  • filearquivo
    Só com source=csv: o CSV, até 20 MB, com uma linha de cabeçalho, enviado como multipart/form-data. Não vale para as outras origens.
  • email_columnstring
    Só com source=csv: a coluna do cabeçalho com os e-mails. Se faltar, a Postal tenta adivinhar.
  • segment_idstring
    Obrigatório com source=segment.
  • recheck_staleboolean
    Verifica de novo os endereços cujo resultado guardado tem mais de 90 dias. Padrão: false.

Erros

  • 422validation_error
    Origem 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.
  • 403plan_limit_reached
    O plano não inclui a validação de e-mails.
  • 429verification_credits_exhausted
    Os créditos acabaram. Os do plano renovam no dia 1º (UTC); o Adicional de validação libera novas validações na hora.
  • 429rate_limit_exceeded
    Já há 2 validações em andamento.
  • 404not_found
    O segmento não existe.
POST /email-verifications/preview

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);
201Resposta
{
  "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

Chave de acesso total

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ório
    De onde vêm os endereços: csv (um arquivo), segment (um segmento) ou contacts (todos os contatos).
  • filearquivo
    Só com source=csv: o CSV, até 20 MB, com uma linha de cabeçalho, enviado como multipart/form-data. Não vale para as outras origens.
  • email_columnstring
    Só com source=csv: a coluna do cabeçalho com os e-mails. Se faltar, a Postal tenta adivinhar.
  • segment_idstring
    Obrigatório com source=segment.
  • recheck_staleboolean
    Verifica de novo os endereços cujo resultado guardado tem mais de 90 dias. Padrão: false.

Erros

  • 422validation_error
    Origem 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.
  • 403plan_limit_reached
    O plano não inclui a validação de e-mails.
  • 429verification_credits_exhausted
    Os créditos acabaram. Os do plano renovam no dia 1º (UTC); o Adicional de validação libera novas validações na hora.
  • 429rate_limit_exceeded
    Já há 2 validações em andamento.
  • 404not_found
    O segmento não existe.
POST /email-verifications

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);
201Resposta
{
  "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
}
POST /email-verifications (CSV)

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

Chave de acesso total

As validações em lote e as prévias do time, da mais recente para a mais antiga.

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
  • previewboolean
    true só as prévias; false só as validações completas.
GET /email-verifications

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);
200Resposta
{
  "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}

Chave de acesso total

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ório
    ID da validação.

Erros

  • 404not_found
    A validação não existe ou é de outro time.
GET /email-verifications/{id}

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);
200Resposta
{
  "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

Chave de acesso total

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ório
    ID da validação.

Erros

  • 422validation_error
    A validação não está mais em andamento.
  • 404not_found
    A validação não existe ou é de outro time.
POST /email-verifications/{id}/cancel

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);
200Resposta
{
  "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

Chave de acesso total

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ório
    ID da validação.

Parâmetros de consulta

  • limitinteger
    De 1 a 100. Padrão: 20.
  • afterstring
    Cursor da próxima página.
  • beforestring
    Cursor da página anterior.
  • statusstring
    Um ou mais status, separados por vírgula: valid, invalid, risky, unknown.

Erros

  • 422validation_error
    status com um valor que não existe.
  • 404not_found
    A validação não existe ou é de outro time.
GET /email-verifications/{id}/results

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);
200Resposta
{
  "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

Chave de acesso total

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ório
    ID da validação.

Corpo da requisição

  • actionstringobrigatório
    unsubscribe_invalid (descadastra) ou delete_invalid (exclui).
  • include_riskyboolean
    Inclui os contatos de resultado risky. Padrão: false.

Erros

  • 422validation_error
    A validação ainda não terminou, é uma prévia, os resultados expiraram, já está sendo aplicada ou nenhum contato corresponde.
  • 404not_found
    A validação não existe ou é de outro time.
POST /email-verifications/{id}/apply

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);
202Resposta
{
  "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

Chave de acesso total

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_datestring
    Início do período: uma data ou um horário ISO 8601. Padrão: 30 dias atrás.
  • end_datestring
    Fim do período. Uma data sozinha cobre o dia inteiro. Padrão: agora; nunca passa de agora.
  • timezonestring
    Fuso IANA, como America/Sao_Paulo. Padrão: UTC.

Erros

  • 422validation_error
    Data inválida, start_date depois de end_date, fuso desconhecido ou período maior que 400 dias.
GET /email-verifications/overview

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);
200Resposta
{
  "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=…"
    }
  ]
}