Saltar al contenido principal

Webhook de email entrante

Convierte los emails de tu propio buzón
en JSON analizado en tu servidor.

Los servicios de parsing quieren tus registros MX o una dirección de reenvío. Conecta en cambio el buzón que ya usas: cada email nuevo llega a tu URL con su asunto, su HTML, sus adjuntos y su hilo, y respondes desde la misma dirección.

¿Con prisa? Resume este artículo con una IA

La respuesta corta

¿Cómo enviar los emails entrantes a un webhook?

Conecta el propio buzón. Unlimited Messaging inicia sesión en tu buzón actual por IMAP (Gmail con una contraseña de aplicación, tu propio dominio, cualquier proveedor IMAP), vigila su bandeja de entrada y envía cada email nuevo a tu URL como un evento JSON firmado, message.received, con asunto, texto, HTML, adjuntos e identificadores de hilo. Tus MX se quedan donde están, a diferencia de SendGrid Inbound Parse. Los webhooks están en los planes de pago Medium y Max.

Los servicios de parsing se colocan delante del correo. SendGrid te pide apuntar el MX de un nombre de host dedicado a mx.sendgrid.net, y luego envía multipart/form-data (o el MIME original) a tu URL. Postmark da a cada servidor una dirección como [email protected], a la que reenvías desde Gmail o enrutas con inbound domain forwarding, y su procesamiento de entrada viene con el plan Pro, desde 16,50 $ al mes. En ambos casos cambia la dirección a la que escriben tus clientes, o su correo pasa por un reenvío.

Unlimited Messaging lee el buzón al que tus clientes ya escriben. Una pasarela mantiene una conexión IMAP IDLE en su bandeja de entrada, en solo lectura, así que un email nuevo se recoge en cuanto el servidor lo anuncia. El email se guarda, sus adjuntos también (hasta 20 por email), y tu webhook recibe un único cuerpo JSON: externalId es el Message-ID, conversation.id la raíz de su hilo y replyTo el mensaje al que responde.

La respuesta cierra el círculo: POST /message con replyToMessageId envía desde el mismo buzón, por su propio servidor SMTP, con Re: y las cabeceras In-Reply-To y References, y guarda una copia en Enviados. El hilo sigue siendo un solo hilo en el correo de tu cliente.

Antes de conectar: configura Gmail por IMAP con una contraseña de aplicación y por qué importa enviar desde tu propio buzón.

Ilustración: una cuadrícula de iconos pequeños con uno resaltado, con el titular «Tu buzón, en JSON».

Los eventos

¿Qué contiene el webhook de email entrante?

El email comparte el sobre de todos los canales y añade tres campos: subject, html y attachments.

  1. message.received

    Llegó un email nuevo a la bandeja de entrada. content es el cuerpo de texto, o el HTML convertido a texto cuando el email no tiene parte de texto; html es el cuerpo tal como llegó, no fiable, que hay que sanear antes de mostrarlo.

  2. attachments

    Cada archivo en orden, hasta 20 por email: url (la copia guardada), filename, mimeType, size y contentId para una imagen incrustada que el HTML referencia como cid:. El primero también se copia en mediaUrl.

  3. Hilos

    externalId es el Message-ID RFC 5322 sin corchetes angulares. conversation.id es la raíz del hilo (primer elemento de References, si no In-Reply-To, si no el propio email), así que todos los emails de un hilo lo comparten.

  4. message.sent y message.status

    Tus respuestas se anuncian como message.sent, y un rebote definitivo pasa el mensaje a UNDELIVERABLE (message.status). La dirección rebotada queda en pausa 30 días en ese buzón para proteger su reputación.

  5. account.status_changed

    El buzón se desconectó, por ejemplo porque el servidor rechazó las credenciales tras un cambio de contraseña. Suscríbete para que un buzón mudo no pase desapercibido.

