Abrir un buzón

API y automatización

Esperar un email en el código: sondeo sin webhook

Un webhook te avisa cuando llega el correo. Sin uno, tienes que preguntar — y el bucle que pregunta es donde las pruebas de extremo a extremo se vuelven inestables, los agentes se quedan colgados, y los scripts chocan con un límite de frecuencia. Esto es lo que ese bucle tiene que hacer bien: un plazo en vez de un contador, un intervalo que se amplía, una regla para decidir qué mensaje es el tuyo, y el único sitio donde la espera se puede hacer por ti.

  • Intermedio
  • 29 min de lectura
Un sobre azul flotando junto a un cronómetro gris con una flecha circular azul girando alrededor

Push y pull, y lo que cuesta cada uno

Solo hay dos formas de que tu código se entere de que ha llegado un mensaje. O el otro lado te avisa, o preguntas tú. Todo lo demás — una librería cliente con un waitFor dentro, un SDK que dice que «transmite» un buzón, un helper de pruebas que bloquea — es una de esas dos formas con la maquinaria escondida, y merece la pena saber cuál de las dos tienes entre manos antes de tener que depurarla.

Cuatro combinaciones cubren casi todo lo que hay disponible.

Un webhook
El servicio hace una petición HTTP a una dirección que tú controlas, cada vez que llega correo. Es la espera más barata que existe — no haces nada en absoluto hasta que hay algo que hacer — y el precio es una dirección en la internet pública, un listener que esté activo justo cuando llega el correo, un secreto compartido para probar que la petición viene de ellos, y tu propia respuesta a qué pasa cuando tu listener no estaba ahí.
Un sondeo largo
Haces la petición y el servidor la mantiene abierta hasta que llega correo o expira un timeout. No necesita de ti nada más que una conexión saliente, y le cuesta al servidor un worker por cada uno que espera — por eso todo servicio que lo ofrece limita tanto la duración de la espera como el número de esperas simultáneas.
Un sondeo simple
Preguntas, repetidamente, y cada petición se responde al instante con lo que haya. Es la única disposición que funciona desde un portátil detrás de un router, desde un runner de CI sin ruta de entrada, y desde un agente que corre dentro del sandbox de otro — y es el tema entero de esta guía.
Un protocolo de buzón
IMAP tiene IDLE, que no es más que un sondeo largo con otro disfraz: la conexión se queda abierta y el servidor anuncia el correo nuevo por ella. Es genuinamente de tipo push, y necesita un buzón con credenciales, un cliente que pueda mantener abierto un socket y reconectar cuando se caiga, y un servidor que respete el comando — que es muchísima maquinaria para una tarea que necesita un solo mensaje.
El servicio de correohace la peticiónTu listenerURL pública, secreto, activollama a tu direcciónTu códigohace la peticiónEl buzónresponde con lo que hayapregunta cada segundoUn webhook necesita una dirección en la internet pública. Un sondeo necesita un bucle, y nada más.
Las dos formas, y lo que en realidad decide entre ellas. Una necesita una dirección en la internet pública; la otra no necesita nada más que poder hacer una petición saliente, que es la única de las dos que un runner de pruebas siempre tiene.

Puestas una junto a otra, la elección resulta depender menos de la elegancia que de lo que cada una le exige a la máquina donde corre tu código.

Qué necesita de tiUn webhookSondeo
Una dirección donde se pueda alcanzar tu códigoSí: una URL pública con certificado, enrutable desde internet.No. Una sola petición saliente es todo el requisito.
Un secreto que guardar y rotarSí: una clave de firma, o cualquier desconocido puede enviarte un mensaje falso.No. No hay nada que verificar, porque nunca llega nada sin que lo hayas pedido.
Algo que esté funcionando en el momento en que llega el correoSí — y cuando está caído, que llegues a recibir el mensaje o no depende de la política de reintentos del remitente, no de ti.No. No se pierde nada mientras no estás mirando: el buzón lo conserva durante 5 días de todas formas.
Peticiones hechas cuando no hay correoNinguna en absoluto. Ese es todo el atractivo.Una por intervalo — el coste real, y de lo que trata el resto de esta guía.

