Saltar al contenido
Docs

Guía práctica

Procesar emails entrantes con webhooks

Suscribe un webhook a email.received, verifica cada petición y obtén con la API el cuerpo, las cabeceras y los adjuntos de cada mensaje recibido.

Actualizado el 1 oct 2026

Esta guía explica cómo reaccionar en tu propio código a los emails recibidos. Emailit avisa a tu endpoint con un evento email.received, y tu código obtiene el mensaje completo con la API. Los ejemplos verifican la firma, gestionan lotes de eventos, descargan los adjuntos y omiten los duplicados.

Antes de empezar

  • Los emails entrantes están configurados y un mensaje de prueba aparece en la pestaña Incoming.
  • Una clave de API con Full Access. Las claves Sending Only no pueden leer el contenido de los emails. Consulta Claves de API.
  • Un endpoint HTTPS público que acepte peticiones POST. Para el desarrollo local, usa un túnel como ngrok o Cloudflare Tunnel.

Cómo funciona el flujo

El webhook te avisa de que ha llegado un mensaje. No contiene el cuerpo ni los adjuntos, lo que mantiene pequeñas las peticiones y te permite obtener el contenido solo cuando lo necesitas.

email.received (un evento del array de la petición)
{
  "event_id": "evt_2xGk9Tb4QmF6wN2cJpR7uZsE1kD",
  "type": "email.received",
  "object": {
    "id": "em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA",
    "object": "email",
    "from": "ada@example.com",
    "to": "support@inbound.acme.com",
    "subject": "Question about order 1042",
    "created_at": "2026-10-01T09:14:05.317000+00:00"
  },
  "data": {
    "object": {
      "id": "em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA",
      "object": "email",
      "from": "ada@example.com",
      "to": "support@inbound.acme.com",
      "subject": "Question about order 1042",
      "created_at": "2026-10-01T09:14:05.317000+00:00"
    }
  }
}

Usa data.object.id con Obtener un email para obtener el mensaje analizado:

GET /v2/emails/em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA (abreviado)
{
  "object": "email",
  "id": "em_2xGk9Rz2NcV7bL4pWqH8sYdT5fA",
  "type": "inbound",
  "status": "received",
  "from": "ada@example.com",
  "to": "support@inbound.acme.com",
  "subject": "Question about order 1042",
  "message_id": "<CAF3x1@mail.example.com>",
  "headers": {
    "From": "Ada Lovelace <ada@example.com>",
    "Reply-To": "ada@example.com",
    "Date": "Thu, 01 Oct 2026 09:14:02 +0000"
  },
  "body": {
    "text": "Hi, my order hasn't arrived yet...",
    "html": "<p>Hi, my order hasn't arrived yet...</p>"
  },
  "attachments": [
    {
      "filename": "receipt.pdf",
      "content_type": "application/pdf",
      "size": 48213,
      "content_id": null,
      "content_disposition": "attachment",
      "content": "JVBERi0xLjcKJcfsj6IK..."
    }
  ]
}

El content de los adjuntos está codificado en Base64. Los nombres de las cabeceras conservan sus mayúsculas y minúsculas originales y, cuando una cabecera aparece más de una vez (como Received), solo se conserva el último valor. Si solo necesitas una parte del mensaje, usa los endpoints más específicos: Obtener el cuerpo, Listar adjuntos u Obtener el MIME en bruto para el código fuente original.

Crear el webhook

  1. Añade el webhook. Ve a Email APIWebhooks, selecciona Add webhook, introduce un nombre y la URL de tu endpoint, y selecciona Create.

  2. Copia el secreto. El cuadro de diálogo muestra una sola vez el secreto del webhook (whsec_…). Guárdalo como EMAILIT_WEBHOOK_SECRET en el entorno de tu aplicación.

  3. Suscríbete solo a email.received. En la pestaña Settings del webhook, desactiva All events, selecciona email.received en Emails y selecciona Save.

En los planes Pro, Business y Custom puedes añadir un filtro de payload para que el webhook solo reciba el correo de algunas direcciones, por ejemplo to termina en @inbound.acme.com.

Escribir el gestor

