Cold Leads

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

HunterCold LeadsNota
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: BearerCabecera x-api-key o Authorization: BearerNo se aceptan claves en la cadena de consulta.
Sin parámetro de tiempo; 202 mientras la comprobación sigue en cursotimeout_ms, de 1 000 a 30 000Cold Leads responde cuando termina la comprobación o se agota el margen (entonces risky, motivo timeout).
data.status validstatus valid con el último motivo okvalid con smtp_unreachable significa que el buzón no se comprobó.
data.status invalidstatus invalid, motivo mailbox_missing, no_mx o syntax
data.status accept_allstatus risky, motivo catch_all, catch_all true
data.status disposabledisposable true (status risky, o invalid cuando el dominio no tiene MX)Lista integrada de 40 dominios de correo desechable.
data.status webmailSin equivalenteLas direcciones de webmail se comprueban como cualquier otra.
data.status unknownstatus risky, motivo smtp_unknown o timeout
data.scorescore (de 0 a 100)Escala distinta; decida por los motivos, no por el número.
data.regexpreasons contiene syntax cuando falla la comprobación de sintaxis
data.mx_recordsmx: el primer host MX, o null cuando el dominio no tiene registro MX ni registro A (motivo no_mx)
data.smtp_serversmtp_unreachable cuando no se pudo conectar con ningún servidor de correo por SMTPok, 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_allUn servidor de correo aceptó la dirección.
data.accept_allcatch_allSolo tiene sentido cuando respondió un servidor de correo (último motivo ok, catch_all o mailbox_missing).
data.gibberish, data.block, data.sourcesSin equivalente

De Email Finder a POST /api/v1/find

HunterCold LeadsNota
domaindomainUna URL se reduce a su host (https://www.example.com/about pasa a ser example.com).
companySin equivalenteEl dominio es obligatorio.
first_name, last_namefirst, lastSe quitan los acentos y se descartan los demás caracteres no latinos, así que translitere antes los nombres.
full_namenameSe divide en first y last por el primer espacio.
linkedin_handle, max_durationSin equivalente
data.emailemailnull cuando no se pudo construir ningún candidato, por ejemplo si el dominio no tiene MX.
data.scoreconfidence (de 0 a 100)
data.verification.statusmethodverified 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_allcatch_all
data.position, twitter, linkedin_url, phone_number, company, sourcesSin equivalente
(ninguno)candidatesDirecciones 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.

hunter_adapter.py
# 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 mesCréditosCoste de Cold Leads al mes
1 0001 000$99: el plan Business, que incluye 10 000 créditos
10 00010 000$99
50 00050 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.