El bucle que todo el mundo escribe primero

Son cuatro líneas, funciona el día en que se escribe, y cada uno de sus problemas aparece más tarde y en otro sitio: en un pipeline a las tres de la madrugada, en un agente que lleva once minutos «pensando», en un buzón que responde 429 a un compañero porque tu bucle se está quedando con todo el presupuesto.

el bucle desde el que empezar
import time
import requests

while True:
    r = requests.get("https://grabmail.io/api/v1/mailbox",
                     params={"address": "signup-42@grabmail.io"})
    if r.json()["messages"]:
        break
    time.sleep(1)

Tiene cinco cosas mal, y solo la primera es obvia.

Nunca se rinde
No hay plazo, así que cuando el mensaje de verdad no va a llegar — el formulario rechazó la dirección, la cola del remitente está atascada, alguien escribió mal el dominio — este bucle no falla. Se queda colgado. Una tarea que se queda colgada es peor que una que falla, porque el log termina sin decir nunca por qué.
Cuenta intentos y los llama segundos
Incluso con un límite en el número de vueltas, treinta intentos de «un segundo» nunca son treinta segundos: cada vuelta también cuesta una petición, y una petición que tarda 400 ms convierte tus treinta segundos en cuarenta y dos. Añade un reintento más y la aritmética deja de ser aritmética.
Todos los runners preguntan en el mismo instante
Lanza veinte tareas desde el mismo pipeline y harán sondeo al unísono, porque todas empezaron con unos pocos milisegundos de diferencia y todas duermen el mismo segundo exacto. El pico es veinte veces la media, y es el pico el que se lleva los rechazos.
Toma el mensaje más nuevo, no el tuyo
La primera entrada de la lista es lo que sea que esté arriba en ese buzón, que en una dirección pública puede ser el correo de otra persona, y en una dirección reutilizada es el de la semana pasada. Un bucle que termina en el primer mensaje que ve, terminará encantado antes de que llegue aquel al que estaba esperando.
Trata toda respuesta como un éxito
Leer la lista de mensajes a partir de un 429 o un 404 lanza un error a tres saltos de distancia de lo que lo explica, y leerla a partir de un 500 puede no lanzar nada en absoluto. El código de estado es lo primero que hay que mirar, no lo último.

Detente por un reloj, no por un contador

Toma el plazo una sola vez, antes de la primera petición, a partir de un reloj monótono — uno que no puede retroceder cuando la máquina corrige su hora — y compáralo con él al principio de cada vuelta. Todo lo demás en el bucle queda entonces libre para cambiar sin cambiar cuánto dura la espera: puedes ampliar el intervalo, reintentar un rechazo, o añadir un segundo filtro, y noventa segundos siguen siendo noventa segundos.

Cuánto es suficiente es una pregunta sobre el remitente, no sobre ti. El correo que una máquina genera como respuesta a un formulario suele entregarse en segundos, con un solo dígito; una cola con trabajo acumulado, un receptor que aplica greylisting, o un envío por lotes cada hora son un orden de magnitud distinto, y ningún intervalo que elijas hace que llegue antes.

Qué estás esperandoUn plazo honestoQué hacer cuando se agota
Un correo de registro o verificación, dentro de una pruebaDe 60 a 120 segundosHaz fallar la prueba e imprime la dirección. Nueve de cada diez veces el buzón está vacío porque el formulario rechazó la dirección, y la dirección es lo primero que necesita ver quien lea el log.
Un restablecimiento de contraseña que alguien acaba de pedirDe 30 a 60 segundosDile que no ha llegado y ofrécete a enviarlo otra vez. No sigas dando vueltas detrás de una pantalla en silencio: te va a pedir uno segundo de todas formas, y ahora hay dos códigos.
Un agente que termina un registro por su cuentaDos o tres esperas del lado del servidor, así que de 50 a 75 segundosDilo en la respuesta. «Sin correo de confirmación después de un minuto» es un resultado sobre el que el agente puede actuar; una llamada a herramienta que nunca vuelve no lo es.
Un boletín, un recibo, cualquier cosa por lotesMinutos — o no esperes en absolutoHaz sondeo según un horario en su lugar, y deja que el proceso termine. Algo que se queda en un socket durante diez minutos es algo que un proxy, un runner o un límite de contenedor van a matar.

