Referencia
Peticiones de webhook
El formato exacto de las peticiones de webhook de Emailit, desde el método y las cabeceras hasta el array JSON de eventos, además del orden, los duplicados y cómo responder.
En esta página se describe lo que llega a tu endpoint: la petición HTTP, sus cabeceras, el cuerpo JSON y lo que Emailit espera como respuesta. Úsala cuando escribas o depures un controlador de webhooks.
Petición
| Propiedad | Valor |
|---|---|
| Método | POST |
| Cuerpo | Un array JSON de 1 a 100 eventos, compacto (sin espacios adicionales) |
| Tiempo de espera | 30 segundos |
| Redirecciones | No se siguen. Una respuesta 3xx cuenta como fallo. |
| Concurrencia | Una petición a la vez por webhook. Los lotes se envían uno tras otro. |
| Latencia | Emailit busca eventos nuevos cada pocos segundos, así que la mayoría de los eventos llegan en cuestión de segundos. |
POST /webhooks/emailit HTTP/1.1
Host: acme.com
Content-Type: application/json
User-Agent: Emailit-Webhooks/2.0
X-Emailit-Signature: 7242b580f77ec488f8ac21b57f092e3db61a431694f13074c7b4c85a4dd04be3
X-Emailit-Timestamp: 1790846045Cabeceras
| Cabecera | Descripción |
|---|---|
Content-Type |
Siempre application/json. |
User-Agent |
Siempre Emailit-Webhooks/2.0. |
X-Emailit-Signature |
HMAC-SHA256 codificado en hexadecimal de {timestamp}.{raw body}, con el secreto completo del webhook (incluido whsec_) como clave. Consulta Firma de las peticiones. |
X-Emailit-Timestamp |
Hora Unix, en segundos, del momento en que Emailit firmó la petición. Cada intento, incluidos los reintentos, se vuelve a firmar con una marca de tiempo nueva. |
Cuerpo
El cuerpo es siempre un array JSON, aunque contenga un solo evento. Cada elemento tiene esta forma:
| Campo | Tipo | Descripción |
|---|---|---|
event_id |
string | El ID del evento, que empieza por evt_. Un mismo evento conserva el mismo ID en todos los reintentos. Los eventos de prueba empiezan por evt_test_. |
type |
string | El tipo de evento, como email.delivered. |
object |
object | El recurso al que se refiere el evento. |
data |
object | Vuelve a contener object. data.object y el object del nivel superior son idénticos; lee el que prefieras. |
Los campos de object dependen del tipo. Los eventos de estado de email describen el email; email.clicked y email.loaded describen el clic o la carga y anidan el email; los eventos de recursos, como contact.created, describen el recurso.
Ejemplo de lote
Esta petición lleva dos eventos: una entrega y un clic posterior en un enlace del mismo email.
[
{
"event_id": "evt_2xGk7Nq1VbD5sR8tLmW3eYhC6aP",
"type": "email.delivered",
"object": {
"id": "em_2xGk7Lr3XcB8pQ1wYzK4dTfG9hJ",
"object": "email",
"from": "billing@acme.com",
"to": "ada@example.com",
"subject": "Your receipt #1042",
"status": "delivered",
"meta": { "order_id": "1042" },
"updated_at": "2026-10-01T09:14:03.512000+00:00",
"created_at": "2026-10-01T09:14:01.207000+00:00"
},
"data": {
"object": {
"id": "em_2xGk7Lr3XcB8pQ1wYzK4dTfG9hJ",
"object": "email",
"from": "billing@acme.com",
"to": "ada@example.com",
"subject": "Your receipt #1042",
"status": "delivered",
"meta": { "order_id": "1042" },
"updated_at": "2026-10-01T09:14:03.512000+00:00",
"created_at": "2026-10-01T09:14:01.207000+00:00"
}
}
},
{
"event_id": "evt_2xGkC8w2TnF4yR7bMqL1sVdJ6pE",
"type": "email.clicked",
"object": {
"id": "click_2xGkC7v5PmD3xQ9aLnK2rUcH8oW",
"object": "click",
"email_id": "em_2xGk7Lr3XcB8pQ1wYzK4dTfG9hJ",
"email": {
"id": "em_2xGk7Lr3XcB8pQ1wYzK4dTfG9hJ",
"rcpt_to": "ada@example.com",
"mail_from": "billing@acme.com",
"subject": "Your receipt #1042",
"created_at": "2026-10-01T09:14:01.207000+00:00",
"campaign": null,
"meta": { "order_id": "1042" }
},
"link": {
"id": "link_2xGk7Mb6QwE1zS8cNpT4vXfK3gB",
"url": "https://acme.com/orders/1042"
},
"contact": { "id": "con_2xFh3Ka9LmP2nR6tVwB8cDqY5sZ", "email": "ada@example.com" },
"ip_address": "203.0.113.24",
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)",
"created_at": "2026-10-01T09:20:47.884000+00:00"
},
"data": {
"object": {
"id": "click_2xGkC7v5PmD3xQ9aLnK2rUcH8oW",
"object": "click",
"email_id": "em_2xGk7Lr3XcB8pQ1wYzK4dTfG9hJ",
"email": {
"id": "em_2xGk7Lr3XcB8pQ1wYzK4dTfG9hJ",
"rcpt_to": "ada@example.com",
"mail_from": "billing@acme.com",
"subject": "Your receipt #1042",
"created_at": "2026-10-01T09:14:01.207000+00:00",
"campaign": null,
"meta": { "order_id": "1042" }
},
"link": {
"id": "link_2xGk7Mb6QwE1zS8cNpT4vXfK3gB",
"url": "https://acme.com/orders/1042"
},
"contact": { "id": "con_2xFh3Ka9LmP2nR6tVwB8cDqY5sZ", "email": "ada@example.com" },
"ip_address": "203.0.113.24",
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X)",
"created_at": "2026-10-01T09:20:47.884000+00:00"
}
}
}
]Las marcas de tiempo están en formato ISO 8601 y en UTC. meta son los metadatos que adjuntaste al enviar, o null. contact es null cuando el destinatario no es uno de tus contactos, y campaign es null en el correo enviado por API y SMTP.
Orden
Dentro de una petición, los eventos se ordenan del más antiguo al más reciente. Entre peticiones no hay garantía de orden: un lote que falló se reintenta más tarde, cuando puede que ya hayan llegado eventos más recientes, y un reintento manual puede volver a enviar eventos antiguos después de otros nuevos.
No des por supuesto el orden de llegada. En los eventos de email, usa el status y el updated_at del email, y quédate con el estado más avanzado que hayas visto (por ejemplo, no sobrescribas clicked con un delivered que llegue tarde). Si tienes dudas, obtén el estado actual con Obtener un email.
Duplicados
Un mismo evento puede llegar más de una vez:
- Cuando se agota el tiempo de espera de tu endpoint o devuelve un error después de procesar parte de un lote, se reintenta el lote completo, incluidos los eventos que ya procesaste.
- Cuando usas Retry failed o reintentas una sola petición.
Guarda cada event_id que hayas procesado, con una restricción de unicidad, y omite los eventos que ya hayas visto.
Respuesta
| Tu respuesta | Resultado |
|---|---|
Cualquier 2xx en menos de 30 segundos |
Todos los eventos del lote se marcan como Delivered. El cuerpo de la respuesta se ignora. |
3xx, 4xx o 5xx |
El lote ha fallado y se reintenta. |
| Sin respuesta en 30 segundos, error de conexión, error de TLS | El lote ha fallado y se reintenta. El panel muestra el código de estado 0. |
En las peticiones fallidas, Emailit guarda los primeros 2000 caracteres del cuerpo de tu respuesta. Puedes leerlos en la pestaña Requests del webhook, así que devolver un mensaje de error breve resulta útil para depurar.
Recomendaciones:
- Verifica y confirma rápido. Comprueba la firma, pon los eventos en una cola o en una tabla y devuelve
200. Haz el trabajo lento, como llamar a otras API, en una tarea en segundo plano. - Devuelve
2xxpara los eventos que ignoras. Si no, se reintentan durante días. - Devuelve
401o403si la firma no es válida. Se reintenta como cualquier fallo, lo que te da tiempo para corregir un secreto mal configurado.
Consulta Reintentos y fallos para ver el calendario de reintentos y qué ocurre cuando un endpoint sigue fallando.