POST https://example.com/webhooks/umJSON
{
  "type": "message.received",
  "eventId": "whd_9a4f2e61-7c3b-4d85-b0e9-5f1a8c2d7e34",
  "occurredAt": "2026-11-09T14:05:12.000Z",
  "data": {
    "message": {
      "id": "msg_3f8a1c2e-9d4b-4e7a-8b6c-2d1e0f9a8b7c",
      "externalId": "[email protected]",
      "direction": "IN",
      "fromMe": false,
      "content": "Hi, please find the signed quote attached. Claire",
      "sentAt": "2026-11-09T14:05:12.000Z",
      "receivedAt": "2026-11-09T14:05:12.000Z",
      "sender": "[email protected]",
      "senderId": "[email protected]",
      "senderName": null,
      "participant": null,
      "replyTo": { "externalId": "[email protected]", "participant": null },
      "mediaUrl": "https://api.unlimitedmessaging.app/message/media/stored/4c1d...",
      "mediaType": "document",
      "mimeType": "application/pdf",
      "mediaFilename": "quote-1042-signed.pdf",
      "transcript": null,
      "subject": "Re: Quote #1042",
      "html": "<p>Hi, please find the signed quote attached.</p><p>Claire</p>",
      "attachments": [
        {
          "url": "https://api.unlimitedmessaging.app/message/media/stored/4c1d...",
          "filename": "quote-1042-signed.pdf",
          "mimeType": "application/pdf",
          "size": 184320,
          "contentId": null
        }
      ]
    },
    "channel": {
      "type": "EMAIL",
      "account": { "id": "acc_8b2e4f6a-1c3d-4e5f-a7b9-0c1d2e3f4a5b", "name": "[email protected]" },
      "conversation": {
        "id": "[email protected]",
        "isGroup": false,
        "address": "[email protected]"
      }
    }
  }
}
Un cuerpo message.received real para un email, construido a partir del esquema publicado (URLs de archivos acortadas). sender es la dirección From; senderName no se rellena en email.

Puesta en marcha

¿Cómo analizar emails entrantes en Next.js?

Conecta el buzón, registra un endpoint, verifica cada POST. Ni parser MIME ni librería IMAP en tu código.

  1. 01

    Conecta tu buzón

    En la aplicación web, añade una cuenta de email con la dirección y su contraseña (una contraseña de aplicación para Gmail). Los ajustes IMAP y SMTP se detectan a partir de la dirección.

  2. 02

    Registra el endpoint

    POST /webhooks con tu URL y tus eventTypes, o añádelo en la página Webhooks del panel. El secreto (whsec_...) se muestra una sola vez.

  3. 03

    Verifica, guarda, responde

    Comprueba la cabecera webhook-signature, haz un upsert por eventId, descarga los adjuntos desde su url cuando los necesites y responde con replyToMessageId.

POST /webhookscurl
curl https://api.unlimitedmessaging.app/webhooks \
  -H "Authorization: Bearer $UM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/um",
    "eventTypes": ["message.received", "account.status_changed"],
    "description": "Inbound email"
  }'

# The response carries "secret": "whsec_..." once. Store it as UM_WEBHOOK_SECRET.
Añade accountIds para limitar el endpoint a un buzón, por ejemplo soporte@ pero no facturacion@. Sin él, cubre todas las cuentas.
app/api/webhooks/um/route.tsNext.js route handler
// app/api/webhooks/um/route.ts (Next.js App Router)
import crypto from 'node:crypto';
import { UnlimitedMessagingApiClient } from '@unlimited-messaging/sdk';

const um = new UnlimitedMessagingApiClient({ token: process.env.UM_API_KEY! });
const key = Buffer.from(process.env.UM_WEBHOOK_SECRET!.replace(/^whsec_/, ''), 'base64');

