Verificación de correos
Comprueba si una dirección recibe correo antes de enviar: una por una, en lote o en tus contactos, con créditos mensuales del plan.
Postal comprueba la sintaxis, el dominio y los servidores de correo de cada dirección, señala direcciones desechables, de función (como contato@) y con errores de escritura, intenta confirmar el buzón con el servidor del destinatario y cruza el resultado con el historial de envíos de tu equipo (rebotes, quejas y supresiones). Hay tres maneras de usarlo: una dirección a la vez, una lista en lote (un CSV, un segmento o todos los contactos) y la vista previa de la salud de una lista. El último resultado de cada dirección pasa a ser el email_status del contacto y puede filtrar la lista de contactos y los segmentos dinámicos. Todos los endpoints usan una clave de API de acceso total y requieren un plan que incluya la verificación.
#Créditos
Cada dirección verificada usa 1 crédito. El plan da créditos por mes (Gratis: 100, Profesional: 5.000, Escala: 50.000, Empresarial: sin límite) que se renuevan el día 1 de cada mes (UTC); un plan con 0 no incluye la verificación (403 plan_limit_reached). Los paquetes del Adicional de validación, comprados en el panel (o concedidos por Postal), se suman a los créditos del plan y aparecen en credits.grants. Cada paquete tiene la vigencia definida para él, en días (validity_days, en los paquetes a la venta), y algunos no tienen vencimiento: en esos, validity_days es 0 y, en credits.grants, expires_at es null. Los créditos del mes se usan primero; después, los paquetes que vencen antes, y por último los que no vencen.
| Qué | Cuesta |
|---|---|
| Una dirección que ya tiene un resultado guardado de menos de 90 días | Nada: el resultado se reutiliza (cached: true o already_verified), recalculado con el historial actual del equipo. Con recheck_stale, las más antiguas se verifican de nuevo. |
Dirección mal formada o fallo de DNS (unknown) | Nada. Los resultados unknown no se guardan, así que la próxima verificación lo intenta de nuevo. |
| Vista previa de la salud de la lista | Gratis hasta 1.000 direcciones muestreadas al mes; por encima de eso, créditos normales. |
| Las demás | 1 crédito por dirección. |
Sin créditos, la respuesta es 429 con verification_credits_exhausted. Los endpoints devuelven el saldo en credits: used y limit (créditos mensuales), monthly_remaining, grant_credits (de los paquetes), remaining (la suma; null si es ilimitado), resets_at y free_preview_remaining.
#Entender el resultado
Todo resultado tiene cuatro lecturas de la misma dirección, de la más simple a la más detallada:
| Campo | Valores |
|---|---|
status | valid (entregable, con riesgo bajo o medio), invalid (no entregable o que no debe recibir), risky (catch_all, desechable, cuenta de función sin interacción o riesgo alto) y unknown (las comprobaciones no terminaron). |
result | deliverable, undeliverable, do_not_send (se quejó o está suprimido), catch_all (el dominio acepta cualquier destinatario) y unknown. |
risk | low, medium, high o unknown. La interacción de tu equipo ajusta el riesgo: quien recibió, abrió o hizo clic antes queda con menor riesgo; una cuenta de función o un error de escritura que nunca recibió nada sube un nivel. |
reason / reasons | El motivo principal (ok en los válidos) y todos los códigos que se aplican, por orden de importancia. |
Código (reason) | Significa |
|---|---|
complained, suppressed | La dirección se quejó de spam o está en la lista de supresión del equipo (do_not_send). |
previously_bounced | Ya rebotó de forma permanente en un envío del equipo. |
invalid_syntax, domain_not_found, no_mx | Mal formada, dominio que no existe y dominio sin servidor de correo. |
mailbox_not_found | El servidor del destinatario dijo que el buzón no existe. |
dns_timeout, mailbox_unverifiable | No se pudo concluir (unknown): falló el DNS o el proveedor no permite confirmar el buzón. |
catch_all | El dominio acepta cualquier destinatario, así que no se puede saber si el buzón existe. |
mailbox_full, disposable, typo_suspected, role_account | Señales de riesgo: buzón lleno, dirección desechable, probable error de escritura (mira suggestion) y cuenta de función (contato@, vendas@). |
free_provider | Informativo: proveedor gratuito. Por sí solo, nunca explica un estado. |
has_engagement, no_history | El historial del equipo: ya envió y la dirección recibió, abrió o hizo clic, o nunca envió nada a ella. |
checks trae las comprobaciones como booleanos: syntax, mx (el dominio acepta correo; null si no se pudo saber), smtp (el buzón existe; null si no se confirmó), disposable, role, free y suppressed (se quejó, está suprimido o ya rebotó). mailbox y smtp_code son el resultado de la conversación con el servidor del destinatario (deliverable, undeliverable, catch_all, full o unknown), o null si no se consultó el buzón. suggestion es la corrección de un probable error de escritura, como gmail.com para gmial.com.
#Validar una dirección
POST/email-verifications/check
Verifica una dirección al instante. Un resultado guardado y reciente (menos de 90 días) vuelve gratis, con cached: true; si no, la verificación usa un crédito (primero los del mes, después los paquetes). Las direcciones mal formadas y los fallos de DNS no gastan crédito. Para listas, usa la verificación en lote. Límite: 30 verificaciones por minuto por equipo (429 rate_limit_exceeded).
Cuerpo de la solicitud
emailstringobligatorioLa dirección, hasta 320 caracteres.
Errores
- 422
validation_errorAlgún campo no es válido. El mensaje indica cuál. - 403
plan_limit_reachedEl plan no incluye la verificación de correos. - 429
verification_credits_exhaustedLos créditos se acabaron. Los del plan se renuevan el día 1 (UTC); el Adicional de validación libera nuevas verificaciones de inmediato. - 429
rate_limit_exceededMás de 30 verificaciones en un 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 el costo de una lista
POST/email-verifications/estimate
Lee el origen y dice cuánto costaría verificarlo, sin verificar ni gastar nada: cuántas direcciones hay (total), cuántas son únicas (unique), cuántas ya tienen resultado guardado (already_verified) y cuántas se verificarían (to_check, que es también credits_needed). enough_credits dice si el saldo lo cubre. preview muestra el tamaño de la muestra de la vista previa y cuánto costaría; max_addresses es el máximo por verificación (200.000).
Cuerpo de la solicitud (JSON o multipart/form-data)
sourcestringobligatorioDe dónde vienen las direcciones:csv(un archivo),segment(un segmento) ocontacts(todos los contactos).filearchivoSolo consource=csv: el CSV, hasta 20 MB, con una fila de encabezado, enviado comomultipart/form-data. No vale para los otros orígenes.email_columnstringSolo consource=csv: la columna del encabezado con los correos. Si falta, Postal intenta adivinarla.segment_idstringObligatorio consource=segment.recheck_stalebooleanVuelve a verificar las direcciones cuyo resultado guardado tiene más de 90 días. Predeterminado:false.
Errores
- 422
validation_errorOrigen no válido, archivo ausente, demasiado grande o que no es CSV, columna que no está en el encabezado, lista sin direcciones o con más de 200.000. - 403
plan_limit_reachedEl plan no incluye la verificación de correos.
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);
#Vista previa de la salud de una lista
POST/email-verifications/preview
Verifica una muestra aleatoria del origen (5% de la lista, de 50 a 500 direcciones) para que veas la proporción de válidas, no válidas y arriesgadas antes de gastar créditos con todo. La muestra es gratis hasta 1.000 direcciones al mes; por encima de eso, usa créditos normales. El cuerpo es el mismo de la estimación. Devuelve la verificación (preview: true), que se ejecuta en segundo plano: síguela en GET /email-verifications/{id}. Una vista previa no se puede aplicar a los contactos.
Cuerpo de la solicitud (JSON o multipart/form-data)
sourcestringobligatorioDe dónde vienen las direcciones:csv(un archivo),segment(un segmento) ocontacts(todos los contactos).filearchivoSolo consource=csv: el CSV, hasta 20 MB, con una fila de encabezado, enviado comomultipart/form-data. No vale para los otros orígenes.email_columnstringSolo consource=csv: la columna del encabezado con los correos. Si falta, Postal intenta adivinarla.segment_idstringObligatorio consource=segment.recheck_stalebooleanVuelve a verificar las direcciones cuyo resultado guardado tiene más de 90 días. Predeterminado:false.
Errores
- 422
validation_errorOrigen no válido, archivo ausente, demasiado grande o que no es CSV, columna que no está en el encabezado, lista sin direcciones o con más de 200.000. - 403
plan_limit_reachedEl plan no incluye la verificación de correos. - 429
verification_credits_exhaustedLos créditos se acabaron. Los del plan se renuevan el día 1 (UTC); el Adicional de validación libera nuevas verificaciones de inmediato. - 429
rate_limit_exceededYa hay 2 verificaciones en curso. - 404
not_foundEl segmento no 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
}
#Verificar una lista en lote
POST/email-verifications
Empieza a verificar todas las direcciones del origen, en segundo plano. Antes de empezar, Postal compara el saldo con el to_check de la estimación (429 verification_credits_exhausted si no lo cubre). Cada equipo puede tener hasta 2 verificaciones en curso, y cada una hasta 200.000 direcciones. Sigue el avance en GET /email-verifications/{id}; al final, lee los resultados o descarga el CSV en download_url (un enlace firmado, válido por 1 hora). Los resultados se conservan 7 días; la verificación en sí, 30. Con source=contacts o segment, cada resultado trae el contact_id, y el resultado pasa a ser el email_status del contacto.
Cuerpo de la solicitud (JSON o multipart/form-data)
sourcestringobligatorioDe dónde vienen las direcciones:csv(un archivo),segment(un segmento) ocontacts(todos los contactos).filearchivoSolo consource=csv: el CSV, hasta 20 MB, con una fila de encabezado, enviado comomultipart/form-data. No vale para los otros orígenes.email_columnstringSolo consource=csv: la columna del encabezado con los correos. Si falta, Postal intenta adivinarla.segment_idstringObligatorio consource=segment.recheck_stalebooleanVuelve a verificar las direcciones cuyo resultado guardado tiene más de 90 días. Predeterminado:false.
Errores
- 422
validation_errorOrigen no válido, archivo ausente, demasiado grande o que no es CSV, columna que no está en el encabezado, lista sin direcciones o con más de 200.000. - 403
plan_limit_reachedEl plan no incluye la verificación de correos. - 429
verification_credits_exhaustedLos créditos se acabaron. Los del plan se renuevan el día 1 (UTC); el Adicional de validación libera nuevas verificaciones de inmediato. - 429
rate_limit_exceededYa hay 2 verificaciones en curso. - 404
not_foundEl segmento no 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 verificaciones
GET/email-verifications
Las verificaciones en lote y las vistas previas del equipo, de la más reciente a la más antigua.
Parámetros de consulta
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.previewbooleantruesolo las vistas previas;falsesolo las verificaciones 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=…"
}
]
}
#Obtener una verificación
GET/email-verifications/{id}
status: pending, processing, completed, canceled o failed (con error). processed de total muestra el avance; counts y rates dan la división por estado, y reasons los motivos más comunes. already_verified son las direcciones reutilizadas, checked las verificadas ahora, credits_used y free_checks lo que se cobró y lo que fue gratis, y credits_exhausted avisa de que la verificación se detuvo por falta de créditos. Aquí también viene contacts: cuántos contactos cambiaría aplicar, para los resultados invalid y risky. Una verificación sin progreso durante 30 minutos (por ejemplo, un servidor que se cayó) se reanuda sola, hasta 3 veces seguidas.
Parámetros de ruta
idstringobligatorioID de la verificación.
Errores
- 404
not_foundLa verificación no existe o es de otro equipo.
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 una verificación
POST/email-verifications/{id}/cancel
Detiene una verificación que todavía está en cola o en ejecución. Lo que ya se verificó queda en los resultados (y se cobró); el resto no se verifica. Una verificación que aún no empezó elimina el archivo enviado.
Parámetros de ruta
idstringobligatorioID de la verificación.
Errores
- 422
validation_errorLa verificación ya no está en curso. - 404
not_foundLa verificación no existe o es de otro equipo.
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 una verificación
GET/email-verifications/{id}/results
Un resultado por dirección, en el orden del archivo. Cada uno trae las lecturas de Entender el resultado, más line (la línea del CSV, contando el encabezado como 1), already_verified y el contact_id del contacto correspondiente, cuando lo hay. Los resultados se eliminan 7 días después del fin de la verificación.
Parámetros de ruta
idstringobligatorioID de la verificación.
Parámetros de consulta
limitintegerDe 1 a 100. Predeterminado: 20.afterstringCursor de la página siguiente.beforestringCursor de la página anterior.statusstringUno o más estados, separados por comas:valid,invalid,risky,unknown.
Errores
- 422
validation_errorstatuscon un valor que no existe. - 404
not_foundLa verificación no existe o es de otro equipo.
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 los resultados a los contactos
POST/email-verifications/{id}/apply
Limpia la audiencia con lo que encontró la verificación: da de baja o elimina los contactos cuya dirección dio invalid (y, con include_risky, también risky). Se ejecuta en segundo plano y devuelve la verificación (202); applied muestra la acción, y count y applied_at se rellenan al terminar. Solo vale para una verificación completa (o cancelada) cuyos resultados aún no han caducado; las vistas previas no se pueden aplicar. Los contactos dados de baja dejan de recibir campañas, y los eliminados salen de la audiencia.
Parámetros de ruta
idstringobligatorioID de la verificación.
Cuerpo de la solicitud
actionstringobligatoriounsubscribe_invalid(da de baja) odelete_invalid(elimina).include_riskybooleanIncluye los contactos con resultadorisky. Predeterminado:false.
Errores
- 422
validation_errorLa verificación aún no terminó, es una vista previa, los resultados caducaron, ya se está aplicando o ningún contacto coincide. - 404
not_foundLa verificación no existe o es de otro equipo.
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=…"
}
#Visión general de la verificación
GET/email-verifications/overview
El panel de verificación en una sola llamada: el saldo y los paquetes a la venta (credits), los totales del período (totals: verificaciones, direcciones verificadas, reutilizadas, división por resultado y créditos usados), las tasas (rates), una serie por día (series), los motivos más comunes (reasons), los dominios con más direcciones verificadas (domains) y las 5 verificaciones más recientes (recent_jobs). Los dominios solo cubren resultados aún guardados (7 días); el resto viene de las verificaciones y de los cobros, guardados por más tiempo.
Parámetros de consulta
start_datestringInicio del período: una fecha o una hora ISO 8601. Predeterminado: hace 30 días.end_datestringFin del período. Una fecha sola cubre el día completo. Predeterminado: ahora; nunca pasa de ahora.timezonestringZona horaria IANA, comoAmerica/Sao_Paulo. Predeterminado:UTC.
Errores
- 422
validation_errorFecha no válida,start_dateposterior aend_date, zona horaria desconocida o período mayor de 400 días.
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=…"
}
]
}