El plazo es también el lugar honesto donde poner tu mensaje de error. «Nada que coincidiera con “Confirma tu correo” llegó a signup-42@grabmail.io en 90 s» nombra la dirección, el filtro y el presupuesto, que son tres de las cuatro cosas que hacen falta para averiguar qué pasó. La cuarta — qué llegó — también merece la pena imprimirla: una lista de los asuntos que el bucle vio y rechazó convierte «es inestable» en «cambió la línea de asunto» en una sola lectura.

Con qué frecuencia preguntar, y cuándo ampliar

El mínimo es lo que el servicio permita, y aquí es una petición por segundo, por dirección. Eso no es una forma de desanimarte — hacer sondeo una vez por segundo es el patrón previsto, no hay cuota diaria, ni cuota mensual, ni crédito de ráfaga que gestionar — pero es un mínimo, y un bucle que pregunta dos veces dentro del mismo segundo recibe un 429 en la segunda, no una respuesta más rápida.

Un intervalo fijo
Un segundo, siempre, hasta el plazo. Perfectamente válido para una espera que va a terminar en diez segundos, y la opción por defecto correcta para una sola prueba en un solo runner. Su único defecto es que sigue preguntando al mismo ritmo mucho después de que sea obvio que el correo no va a llegar.
Un intervalo que se amplía
Un segundo mientras el mensaje probablemente todavía está en camino, y luego se duplica — dos, cuatro, ocho — con un techo. Cuesta algo de latencia en un mensaje que llega tarde, y ahorra la mayoría de las peticiones en una espera que de todas formas iba a fallar. Ponle un techo: un intervalo que se duplica sin techo se pasa la segunda mitad de un plazo de dos minutos dormido.
Jitter, que se suma y nunca se resta
Separa las vueltas con una fracción al azar para que veinte runners dejen de preguntar en el mismo instante. La receta habitual — un valor al azar entre cero y el intervalo — no funciona aquí, porque la mitad de ese rango cae por debajo del mínimo de un segundo. Añade la aleatoriedad por encima en su lugar: el intervalo es un mínimo, y el jitter solo consigue retrasar una vuelta, nunca adelantarla.
Una pausa que no te toca elegir a ti
Cuando la respuesta es un 429, el intervalo es lo que diga Retry-After, y la vuelta que fue rechazada no cuenta como un intento. Cuéntala como uno y un bucle al que se le está limitando la velocidad se va a pasar todo el plazo acumulando rechazos sin llegar nunca a leer el buzón.

Quince segundos a un segundo, y luego duplicando hasta un techo de ocho, con jitter por encima, hace que casi cualquier espera de esta guía quepa en seis líneas:

el intervalo, por separado
def delay(attempt: int) -> float:
    # One second while the message is probably still in flight, then
    # wider. Never below a second: the list endpoint allows one call
    # per second, per address, so jitter is added and never taken off.
    step = 1.0 if attempt < 15 else min(8.0, 2.0 ** (attempt - 14))
    return step + random.uniform(0.0, step / 2)

El exponente está desplazado para que la ampliación empiece después del tramo fijo, y no desde la primera vuelta. Sin ese desplazamiento, el intervalo ya habría llegado a los ocho segundos para cuando llega un correo de registro lento, y una espera que debería haber tardado doce segundos tarda veinte.

Nada de esto se aplica a la primera petición. Pregunta de inmediato, antes de dormir nada: un mensaje que ya estaba en el buzón cuando empezó el bucle — el caso normal para cualquier cosa que se disparó antes de que comenzara la espera — no debería costar ni un segundo de latencia notarlo.

Leer un rechazo

Toda respuesta del endpoint de listado es JSON, y las que no son un buzón comparten una misma forma: un slug error, que es estable y es sobre lo que hay que ramificar, y un message, que es texto libre y puede reformularse en cualquier momento. Un rechazo por ir demasiado rápido lleva además una cabecera:

el aspecto de un rechazo
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json; charset=utf-8

{"error":"rate_limited","message":"one request per second, per address"}