El cuerpo de cada petición es un array JSON de hasta 100 eventos. El gestor siguiente verifica la firma, recorre el array, omite todo lo que no sea email.received o que ya se haya procesado, y obtiene cada mensaje.

server.js
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const WEBHOOK_SECRET = process.env.EMAILIT_WEBHOOK_SECRET; // whsec_...
const API_KEY = process.env.EMAILIT_API_KEY; // secret_..., Full Access
const processed = new Set(); // use a database table with a unique key in production

function verify(rawBody, signature, timestamp) {
  if (!signature || !timestamp) return false;
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!(age <= 300)) return false; // 5-minute tolerance
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(signature);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

async function fetchEmail(id) {
  const res = await fetch(`https://api.emailit.com/v2/emails/${id}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });
  if (!res.ok) throw new Error(`Emailit API returned ${res.status}`);
  return res.json();
}

async function handleReceived(event) {
  const email = await fetchEmail(event.data.object.id);
  console.log(`From ${email.from} to ${email.to}: ${email.subject}`);
  console.log(email.body.text ?? email.body.html);

  for (const file of email.attachments ?? []) {
    const bytes = Buffer.from(file.content, 'base64');
    console.log(`Attachment ${file.filename} (${file.content_type}, ${bytes.length} bytes)`);
  }
}

// Keep the raw body: the signature is computed over the exact bytes.
app.post('/webhooks/emailit', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verify(req.body, req.get('X-Emailit-Signature'), req.get('X-Emailit-Timestamp'))) {
    return res.status(401).send('Invalid signature');
  }

  const events = JSON.parse(req.body.toString('utf8'));
  try {
    for (const event of events) {
      if (event.type !== 'email.received' || processed.has(event.event_id)) continue;
      await handleReceived(event);
      processed.add(event.event_id);
    }
    res.sendStatus(200);
  } catch (err) {
    console.error(err);
    res.sendStatus(500); // Emailit retries the whole batch later
  }
});

app.listen(3000);

Gestionar los reintentos y los duplicados

  • Responde en menos de 30 segundos. Emailit espera hasta 30 segundos una respuesta 2xx. Un tiempo de espera agotado, un estado distinto de 2xx o una redirección cuentan como fallo, y todo el lote se reintenta según el calendario de reintentos. Si obtener y procesar los mensajes puede tardar más, guarda los eventos en una cola, devuelve 200 y procésalos en una tarea en segundo plano.
  • Elimina los duplicados por event_id. Un lote reintentado vuelve a contener todos sus eventos, incluidos los que ya gestionaste antes del fallo. Registra cada event_id después de procesarlo y omite los ID que ya hayas visto. Cada mensaje recibido tiene además su propio ID de email, que puedes usar como segunda clave.
  • Confirma los eventos que no gestionas. Devuelve 2xx aunque un lote solo contenga tipos de eventos que ignoras. Si no, Emailit los sigue reintentando.
  • Obtén el contenido sin demora. El contenido de los mensajes se conserva durante un tiempo limitado que depende de tu plan (7 días en Pay as you go). Después, la API devuelve el email sin su cuerpo ni sus adjuntos. Consulta Retención de datos.

Enrutar los mensajes por dirección

Como se acepta cualquier parte local, puedes codificar información en la dirección y leerla después en to. Por ejemplo, envía las notificaciones con reply_to igual a reply+4821@inbound.acme.com y después dirige las respuestas a la incidencia 4821:

JavaScript
const match = email.to.match(/^reply\+(\d+)@inbound\.acme\.com$/i);
if (match) {
  await addReplyToTicket(match[1], email.body.text);
}

Comprobar que funciona

  1. Envía un mensaje a una dirección de tu subdominio de entrada.
  2. En la pestaña Requests del webhook, la petición email.received muestra Delivered.
  3. Tu aplicación registra el remitente, el asunto y los adjuntos que haya.

Si la petición muestra Attempting o Failed, selecciona View en una fila fallida para ver el código de estado y el cuerpo de la respuesta que ha devuelto tu endpoint. Consulta Reintentos y fallos.

¿Te ha resultado útil esta página?

Gracias por tu opinión.

Gracias. Leemos todos los mensajes.