Iniciar sesión Crear cuenta
Audiencia

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íasNada: 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 listaGratis hasta 1.000 direcciones muestreadas al mes; por encima de eso, créditos normales.
Las demás1 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:

CampoValores
statusvalid (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).
resultdeliverable, undeliverable, do_not_send (se quejó o está suprimido), catch_all (el dominio acepta cualquier destinatario) y unknown.
risklow, 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 / reasonsEl motivo principal (ok en los válidos) y todos los códigos que se aplican, por orden de importancia.
Código (reason)Significa
complained, suppressedLa dirección se quejó de spam o está en la lista de supresión del equipo (do_not_send).
previously_bouncedYa rebotó de forma permanente en un envío del equipo.
invalid_syntax, domain_not_found, no_mxMal formada, dominio que no existe y dominio sin servidor de correo.
mailbox_not_foundEl servidor del destinatario dijo que el buzón no existe.
dns_timeout, mailbox_unverifiableNo se pudo concluir (unknown): falló el DNS o el proveedor no permite confirmar el buzón.
catch_allEl dominio acepta cualquier destinatario, así que no se puede saber si el buzón existe.
mailbox_full, disposable, typo_suspected, role_accountSeñales de riesgo: buzón lleno, dirección desechable, probable error de escritura (mira suggestion) y cuenta de función (contato@, vendas@).
free_providerInformativo: proveedor gratuito. Por sí solo, nunca explica un estado.
has_engagement, no_historyEl 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

Clave de acceso total

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

  • emailstringobligatorio
    La dirección, hasta 320 caracteres.

Errores

  • 422validation_error
    Algún campo no es válido. El mensaje indica cuál.
  • 403plan_limit_reached
    El plan no incluye la verificación de correos.
  • 429verification_credits_exhausted
    Los créditos se acabaron. Los del plan se renuevan el día 1 (UTC); el Adicional de validación libera nuevas verificaciones de inmediato.
  • 429rate_limit_exceeded
    Más de 30 verificaciones en un 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);
200Respuesta
{
  "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

Clave de acceso total

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)

  • sourcestringobligatorio
    De dónde vienen las direcciones: csv (un archivo), segment (un segmento) o contacts (todos los contactos).
  • filearchivo
    Solo con source=csv: el CSV, hasta 20 MB, con una fila de encabezado, enviado como multipart/form-data. No vale para los otros orígenes.
  • email_columnstring
    Solo con source=csv: la columna del encabezado con los correos. Si falta, Postal intenta adivinarla.
  • segment_idstring
    Obligatorio con source=segment.
  • recheck_staleboolean
    Vuelve a verificar las direcciones cuyo resultado guardado tiene más de 90 días. Predeterminado: false.

Errores

  • 422validation_error
    Origen 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.
  • 403plan_limit_reached
    El plan no incluye la verificación de correos.
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);
200Respuesta
{
  "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);

#Vista previa de la salud de una lista

POST/email-verifications/preview

Clave de acceso total

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)

  • sourcestringobligatorio
    De dónde vienen las direcciones: csv (un archivo), segment (un segmento) o contacts (todos los contactos).
  • filearchivo
    Solo con source=csv: el CSV, hasta 20 MB, con una fila de encabezado, enviado como multipart/form-data. No vale para los otros orígenes.
  • email_columnstring
    Solo con source=csv: la columna del encabezado con los correos. Si falta, Postal intenta adivinarla.
  • segment_idstring
    Obligatorio con source=segment.
  • recheck_staleboolean
    Vuelve a verificar las direcciones cuyo resultado guardado tiene más de 90 días. Predeterminado: false.

Errores

  • 422validation_error
    Origen 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.
  • 403plan_limit_reached
    El plan no incluye la verificación de correos.
  • 429verification_credits_exhausted
    Los créditos se acabaron. Los del plan se renuevan el día 1 (UTC); el Adicional de validación libera nuevas verificaciones de inmediato.
  • 429rate_limit_exceeded
    Ya hay 2 verificaciones en curso.
  • 404not_found
    El segmento no 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);
201Respuesta
{
  "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

Clave de acceso total

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)

  • sourcestringobligatorio
    De dónde vienen las direcciones: csv (un archivo), segment (un segmento) o contacts (todos los contactos).
  • filearchivo
    Solo con source=csv: el CSV, hasta 20 MB, con una fila de encabezado, enviado como multipart/form-data. No vale para los otros orígenes.
  • email_columnstring
    Solo con source=csv: la columna del encabezado con los correos. Si falta, Postal intenta adivinarla.
  • segment_idstring
    Obligatorio con source=segment.
  • recheck_staleboolean
    Vuelve a verificar las direcciones cuyo resultado guardado tiene más de 90 días. Predeterminado: false.

Errores

  • 422validation_error
    Origen 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.
  • 403plan_limit_reached
    El plan no incluye la verificación de correos.
  • 429verification_credits_exhausted
    Los créditos se acabaron. Los del plan se renuevan el día 1 (UTC); el Adicional de validación libera nuevas verificaciones de inmediato.
  • 429rate_limit_exceeded
    Ya hay 2 verificaciones en curso.
  • 404not_found
    El segmento no 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);
201Respuesta
{
  "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 verificaciones

GET/email-verifications

Clave de acceso total

Las verificaciones en lote y las vistas previas del equipo, de la más reciente a la más antigua.

Parámetros de consulta

  • limitinteger
    De 1 a 100. Predeterminado: 20.
  • afterstring
    Cursor de la página siguiente.
  • beforestring
    Cursor de la página anterior.
  • previewboolean
    true solo las vistas previas; false solo las verificaciones 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);
200Respuesta
{
  "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}

Clave de acceso total

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

  • idstringobligatorio
    ID de la verificación.

Errores

  • 404not_found
    La verificación no existe o es de otro equipo.
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);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID de la verificación.

Errores

  • 422validation_error
    La verificación ya no está en curso.
  • 404not_found
    La verificación no existe o es de otro equipo.
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);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID de la verificación.

Parámetros de consulta

  • limitinteger
    De 1 a 100. Predeterminado: 20.
  • afterstring
    Cursor de la página siguiente.
  • beforestring
    Cursor de la página anterior.
  • statusstring
    Uno o más estados, separados por comas: valid, invalid, risky, unknown.

Errores

  • 422validation_error
    status con un valor que no existe.
  • 404not_found
    La verificación no existe o es de otro equipo.
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);
200Respuesta
{
  "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

Clave de acceso total

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

  • idstringobligatorio
    ID de la verificación.

Cuerpo de la solicitud

  • actionstringobligatorio
    unsubscribe_invalid (da de baja) o delete_invalid (elimina).
  • include_riskyboolean
    Incluye los contactos con resultado risky. Predeterminado: false.

Errores

  • 422validation_error
    La verificación aún no terminó, es una vista previa, los resultados caducaron, ya se está aplicando o ningún contacto coincide.
  • 404not_found
    La verificación no existe o es de otro equipo.
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);
202Respuesta
{
  "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

Clave de acceso total

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_datestring
    Inicio del período: una fecha o una hora ISO 8601. Predeterminado: hace 30 días.
  • end_datestring
    Fin del período. Una fecha sola cubre el día completo. Predeterminado: ahora; nunca pasa de ahora.
  • timezonestring
    Zona horaria IANA, como America/Sao_Paulo. Predeterminado: UTC.

Errores

  • 422validation_error
    Fecha no válida, start_date posterior a end_date, zona horaria desconocida o período mayor de 400 días.
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);
200Respuesta
{
  "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=…"
    }
  ]
}