Retry-After viene en segundos enteros, y es la cifra real — sacada de cuánto le queda en realidad al presupuesto de esta dirección, no de una constante de la documentación. Dormir exactamente eso es a la vez lo más educado y lo más rápido que se puede hacer: un sueño más corto se vuelve a rechazar, uno más largo es tiempo que regalas. Esto es todo lo que puede encontrarse un bucle de sondeo, y lo que cada respuesta le está pidiendo en realidad que haga.

Qué devuelveQué significaQué debería hacer el bucle
200 con count: 0El buzón existe y está vacío. Es la respuesta normal durante casi toda una espera.Sigue esperando. No es un error, y nunca lo llega a ser.
429rate_limitedDemasiado rápido: una segunda petición de listado dentro del mismo segundo para esta dirección, o más de 1,200 peticiones en un minuto desde este origen.Duerme el número de segundos que indique Retry-After, y vuelve a preguntar. No cuentes el rechazo como un intento.
404unknown_domainLa parte que va después de la @ no está alojada aquí. Casi siempre es una errata, o un dominio cuyo registro MX nunca se apuntó hacia aquí.Detente. Ninguna cantidad de espera arregla un dominio. Imprime la dirección que te dieron.
400invalid_addressFalta el parámetro address, mide más de 320 caracteres, o no tiene la forma nombre@dominio.Detente. Es un fallo de quien llama, y va a ser el mismo fallo en cada vuelta.
400bad_cursorEl valor de before no tiene ni de lejos la forma de un id de mensaje. Un id con buen formato pero caducado no es este error: responde 200 con una página vacía.Deja de paginar y empieza de nuevo desde la primera página.
404not_found, de un solo mensajeEse id no está en ese buzón — o lo estuvo, y desde entonces ha caducado o se ha eliminado.Trátalo como desaparecido, no como tardío. Un id que viste en un listado hace segundos no va a volver.
500storage_failedAlgo falló de nuestro lado al leer el buzón.Vuelve a preguntar, pero deja que el plazo mande y no preguntes más rápido de lo habitual.

Dos de esos siete significan detente, y son los dos que merece la pena señalar bien alto. Un bucle que trata unknown_domain como «todavía no» se pasa noventa segundos enteros demostrando algo que el servicio ya le dijo en los primeros cuarenta milisegundos.

Qué mensaje es el tuyo

Un buzón no es una cola, y lo más nuevo que hay en él no es necesariamente lo que estás esperando. En un dominio público, cualquiera que adivine la dirección puede enviarle correo; en una suite de pruebas la misma dirección se reutiliza a menudo entre ejecuciones; y un solo registro suele enviar dos mensajes — uno de bienvenida y uno de confirmación — de los cuales solo uno lleva el código. La solución es una marca de agua, y hay que tomarla antes de lo que provoca el correo.

  1. Antes de enviar el formulario, lista el buzón con limit=1 y guarda el id del mensaje más nuevo, o nada en absoluto si está vacío. Ese id es la marca de agua.
  2. Haz lo que sea — envía el formulario, llama al endpoint, haz clic en el botón.
  3. Haz sondeo sobre la lista. Los mensajes vuelven del más nuevo al más viejo, así que baja desde arriba y detente en el momento en que llegues a la marca de agua: todo lo que hay de ahí para abajo es más viejo que tu acción y se puede ignorar sin leerlo.
  4. Filtra lo que quede por encima por el remitente, el asunto, o ambos. Normalmente basta con una subcadena, y debería ser la parte que no se va a traducir — una prueba que busca «Confirma tu correo» falla el día en que la cuenta bajo prueba cambia a otro idioma.
  5. Solo entonces, ábrelo. El listado lleva un preview corto y no el cuerpo, y el código que buscas muy a menudo está más allá del final de ese fragmento. Una petición más consigue el mensaje completo, y se cobra contra un presupuesto distinto y mucho más generoso que el de la lista.

En una shell, esas dos lecturas se ven así — primero la marca de agua, luego el sondeo:

shell
curl -sG https://grabmail.io/api/v1/mailbox \
  --data-urlencode "address=signup-42@grabmail.io" --data-urlencode "limit=1"
