Cómo migrar la verificación de correos de Hunter o Apollo a Cold Leads
- Para quién
- Equipos que hoy verifican correos con Hunter o Apollo
- El problema
- El código de verificación está lleno de parámetros y valores de estado propios de cada proveedor. Cambiar el endpoint sin mapearlos altera en silencio a qué direcciones envía un pipeline.
- La solución
- Mapee cada parámetro y campo de respuesta a su equivalente en Cold Leads, deje donde está lo que Cold Leads no sustituye (datos de personas, búsqueda por dominio) y use un pequeño adaptador para que el código existente conserve su forma.
- Qué obtiene
- Un mapeo campo a campo, un adaptador de Python que devuelve campos al estilo de Hunter a partir de Cold Leads y una tabla de costes de Cold Leads para 1 000, 10 000 y 50 000 verificaciones al mes.
Las direcciones, los identificadores y los resultados de los ejemplos son ilustrativos. example.com está reservado para documentación, así que una comprobación real de estas direcciones devuelve invalid.
Los ejemplos de código son iguales en todos los idiomas; sus comentarios están en inglés.
Qué se traslada y qué se queda
- Se traslada: las comprobaciones de una sola dirección (Hunter Email Verifier) a POST /api/v1/verify, las listas a POST /api/v1/verify/bulk (hasta 5 000 direcciones por tarea) y las deducciones a partir de nombre y dominio (Hunter Email Finder) a POST /api/v1/find.
- Se queda: Domain Search de Hunter y las fuentes web detrás de sus resultados, y People Search, People Enrichment y Bulk People Enrichment de Apollo. Cold Leads no tiene una base de datos de personas ni de empresas; GET /api/v1/leads solo busca en los contactos de su propio CRM de Cold Leads.
- La API pública de Apollo no tiene un endpoint de verificación independiente; email_status llega con los datos de la persona. Si mantiene Apollo para los datos, puede comprobar con Cold Leads las direcciones que devuelve antes de enviar.
De Email Verifier a POST /api/v1/verify
| Hunter | Cold Leads | Nota |
|---|---|---|
| GET /v2/email-verifier?email=… | POST /api/v1/verify con un cuerpo JSON {email} | Cold Leads recibe un POST con cuerpo JSON. |
| Parámetro de consulta api_key, cabecera X-API-KEY o Authorization: Bearer | Cabecera x-api-key o Authorization: Bearer | No se aceptan claves en la cadena de consulta. |
| Sin parámetro de tiempo; 202 mientras la comprobación sigue en curso | timeout_ms, de 1 000 a 30 000 | Cold Leads responde cuando termina la comprobación o se agota el margen (entonces risky, motivo timeout). |
| data.status valid | status valid con el último motivo ok | valid con smtp_unreachable significa que el buzón no se comprobó. |
| data.status invalid | status invalid, motivo mailbox_missing, no_mx o syntax | |
| data.status accept_all | status risky, motivo catch_all, catch_all true | |
| data.status disposable | disposable true (status risky, o invalid cuando el dominio no tiene MX) | Lista integrada de 40 dominios de correo desechable. |
| data.status webmail | Sin equivalente | Las direcciones de webmail se comprueban como cualquier otra. |
| data.status unknown | status risky, motivo smtp_unknown o timeout | |
| data.score | score (de 0 a 100) | Escala distinta; decida por los motivos, no por el número. |
| data.regexp | reasons contiene syntax cuando falla la comprobación de sintaxis | |
| data.mx_records | mx: el primer host MX, o null cuando el dominio no tiene registro MX ni registro A (motivo no_mx) | |
| data.smtp_server | smtp_unreachable cuando no se pudo conectar con ningún servidor de correo por SMTP | ok, catch_all y mailbox_missing significan que un servidor de correo respondió a la comprobación del destinatario. |
| data.smtp_check | último motivo ok o catch_all | Un servidor de correo aceptó la dirección. |
| data.accept_all | catch_all | Solo tiene sentido cuando respondió un servidor de correo (último motivo ok, catch_all o mailbox_missing). |
| data.gibberish, data.block, data.sources | Sin equivalente |
De Email Finder a POST /api/v1/find
| Hunter | Cold Leads | Nota |
|---|---|---|
| domain | domain | Una URL se reduce a su host (https://www.example.com/about pasa a ser example.com). |
| company | Sin equivalente | El dominio es obligatorio. |
| first_name, last_name | first, last | Se quitan los acentos y se descartan los demás caracteres no latinos, así que translitere antes los nombres. |
| full_name | name | Se divide en first y last por el primer espacio. |
| linkedin_handle, max_duration | Sin equivalente | |
| data.email | null cuando no se pudo construir ningún candidato, por ejemplo si el dominio no tiene MX. | |
| data.score | confidence (de 0 a 100) | |
| data.verification.status | method | verified solo cuando un servidor de correo confirmó el buzón; pattern para una suposición basada en patrones de dirección habituales; none cuando no se encontró nada. |
| data.accept_all | catch_all | |
| data.position, twitter, linkedin_url, phone_number, company, sources | Sin equivalente | |
| (ninguno) | candidates | Direcciones construidas a partir de patrones habituales, cada una con el estado de su comprobación si se comprobó. |
Un adaptador de sustitución directa
Si su código se ramifica según los nombres de campo de Hunter, estas dos funciones devuelven la misma forma a partir de Cold Leads, más la respuesta original bajo coldleads. El mapeo es deliberadamente conservador: una dirección cuyo buzón no se comprobó (smtp_unreachable) pasa a ser unknown, no valid.
# pip install httpx
# Drop-in helpers for code written against Hunter's email-verifier and email-finder.
# They call Cold Leads and return Hunter-style fields, plus the raw Cold Leads answer.
import os
import httpx
cold = httpx.Client(
base_url="https://coldleads.app/api/v1",
headers={"x-api-key": os.environ["COLDLEADS_API_KEY"]}, # the key goes in a header, never in the URL
timeout=40.0,
)
# a mail server answered the SMTP probe only when the last reason is one of these
PROBED = {"ok", "catch_all", "mailbox_missing"}
def email_verifier(email: str) -> dict:
r = cold.post("/verify", json={"email": email, "timeout_ms": 20000})
r.raise_for_status()
c = r.json()
last = c["reasons"][-1]
if c["disposable"] and last not in ("syntax", "no_mx"):
status = "disposable"
elif last == "ok":
status = "valid"
elif last == "catch_all":
status = "accept_all"
elif last in ("mailbox_missing", "no_mx", "syntax"):
status = "invalid"
else:
# smtp_unreachable (mailbox not checked), smtp_unknown or timeout: not verified
status = "unknown"
return {
"email": c["email"],
"status": status,
"score": c["score"],
"regexp": last != "syntax",
"disposable": c["disposable"],
"mx_records": c["mx"] is not None,
"smtp_check": last in ("ok", "catch_all"),
"accept_all": c["catch_all"] if last in PROBED else None, # None: not tested
"coldleads": c,
}
def email_finder(domain: str, first_name: str = "", last_name: str = "", full_name: str = "") -> dict:
body = {"domain": domain, "first": first_name, "last": last_name}
if full_name and not (first_name or last_name):
body = {"domain": domain, "name": full_name}
r = cold.post("/find", json=body)
r.raise_for_status()
c = r.json()
return {
"email": c["email"],
"score": c["confidence"],
"accept_all": c["catch_all"],
# Cold Leads says "verified" only when a mail server confirmed the mailbox; a pattern guess is not verified
"verification": {"status": "valid" if c["method"] == "verified" else None},
"coldleads": c,
}
if __name__ == "__main__":
print(email_verifier("anna@example.com"))Diferencias de comportamiento que debe prever
- Códigos de error: Cold Leads responde 429 por su límite de peticiones (rate_limited, con Retry-After: 60) y por demasiadas tareas masivas abiertas (too_many_jobs), y 402 no_credits cuando se acaban los créditos. Hunter documenta 403 para su límite de peticiones y 429 para su límite de uso, así que hay que reescribir el manejo de errores, no basta con renombrar.
- Límite de peticiones: 120 peticiones por minuto por clave. Para listas, una tarea masiva sustituye hasta 5 000 llamadas individuales.
- Cada llamada cuesta 1 crédito, incluidos los resultados servidos desde la caché de 30 días.
- La comprobación SMTP del buzón y de accept-all solo se ejecuta cuando Cold Leads puede abrir una conexión SMTP con el servidor de correo del destinatario. Los reasons de cada resultado indican si se ejecutó: smtp_unreachable significa que el buzón no se comprobó.
Límites y costes
| Verificaciones al mes | Créditos | Coste de Cold Leads al mes |
|---|---|---|
| 1 000 | 1 000 | $99: el plan Business, que incluye 10 000 créditos |
| 10 000 | 10 000 | $99 |
| 50 000 | 50 000 | $299: $99 más 40 paquetes extra de 1 000 créditos a $5 |
La API solo está en el plan Business, que también se puede pagar anualmente ($999). Las llamadas de búsqueda (find) consumen los mismos créditos, 1 por llamada. Los créditos mensuales se cuentan por mes natural (UTC) y se usan antes que los paquetes comprados. Los precios están en dólares estadounidenses; pueden aplicarse impuestos.
Preguntas frecuentes
¿Tiene Cold Leads un equivalente de Domain Search de Hunter o de la búsqueda de personas de Apollo?
No. Cold Leads no recopila datos de personas. GET /api/v1/leads solo busca en los contactos que ya tiene en su CRM de Cold Leads.
¿Seguirán funcionando mis umbrales de puntuación actuales?
No de forma fiable, porque las escalas son distintas. Decida mejor por el estado y el último código de motivo: ok para enviar, catch_all y smtp_unknown para revisar, mailbox_missing, no_mx y syntax para descartar, y smtp_unreachable para direcciones cuyo buzón no se comprobó.
¿Puedo verificar una exportación de Apollo con Cold Leads?
Sí. Envíe las direcciones como tareas masivas de hasta 5 000 y consulte los resultados, o use el script de Python para archivos CSV de este sitio. Cada dirección cuesta 1 crédito.
¿Cuestan menos los resultados en caché?
No. Un resultado servido desde la caché de 30 días lleva cached: true y cuesta 1 crédito como cualquier otra llamada.