Webhooks
Registra endpoints que reciben notificaciones de eventos firmadas.
Crear un webhook
Crea un endpoint de webhook en tu espacio de trabajo. Emailit envía los eventos que coinciden a la URL en lotes de hasta 100, como un array JSON firmado con el secret del webhook. Para el formato de la petición, consulta Peticiones de webhook. Requiere una clave de API con el permiso full.
/webhooksCuerpo de la petición
namestringObligatorioEl nombre del webhook. Debe ser único en el espacio de trabajo; puedes usarlo en lugar del ID en los demás endpoints de webhooks.
urlstringObligatorioEl endpoint que recibe los eventos. Se aceptan URL http y https; usa https en producción.
Emailit resuelve el nombre de host al guardar y rechaza localhost, las direcciones IP privadas, las de enlace local y otras direcciones IP reservadas. Las redirecciones no se siguen en la entrega, así que usa la URL final.
all_eventsbooleanEnvía todos los tipos de eventos, incluidos los que se añadan más adelante. Por defecto, false. Si es true, events se ignora.
enabledbooleanSi Emailit entrega eventos al webhook. Por defecto, true.
eventsstring[]Los tipos de eventos que se envían, por ejemplo ["email.delivered", "email.bounced"]. Consulta Tipos de eventos. Por defecto, [], lo que, con all_events: false, significa que el webhook no recibe nada.
Los nombres de eventos no se validan. Un tipo mal escrito se guarda, pero nunca coincide con ningún evento.
filterobject | nullEl filtro de payload. Emailit solo envía los eventos cuyo objeto cumple las reglas. Disponible en los planes Pro, Business y Custom; en Pay as you go, un filtro con reglas devuelve 403.
filter.matchstringall (por defecto) envía un evento si se cumplen todas las reglas. any lo envía si se cumple al menos una.
filter.rulesobject[]Hasta 25 reglas.
filter.rules[].fieldstringObligatorioLa ruta con puntos dentro del objeto del evento, por ejemplo to, status, meta.plan o, en los eventos de clic y de apertura, email.campaign.id. Un prefijo payload. se ignora.
filter.rules[].operatorstringObligatorioequals, not_equals, contains, not_contains, starts_with, ends_with, greater_than, less_than, is_set, is_not_set, in o not_in. Los operadores de texto comparan los valores como cadenas; greater_than y less_than comparan números.
filter.rules[].valueanyEl valor con el que se compara. Obligatorio con todos los operadores salvo is_set e is_not_set. Usa un array con in y not_in.
Devuelve
Devuelve 201 Created con el objeto de webhook, incluido el secret de firma (whsec_ seguido de 64 caracteres hexadecimales). Usa el secreto para verificar las firmas de las peticiones. Puedes volver a leerlo con Obtener un webhook y rotarlo con Rotar el secreto de firma.
| Código | Cuándo |
|---|---|
400 |
Falta name o url, la URL no es válida, no se puede resolver o apunta a una dirección no permitida, o el filtro no es válido. |
403 |
El filtro tiene reglas y tu plan no incluye filtros de webhooks. El cuerpo es {"error": "plan_required", "required_plan": "pro"}. |
409 |
Ya existe un webhook con este nombre. El cuerpo incluye en existing el id y el name del webhook existente. |
422 |
El espacio de trabajo ha alcanzado el límite de webhooks de su plan. El cuerpo incluye usage.used y usage.limit. Consulta Límites. |
{
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events",
"url": "https://api.acme.com/webhooks/emailit",
"all_events": false,
"enabled": true,
"events": ["email.delivered", "email.bounced"],
"filter": null,
"filters_allowed": true,
"last_used_at": null,
"created_at": "2026-10-01T09:41:05.302000+00:00",
"updated_at": "2026-10-01T09:41:05.302000+00:00",
"secret": "whsec_175a02aca3fb7dc3107ca21e4224a73d058cacc628356a39e020422aec3ca434"
}{
"error": "URL resolves to a private/reserved IP address"
}{
"error": "plan_required",
"required_plan": "pro"
}{
"error": "Webhook with this name already exists",
"existing": {
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events"
}
}{
"error": "Pay as you go includes 3 webhook endpoints.",
"usage": {
"used": 3,
"limit": 3
}
}{
"name": "Enterprise bounces",
"url": "https://api.acme.com/webhooks/emailit",
"events": ["email.bounced", "email.complained"],
"filter": {
"match": "all",
"rules": [
{ "field": "meta.plan", "operator": "equals", "value": "enterprise" },
{ "field": "to", "operator": "not_contains", "value": "@acme.com" }
]
}
}Obtener un webhook
Devuelve un webhook, buscado por su ID o por su nombre. Es el único endpoint de lectura que devuelve el secret de firma. Requiere una clave de API con el permiso full.
/webhooks/:idParámetros de ruta
idstringObligatorioEl ID del webhook (wh_…) o su nombre, codificado para URL.
Devuelve
Devuelve 200 OK con el objeto de webhook, incluidos secret y filters_allowed (si tu plan permite que el webhook use un filtro de payload). last_used_at es la hora de la última entrega correcta, o null si todavía no se ha entregado nada.
Devuelve 404 con error: "Webhook not found" si ningún webhook coincide.
{
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events",
"url": "https://api.acme.com/webhooks/emailit",
"all_events": false,
"enabled": true,
"events": ["email.delivered", "email.bounced"],
"filter": {
"match": "all",
"rules": [
{ "field": "meta.plan", "operator": "equals", "value": "enterprise" }
]
},
"filters_allowed": true,
"last_used_at": "2026-10-01T10:02:17.845000+00:00",
"created_at": "2026-10-01T09:41:05.302000+00:00",
"updated_at": "2026-10-01T09:41:05.302000+00:00",
"secret": "whsec_175a02aca3fb7dc3107ca21e4224a73d058cacc628356a39e020422aec3ca434"
}{
"error": "Webhook not found"
}Actualizar un webhook
Actualiza un webhook. Envía solo los campos que quieras cambiar; tienes que enviar al menos uno. El secreto de firma no cambia; para rotarlo, usa Rotar el secreto de firma. Requiere una clave de API con el permiso full.
/webhooks/:idParámetros de ruta
idstringObligatorioEl ID del webhook (wh_…) o su nombre, codificado para URL.
Cuerpo de la petición
namestringEl nuevo nombre. Debe ser único en el espacio de trabajo.
urlstringLa nueva URL del endpoint, http o https. Se valida igual que al crear el webhook.
all_eventsbooleantrue envía todos los tipos de eventos y vacía la lista events. Si lo pones en false, envía también events; si no, el webhook no recibe nada.
enabledbooleanfalse detiene las entregas y true las reanuda. Los eventos que se producen mientras el webhook está desactivado no se ponen en cola para él ni se envían más tarde.
eventsstring[]Sustituye la lista de tipos de eventos. Se ignora mientras all_events sea true. Los nombres no se validan.
filterobject | nullSustituye el filtro de payload, con el mismo formato que al crear el webhook. Envía null para quitarlo. Un filtro con reglas requiere un plan Pro, Business o Custom.
Devuelve
Devuelve 200 OK con el webhook actualizado. No incluye el secret; para leerlo, usa Obtener un webhook.
| Código | Cuándo |
|---|---|
400 |
El cuerpo no incluye ninguno de los campos anteriores, o la URL o el filtro no son válidos. |
403 |
El filtro tiene reglas y tu plan no incluye filtros de webhooks (plan_required). |
404 |
Ningún webhook coincide con id. |
409 |
Otro webhook ya usa el nuevo nombre. |
{
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events",
"url": "https://api.acme.com/webhooks/emailit",
"all_events": false,
"enabled": false,
"events": ["email.delivered", "email.bounced"],
"filter": null,
"filters_allowed": true,
"last_used_at": "2026-10-01T10:02:17.845000+00:00",
"created_at": "2026-10-01T09:41:05.302000+00:00",
"updated_at": "2026-10-01T10:15:40.000000+00:00"
}{
"error": "No valid fields provided for update. Provide at least one of: name, url, all_events, enabled, events, filter"
}{
"error": "Webhook not found"
}{
"error": "Another webhook with this name already exists"
}Listar webhooks
Devuelve los webhooks de tu espacio de trabajo, del más reciente al más antiguo, y cuántos permite tu plan. El listado no incluye los secretos de firma. Requiere una clave de API con el permiso full.
/webhooksParámetros de consulta
pageintegerEl número de página, empezando por 1. Por defecto, 1.
limitintegerWebhooks por página, de 1 a 100. Por defecto, 10.
searchstringBúsqueda en el nombre o la URL del webhook, sin distinguir mayúsculas y minúsculas.
matchstringall (por defecto) exige que se cumplan todos los filtros. or coincide con cualquier filtro. Consulta Filtrado.
orderstringClave de ordenación de esta lista. Consulta las claves de ordenación más abajo.
directionstringasc o desc.
Filtros y ordenación
Los filtros de listado son un único nivel de parámetros de consulta key.condition=value. Consulta Filtrado para ver match, order, direction y la lista de condiciones de cada tipo.
Claves de filtro
| Clave | Tipo | Condiciones | Notas |
|---|---|---|---|
name | string | exact, not_exact, contains, not_contains, starts_with, ends_with, empty, not_empty | |
url | string | exact, not_exact, contains, not_contains, starts_with, ends_with, empty, not_empty | |
enabled | boolean | exact, not_exact | |
created_at | date | exact, before, after, empty, not_empty |
Claves de ordenación
Pasa en order una de estas claves y en direction el valor asc o desc: name, url, enabled, created_at
Devuelve
Devuelve 200 OK con los webhooks en data, next_page_url y previous_page_url (null en los extremos) y un objeto usage: used es el número de webhooks del espacio de trabajo, limit es el máximo de tu plan y filters_allowed indica si tu plan incluye filtros de payload.
{
"data": [
{
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events",
"url": "https://api.acme.com/webhooks/emailit",
"all_events": false,
"enabled": true,
"events": ["email.delivered", "email.bounced"],
"filter": null,
"filters_allowed": true,
"last_used_at": "2026-10-01T10:02:17.845000+00:00",
"created_at": "2026-10-01T09:41:05.302000+00:00",
"updated_at": "2026-10-01T10:02:17.845000+00:00"
}
],
"next_page_url": null,
"previous_page_url": null,
"usage": {
"used": 1,
"limit": 10,
"filters_allowed": true
}
}{
"statusCode": 401,
"error": "Unauthorized",
"message": "Invalid API key"
}Eliminar un webhook
Elimina de forma permanente un webhook y sus suscripciones a eventos. Para detener las entregas temporalmente, actualiza el webhook con enabled: false en lugar de eliminarlo. Requiere una clave de API con el permiso full.
/webhooks/:idParámetros de ruta
idstringObligatorioEl ID del webhook (wh_…) o su nombre, codificado para URL.
Devuelve
Devuelve 200 OK con el id y el name del webhook eliminado y deleted: true. Devuelve 404 con error: "Webhook not found" si ningún webhook coincide.
{
"object": "webhook",
"id": "wh_2xLb3Kp9Qr1Vt7Mn5Ws0Yd4Hc8e",
"name": "Production events",
"deleted": true
}{
"error": "Webhook not found"
}Enviar un evento de prueba
Envía un evento de muestra del tipo que elijas a la URL del webhook y devuelve la respuesta de tu endpoint. Úsalo para comprobar que tu endpoint es accesible y que verifica correctamente las firmas. Requiere una clave de API con el permiso full.
La petición tiene el mismo formato, las mismas cabeceras y la misma firma que una entrega real: un array JSON con un evento cuyo event_id empieza por evt_test_, firmado con el secreto actual del webhook. Se envía aunque el webhook esté desactivado o no esté suscrito a ese tipo, no se guarda como petición de webhook y no se reintenta. Los datos de muestra son fijos y no hacen referencia a objetos reales.
Puedes enviar 5 eventos de prueba por minuto desde la misma dirección IP; los que superen ese número devuelven 429.
/webhooks/{id}/testParámetros de ruta
idstringobligatoriowh_…) o su nombre.Parámetros del cuerpo
typestringobligatorio| Recurso | Tipos de eventos |
|---|---|
email.accepted, email.scheduled, email.delivered, email.bounced, email.attempted, email.failed, email.rejected, email.clicked, email.loaded, email.complained, email.received, email.suppressed, email.canceled, email.unsubscribed, email.resubscribed |
|
| Dominio | domain.created, domain.updated, domain.deleted |
| Lista de contactos | audience.created, audience.updated, audience.deleted |
| Suscriptor | subscriber.created, subscriber.updated, subscriber.deleted |
| Contacto | contact.created, contact.updated, contact.deleted |
| Plantilla | template.created, template.updated, template.deleted |
| Dirección bloqueada | suppression.created, suppression.updated, suppression.deleted |
| Verificación de emails | email_verification.created, email_verification.updated, email_verification_list.created, email_verification_list.updated |
| Campaña | campaign.created, campaign.updated, campaign.deleted, campaign.scheduled, campaign.queued, campaign.sending, campaign.testing, campaign.sent, campaign.canceled, campaign.archived |
Para saber qué significa cada evento, consulta Tipos de eventos.
Devuelve
okbooleantrue si tu endpoint respondió con un estado 2xx.status_codeinteger0 si Emailit no pudo conectarse, si se agotó el tiempo de espera de la petición tras 30 segundos, si el endpoint redirigió (las redirecciones no se siguen) o si la URL apunta a una dirección no permitida.bodystringtypestringpayloadobject[]Devuelve 400 si falta type o es desconocido, 404 si el webhook no existe y 429 si superas el límite de pruebas.
{
"ok": true,
"status_code": 200,
"type": "email.delivered",
"payload": [
{
"event_id": "evt_test_3G0pUekgBm1uIxi9m4C380H70Zq",
"type": "email.delivered",
"object": {
"id": "eml_test_001",
"email_id": 12345,
"message_id": "<test-token@mydomain.com>",
"from": "sender@mydomain.com",
"to": "recipient@example.com",
"subject": "Test email",
"status": "delivered",
"delivered_at": "2026-01-15T10:30:00.000Z"
},
"data": {
"object": {
"id": "eml_test_001",
"email_id": 12345,
"message_id": "<test-token@mydomain.com>",
"from": "sender@mydomain.com",
"to": "recipient@example.com",
"subject": "Test email",
"status": "delivered",
"delivered_at": "2026-01-15T10:30:00.000Z"
}
}
}
],
"body": "{\"received\":true}"
}{
"error": "Unknown event type"
}{
"statusCode": 429,
"error": "Too Many Requests",
"message": "Rate limit exceeded, retry in 1 minute"
}Rotar el secreto de firma
Genera un nuevo secreto de firma para el webhook y lo devuelve. Requiere una clave de API con el permiso full.
El secreto anterior deja de usarse de inmediato: todas las peticiones enviadas después de la rotación, incluidos los reintentos de eventos anteriores, se firman con el nuevo secreto. No hay periodo de solapamiento, así que actualiza el secreto en tu endpoint justo después de rotarlo, o acepta ambos secretos durante un breve periodo mientras haces el cambio. Consulta Verificar las firmas de los webhooks.
/webhooks/{id}/reset-secretParámetros de ruta
idstringobligatoriowh_…) o su nombre.Devuelve
Devuelve el objeto de webhook con el nuevo secret (whsec_ seguido de 64 caracteres hexadecimales). Devuelve 404 si el webhook no existe.
{
"object": "webhook",
"id": "wh_3NGa6raJ3s1VJ75lO9km6Dqg4cQ",
"name": "Order notifications",
"url": "https://acme.com/webhooks/emailit",
"all_events": false,
"enabled": true,
"events": ["email.delivered", "email.bounced", "email.complained"],
"filter": null,
"last_used_at": "2026-10-01T12:58:40.000000+00:00",
"created_at": "2026-08-14T09:12:03.000000+00:00",
"updated_at": "2026-10-01T13:20:11.000000+00:00",
"secret": "whsec_a0a593d006bdec5aff2f21e88afd543ba239431acbe7d8dc7a9723d76e0a64e2"
}{
"error": "Webhook not found"
}Reintentar peticiones fallidas
Vuelve a poner en cola para su entrega todas las peticiones de este webhook que fallaron de forma definitiva en los últimos 7 días. Requiere una clave de API con el permiso full.
Una petición falla de forma definitiva tras su último reintento automático (11 intentos a lo largo de varios días; consulta Reintentos y fallos). Las peticiones reintentadas empiezan de nuevo con un calendario de reintentos completo y se entregan en cuestión de segundos. Si se pone en cola al menos una petición y el webhook estaba desactivado, por ejemplo tras 3 días de fallos continuos, se vuelve a activar.
Corrige primero tu endpoint o las peticiones volverán a fallar. Para reintentar una sola petición, usa Reintentar una petición.
/webhooks/{id}/retry-failedParámetros de ruta
idstringobligatoriowh_…) o su nombre.Devuelve
retriedinteger0 si no había nada que reintentar; en ese caso, el estado de activación del webhook no cambia.Devuelve 404 si el webhook no existe.
{
"retried": 37
}{
"error": "Webhook not found"
}Reintentar una petición
Vuelve a poner en cola para su entrega una petición de webhook que falló de forma definitiva, con un nuevo calendario de reintentos. Si el webhook estaba desactivado, se vuelve a activar. Requiere una clave de API con el permiso full.
Solo se pueden reintentar así las peticiones que han agotado sus reintentos automáticos; las que siguen pendientes o en reintento devuelven 400. Encontrarás los ID de las peticiones (whr_…) en la pestaña Requests del webhook, en Email APIWebhooks. Para reintentar a la vez todo lo de los últimos 7 días, usa Reintentar peticiones fallidas.
/webhooks/{id}/requests/{request_id}/retryParámetros de ruta
idstringobligatoriowh_…) o su nombre.request_idstringobligatoriowhr_…).Devuelve
retriedinteger1.idstring| Código | Cuándo |
|---|---|
400 |
La petición no ha fallado de forma definitiva o no tiene ningún evento que volver a enviar. |
404 |
El webhook no existe o la petición no le pertenece. |
{
"retried": 1,
"id": "whr_3tWVVMd2eHv3R9B5DWTLtwwU05X"
}{
"error": "Only permanently failed requests can be retried"
}{
"error": "Webhook request not found"
}