curl -sG https://grabmail.io/api/v1/mailbox \
  --data-urlencode "address=signup-42@grabmail.io" --data-urlencode "limit=25"

Las dos peticiones nombran la dirección completa, porque aquí la dirección es el buzón: no hay sesión, no hay cursor guardado en tu nombre, y no hay nada de una llamada que la siguiente recuerde. Es también el motivo por el que una dirección se puede observar de forma segura desde dos sitios a la vez — leer no consume nada, así que dos bucles sobre el mismo buzón ven los dos todos los mensajes, y ninguno puede quitarle uno al otro por debajo.

Actuar exactamente una vez

Un sondeo que se reintenta puede ver el mismo mensaje dos veces, y no es un evento raro: el servidor responde, la conexión se cae antes de que el cuerpo te llegue, tu cliente HTTP reintenta, y la segunda respuesta contiene el mensaje que la primera ya llevaba. Si lo que haces con un mensaje es hacer clic en un enlace, confirmar un pago o publicar en un canal, hacerlo dos veces es un fallo con consecuencias fuera de tu proceso.

Guarda los ids que ya has gestionado
Un conjunto de ids en memoria basta para una espera que nace y muere dentro de una sola función. Para cualquier cosa que tenga que sobrevivir a un reinicio — un buzón que vacía una tarea programada, un agente que va procesando una acumulación — hay que dejarlo escrito en algún sitio que sobreviva junto con ella.
Eliminar es idempotente
Eliminar un mensaje responde 200 tanto la segunda vez como la primera, así que un borrado reintentado nunca parece un fallo y nunca necesita un caso especial. Elimina después de haber actuado, no antes: un fallo entre las dos cosas te cuesta entonces una relectura, que es recuperable, en vez del mensaje, que no lo es.
El id de aquí no es el Message-ID del remitente
El id de la API es nuestro: está limitado a un buzón, y deja de existir cuando el mensaje caduca. La cabecera Message-ID es del remitente, viaja con el mensaje, y es la que quieres si estás emparejando el mismo correo entre dos sistemas — la guía sobre las cabeceras dice dónde encontrarla.

Nada de esto hace falta para una prueba que espera un código y luego tira el buzón. Todo esto hace falta en el momento en que un bucle corre sin supervisión, porque el fallo que evita no parece un fallo: parece que el trabajo se hizo, dos veces, correctamente.

Cuando la espera le toca al servidor

Hay un sitio aquí donde no escribes tú el bucle, y existe para quienes no se pueden permitir uno. Un agente de IA paga por cada turno que dedica a comprobar, así que una herramienta que responde «todavía nada» nueve veces son nueve turnos de nada. El wait_for_message del servidor MCP mantiene la petición abierta en su lugar, hace el sondeo de nuestro lado, y responde una sola vez — con el mensaje, o con una simple afirmación de que esperó y no llegó nada.

una sola llamada, hasta 25 segundos de espera
$ curl -sX POST https://grabmail.io/mcp -H "Content-Type: application/json" -d '{
  "jsonrpc":"2.0","id":1,"method":"tools/call",
  "params":{"name":"wait_for_message","arguments":{
    "address":"signup-42@grabmail.io","subject_contains":"code",
    "timeout_seconds":25}}}'

Merece la pena saber cuatro cosas sobre esto antes de construir algo encima.

Espera como máximo 25 segundos
timeout_seconds puede pedir menos, nunca más. El techo no es arbitrario: cada uno que espera es un worker que no hace nada más que dormir, y una petición mantenida abierta durante minutos es una petición que muere en el timeout de algún proxy mucho antes de volver.
Filtra a la entrada
from_contains, subject_contains y since_id son las mismas tres decisiones que en la sección anterior, tomadas en el servidor. since_id es la marca de agua, e importa aquí más que en ningún otro sitio: sin ella la llamada vuelve de inmediato con lo que fuera que ya estaba en el buzón.
Un timeout es una respuesta, no un error
Cuando no llega nada, devuelve timed_out activado, junto con cuánto tiempo esperó en realidad, y dice básicamente que volver a llamar es la forma de seguir esperando. Dos o tres llamadas son una espera normal para un correo de registro: eso es el bucle, y son tres turnos en lugar de noventa.
Hay 8 plazas de espera, y no hay cola
Cuando todas están ocupadas, la llamada vuelve al instante y lo dice, en vez de ponerse a la cola detrás de otros siete agentes. Es el fallo correcto: a un agente al que se le dice «demasiadas esperas en curso» puede listar el buzón y seguir adelante, mientras que un agente sentado en una cola solo puede esperar sentado.