// Standard Webhooks: HMAC-SHA256 of "id.timestamp.rawBody", base64.
function verify(raw: string, id: string, ts: string, header: string) {
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${raw}`).digest('base64');
  return header.split(' ').some((entry) => {
    const sig = entry.startsWith('v1,') ? entry.slice(3) : '';
    return sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
}

export async function POST(req: Request) {
  const raw = await req.text(); // the raw body, before any JSON parsing
  const h = req.headers;
  if (!verify(raw, h.get('webhook-id') ?? '', h.get('webhook-timestamp') ?? '',
    h.get('webhook-signature') ?? '')) {
    return new Response('bad signature', { status: 401 });
  }

  const event = JSON.parse(raw);
  if (event.type === 'message.received' && event.data.channel.type === 'EMAIL') {
    const m = event.data.message;
    const thread = event.data.channel.conversation.id; // same id for the whole thread
    const files = m.attachments ?? []; // [{ url, filename, mimeType, size, contentId }]
    await saveTicket({ eventId: event.eventId, thread, from: m.sender, subject: m.subject,
      text: m.content, files }); // upsert on eventId: delivery is at least once

    // Answer in the same thread, from the same mailbox.
    await um.messages.messageSend({ replyToMessageId: m.id, text: 'Thanks, received. We reply within a day.' });
  }
  return new Response(null, { status: 200 }); // within 10 s; queue slow work
}
req.text() conserva el cuerpo original sobre el que se calcula la firma (Standard Webhooks, HMAC-SHA256 sobre id.timestamp.rawBody). saveTicket es tuyo. La respuesta sale con Re: y las cabeceras de hilo, desde el mismo buzón.

Vibe coding

¿Programas con Cursor, Claude Code, Lovable o Bolt? Pega esto

Tu editor con IA recibe la ruta, la verificación de firma y los campos de email que necesita, sin inventar un parser MIME.

Prompt para copiar
Add an Unlimited Messaging webhook to this app so incoming emails from my own mailbox become support tickets.

1. Create a POST route (Next.js: app/api/webhooks/um/route.ts) that reads the RAW body with req.text().
2. Verify the Standard Webhooks signature: key = base64-decode(UM_WEBHOOK_SECRET without "whsec_"); expected = base64(HMAC-SHA256(key, `${webhook-id}.${webhook-timestamp}.${rawBody}`)); accept if any space-separated entry of the webhook-signature header equals `v1,${expected}` (constant-time compare); reject timestamps more than 5 minutes off; return 401 otherwise.
3. On type "message.received" with data.channel.type "EMAIL": upsert a ticket keyed on body.eventId with data.channel.conversation.id (thread id), data.message.sender, data.message.subject, data.message.content (text) and data.message.attachments ([{ url, filename, mimeType, size, contentId }]). Treat data.message.html as untrusted: sanitise before rendering.
4. To answer, install @unlimited-messaging/sdk, read UM_API_KEY from env, and call `client.messages.messageSend({ replyToMessageId: data.message.id, text })`: it replies in the same thread from the same mailbox.
5. Return 200 within 10 seconds; queue slow work.
6. Put UM_API_KEY and UM_WEBHOOK_SECRET in .env and .env.example.

Docs: https://docs.unlimitedmessaging.app (Webhooks). Register with POST https://api.unlimitedmessaging.app/webhooks and eventTypes ["message.received"].

Lo que se construye

¿Qué puedes construir con el email entrante?

Todo lo que hoy consiste en que alguien copie un email en otra herramienta.

Un helpdesk en soporte@

Un ticket por conversation.id, cada respuesta añadida, contestaciones enviadas desde soporte@ en el mismo hilo. Ninguna dirección de ticketing que publicar.

Clasificación y respuesta automática con IA

Pasa subject y content a un LLM, etiqueta y enruta el email, o envía una primera respuesta con replyToMessageId y deja que una persona tome el relevo.

  1. Facturas y documentos entrantes

    Descarga el PDF desde attachments[].url y envíalo a tu contabilidad o a tu OCR.

  2. Captación de leads

    Convierte las notificaciones de formularios o los emails directos en leads del CRM, por dirección del remitente.

  3. Una sola bandeja con WhatsApp y SMS

    El mismo handler recibe los eventos de WhatsApp y SMS: solo cambia data.channel.type.

  4. Flujos sin código

    Apunta el webhook a n8n, Make o Zapier y ramifica según el asunto o el remitente, sin servidor.

La tabla

Email entrante a webhook: SendGrid, Postmark y Unlimited Messaging

Los servicios de parsing reciben el correo de una dirección o un nombre de host nuevos. Unlimited Messaging lee el buzón que ya tienes.

Email entrante a webhook: SendGrid, Postmark y Unlimited Messaging
SendGrid Inbound ParseMX en un nombre de host dedicadoPostmark InboundDirección o dominio de entradaUnlimited MessagingTu buzón por IMAP, planes de pago
Dirección que recibeCualquier dirección de un nombre de host dedicado al parsing[email protected], o un reenvío hacia ellaTu dirección de siempre
Cambio de DNSMX hacia mx.sendgrid.netNinguno con reenvío; DNS para inbound domain forwardingNinguno
Formato del cuerpomultipart/form-data, o el mensaje MIME originalJSON; autenticación HTTP basic en la URL como protecciónJSON, firmado según Standard Webhooks
Identificadores de hiloEn las cabeceras que analizasEn las cabeceras analizadasconversation.id (raíz del hilo) y replyTo, listos para usar
Responder desde la misma direcciónCon el envío de SendGrid, configurado aparteCon el envío de Postmark, configurado apartereplyToMessageId, por el SMTP de tu buzón, copia en Enviados
PrecioIncluido en un plan de SendGridEntrada en el plan Pro, desde 16,50 $ al mesWebhooks con Medium o Max; sin coste por email
WhatsApp y SMS en el mismo webhookNoNoSí, el mismo sobre

Comprobado el 9 de noviembre de 2026 en la guía de configuración del Inbound Parse de SendGrid, la guía de servidores de entrada y la página de precios de Postmark (enlaces en las fuentes).

La letra pequeña

¿Qué límites tiene una pasarela de buzón a webhook?

Leer tu propio buzón elimina el trabajo de DNS. Trae consigo las reglas de ese buzón.

  1. Los webhooks son de pago

    En el plan Free, los emails recibidos aparecen en la bandeja de la aplicación web, agrupados por hilo. Enviarlos a tu servidor requiere Medium (19 € al mes, o 17 € al mes con pago anual) o Max.

  2. Solo la bandeja de entrada

    La pasarela vigila INBOX. Un email que tu proveedor marca como spam, o que un filtro mueve a otra carpeta, nunca se ve.

  3. Todavía sin Outlook ni Microsoft 365

    Outlook, Hotmail y Microsoft 365 exigen OAuth, previsto pero aún no disponible. Gmail funciona con una contraseña de aplicación, que requiere la verificación en dos pasos en la cuenta de Google.

  4. Hasta 20 adjuntos por email

    Los archivos a partir del 21 no se guardan. El HTML se pasa tal como llegó: sanéalo antes de mostrarlo en cualquier sitio.

  5. Al menos una vez, sin orden garantizado

    El mismo evento puede llegar dos veces, y los eventos no llegan en el orden en que ocurrieron: haz un upsert por eventId y ordena por sentAt.

FAQ

Preguntas sobre los webhooks de email entrante

Ilian, ingeniero de software senior

Escrito por

· Ingeniero de software senior

Ingeniero de software senior, funda y dirige varias empresas tecnológicas. Diseña productos en torno a las API de mensajería, la automatización y la integración de la IA, y comparte aquí lo que aprende construyendo.

Ver el perfil de LinkedInActualizado el

Tu bandeja de entrada, en JSON limpio.

Crea una cuenta, conecta tu buzón y activa los webhooks con Medium o Max. Enviar emails sigue siendo gratis en todos los planes.