El límite por dirección sigue aplicándose dentro de esto — nuestro bucle está tan limitado en frecuencia como el tuyo, así que una espera del lado del servidor no es una forma de saltarse el mínimo, solo una forma de dejar de pagarlo en turnos. Para una suite de pruebas nada de esto merece la molestia: una prueba ya es un proceso al que se le permite dormir, y un bucle en el lenguaje en el que está escrita la prueba es mucho más fácil de depurar que uno remoto. La guía de MCP cubre el resto de las herramientas.

El bucle completo, de una vez

Todo lo de arriba, en un solo archivo: un plazo a partir de un reloj monótono, un intervalo que se amplía con jitter de un solo lado, Retry-After respetado y no contado como intento, una marca de agua para decidir qué es nuevo, un filtro sobre el asunto, y una petición extra para obtener el mensaje que el listado solo muestra en vista previa.

una espera que aguanta
import random
import time
import requests

API     = "https://grabmail.io/api/v1"
ADDRESS = "signup-42@grabmail.io"


def delay(attempt: int) -> float:
    # One second while the message is probably still in flight, then
    # wider. Never below a second: the list endpoint allows one call
    # per second, per address, so jitter is added and never taken off.
    step = 1.0 if attempt < 15 else min(8.0, 2.0 ** (attempt - 14))
    return step + random.uniform(0.0, step / 2)


def watermark(s):
    # Read this BEFORE the form is submitted. Every id above it
    # afterwards is mail that arrived because of what you did.
    r = s.get(f"{API}/mailbox", params={"address": ADDRESS, "limit": 1})
    r.raise_for_status()
    seen = r.json()["messages"]
    return seen[0]["id"] if seen else None


def wait_for(s, subject, since, timeout=120.0):
    deadline = time.monotonic() + timeout
    attempt  = 0
    rejected = set()

    while time.monotonic() < deadline:
        r = s.get(f"{API}/mailbox", params={"address": ADDRESS, "limit": 25})

        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", "1")))
            continue                      # refused, so it was not an attempt
        if r.status_code == 200:
            for m in r.json()["messages"]:        # newest first
                if m["id"] == since:
                    break                 # older than the watermark
                if subject.lower() in m["subject"].lower():
                    full = s.get(f"{API}/message/{m['id']}",
                                 params={"mailbox": ADDRESS})
                    full.raise_for_status()
                    return full.json()
                rejected.add(m["subject"])
        elif r.status_code < 500:
            raise RuntimeError(r.json().get("error", r.status_code))
        # a 5xx falls through: transient, and the deadline still governs

        time.sleep(delay(attempt))
        attempt += 1

    raise TimeoutError(
        f"nothing matching {subject!r} at {ADDRESS} in {timeout:.0f}s; "
        f"saw {sorted(rejected) or 'nothing at all'}")

Son, a propósito, cincuenta y tantas líneas de librería estándar y un cliente HTTP. No hay nada que instalar, nada que configurar, y ningún secreto en ninguna parte — que es la idea: la misma forma se traslada sin cambios a Node, a un script de shell, o a lo que sea que tu framework de pruebas ya use para hacer peticiones.

  1. Lee la marca de agua antes de la acción que provoca el correo, nunca después.
  2. Pregunta una vez de inmediato, y solo entonces duerme. Nunca duermas primero.
  3. Toma el plazo de un reloj monótono, y compruébalo al principio de cada vuelta.
  4. Mantén el intervalo en un segundo por dirección o más, y añade el jitter solo hacia arriba.
  5. Duerme exactamente lo que diga Retry-After, y no cuentes un rechazo como un intento.
  6. Ramifica según el slug error: unknown_domain e invalid_address significan detente, no esperes.
  7. Compara por remitente o asunto, y deja de recorrer la lista cuando llegues a la marca de agua.
  8. Abre el mensaje antes de analizarlo — el listado lleva una vista previa, no el cuerpo.
  9. Falla mostrando la dirección, el filtro, el presupuesto, y los asuntos que el bucle rechazó.

Nueve reglas, y ocho de ellas existen por un fallo que alguien tuvo que reconstruir a partir de un log. La que no trata de un fallo es la segunda: preguntar una vez antes de la primera pausa es lo que hace que esperar un mensaje que ya ha llegado tome cuatro milisegundos en lugar de un segundo — algo que, a lo largo de una suite de doscientas pruebas, son tres minutos de reloj real que después nadie tiene que explicar.

Preguntas

¿Tiene GrabMail un webhook?

No, y no es un hueco a la espera de llenarse. El servicio recibe correo y lo expone por HTTP sin clave: no hay una cuenta detrás de una dirección pública a la que enganchar un callback, ni una cola que retenga una entrega que tu endpoint rechazó. Si tu flujo de trabajo de verdad no puede hacer sondeo, la página de comparación nombra los servicios que sí ofrecen uno.

¿Con qué frecuencia puedo hacer sondeo sobre una dirección?

Una vez por segundo, por dirección — y eso es el patrón previsto, no el límite extremo. No hay cuota diaria, ni cuota mensual, ni crédito de ráfaga que gestionar. Veinte buzones con sondeo una vez por segundo desde un solo runner es un uso normal; el único otro techo son 1,200 peticiones por minuto desde un solo origen, que son exactamente esos veinte y no uno más.

¿Por qué mi bucle devolvió un mensaje de una ejecución de pruebas anterior?

Porque tomó la primera entrada de la lista sin preguntar cuándo llegó. Un buzón conserva lo que se le ha enviado durante 5 días, y una dirección reutilizada está llena de la ejecución anterior. Lee el id más reciente antes de provocar el correo, e ignora todo lo que quede desde ese id hacia abajo — o elimina el contenido del buzón al principio de la prueba, que es una petición por mensaje y elimina la ambigüedad por completo.

¿Un buzón vacío es un 404?

No. Un buzón vacío es 200 con count: 0 y una lista vacía, a propósito, para que un bucle de sondeo nunca tenga que tratar «todavía nada» como caso especial. Un 404 del endpoint de listado significa que el dominio no está alojado aquí; un 404 de un solo mensaje significa que ese id no está en ese buzón, o ha caducado.

¿Cuánto debería esperar un correo de verificación?

De sesenta a ciento veinte segundos en una prueba automatizada, de treinta a sesenta para una persona esperando delante de una pantalla. La mayoría del correo generado por máquina llega en segundos de un solo dígito; la cola larga pertenece a la cola del remitente, no a la entrega. Si rutinariamente se acerca a tu plazo, un plazo más largo no es la respuesta — algo más está fallando.

¿Pueden dos procesos hacer sondeo sobre la misma dirección a la vez?

Sí. Leer no consume nada, así que los dos ven todos los mensajes y ninguno le oculta correo al otro. Eso sí, comparten el presupuesto de una petición por segundo para esa dirección, así que dos bucles preguntando cada segundo se verán rechazados más o menos la mitad de las veces cada uno: dale a cada uno dos segundos, o deja que uno haga el sondeo y le pase los resultados al otro.

¿Debería hacer sondeo, o usar la herramienta de espera por MCP?

Haz sondeo si estás escribiendo una prueba o un script: un proceso al que se le permite dormir debería dormir, y un bucle en tu propio lenguaje es más fácil de depurar que uno remoto. Usa wait_for_message cuando quien llama paga por turno en lugar de por segundo, que en la práctica significa un agente de IA. Espera hasta 25 segundos por llamada, filtra por remitente y asunto, y devuelve un simple timeout sobre el que puedes sencillamente volver a llamar.

Pruébalo mientras está reciente

Una dirección lleva un clic, sin cuenta y sin tarjeta. Todo lo de esta guía funciona con ella de inmediato.

Bienvenido de nuevo

Tus buzones y tus dominios, en un solo lugar.