Webhooks
Webhooks de ReallyQuickEmails — eventos email.send/delivery/bounce/complaint/reject/deliverydelay/open/click/inbound, headers X-RQE-Signature y X-RQE-Environment, verificación HMAC-SHA256, routing webhook_url/webhook_url_dev/webhook_environments, payload de replies con adjuntos, retries.
ReallyQuickEmails te dispara un webhook por cada evento que ocurre durante la vida de un email: aceptación, entrega, rebote, queja, apertura, click y respuesta del destinatario.
Cómo recibir webhooks
Configura la URL en el dashboard
En el dashboard, entra a tu proyecto y abre Configuración → Integraciones → Webhooks. Pega la URL pública de tu endpoint. Las URLs disponibles por proyecto y qué recibe cada una están en Routing live / test / environment.
Expón tu endpoint local
Para desarrollo, crea un túnel hacia tu servidor local con una herramienta como ngrok (ngrok http 3000) y usa la URL pública generada como URL del webhook.
Valida la firma
Cada POST incluye el header X-RQE-Signature. Verifica el HMAC sobre el body raw antes de procesar el evento. Ver más en Verificación HMAC.
Responde 200
Devuelve un status 2xx lo antes posible; cualquier otro status, timeout o error de red activa reintentos. Ver más en Retry y respuesta esperada.
Pasa a producción
Reemplaza la URL del túnel por la URL pública de tu servidor. El flag is_test del payload te permite distinguir el tráfico de envíos con sk_test_*.
Eventos
event | Cuándo dispara | Datos extra en data |
|---|---|---|
email.send | RQE aceptó y despachó el envío | — |
email.delivery | El servidor del destinatario aceptó el email | — |
email.bounce | Email rebotado | bounce_type, bounce_subtype |
email.complaint | Destinatario marcó como spam | complaint_feedback_type |
email.reject | Envío rechazado antes de salir | — |
email.deliverydelay | Entrega temporalmente demorada | — |
email.open | Destinatario abrió el email (pixel cargado) | — |
email.click | Click en un link | url_clicked |
email.inbound | Destinatario respondió tu email | (ver sección Replies) |
Payload outbound
Todos los eventos outbound (email.send, email.delivery, email.bounce, email.complaint, email.reject, email.deliverydelay, email.open, email.click) comparten la misma estructura top-level. Lo que cambia es data.
{
"event": "email.delivery",
"timestamp": "2026-04-29T15:30:42.123Z",
"project_id": "uuid",
"is_test": false,
"environment": "staging",
"data": { "...": "..." }
}| Campo | Tipo | Descripción |
|---|---|---|
event | string | Tipo del evento (ver tabla arriba). |
timestamp | string ISO | Momento en que RQE despachó el webhook. |
project_id | uuid | Tu proyecto. |
is_test | boolean | true si el email se envió con sk_test_*. |
environment | string | (Opcional) presente solo si el envío incluyó environment. Ver Webhook environments. |
data | object | Detalle del evento (ver siguiente tabla). |
Campos de data
| Campo | Presente en | Descripción |
|---|---|---|
activity_id | todos | UUID del email original (lo recibes en la respuesta de /v1/send-email). |
message_id | todos, salvo algunos open/click | Message-ID asignado al email. Los open/click registrados por el tracking propio de RQE (pixel/redirect) no lo incluyen — usa activity_id como identificador estable. |
recipient | todos | Email del destinatario. |
event_type | todos | Tipo lowercase (delivery, bounce, open, click, ...). Equivale a event sin email.. |
event_timestamp | todos | ISO timestamp del evento real (distinto del top-level timestamp que es cuando RQE lo despachó). |
bounce_type | bounce | Permanent o Transient. |
bounce_subtype | bounce | General, NoEmail, Suppressed, etc. |
complaint_feedback_type | complaint | abuse, auth-failure, fraud, not-spam, other, virus. |
url_clicked | click | URL original a la que el destinatario hizo click. |
Nota: los eventos
email.sendyemail.deliveryno incluyen un campodelivered_atseparado — usaevent_timestamp.
Headers de la request
RQE hace POST a tu URL con:
| Header | Valor |
|---|---|
Content-Type | application/json |
User-Agent | ReallyQuickEmails-Webhook/1.0 |
X-RQE-Signature | sha256=<hmac_hex> — HMAC-SHA256 del body raw, secret = tu API key de producción (sk_proj_*), incluso para eventos de envíos test |
X-RQE-Environment | live, dev, o el nombre del environment custom si aplica. Solo en eventos outbound — el POST de email.inbound no incluye este header |
Verificación HMAC
const crypto = require('crypto');
const expected = 'sha256=' + crypto
.createHmac('sha256', apiKey)
.update(rawBody)
.digest('hex');
if (req.headers['x-rqe-signature'] !== expected) {
return res.status(401).send('Invalid signature');
}Importante: verifica la firma sobre el body raw, antes de cualquier parser JSON. Si el body cambia (espacios, orden de keys), el HMAC no va a coincidir.
Routing live / test / environment
Cada proyecto tiene URLs configurables:
| URL | Recibe |
|---|---|
webhook_url (Producción) | Todos los eventos outbound — live y test |
webhook_url_dev (Desarrollo) | Todos los eventos outbound — live y test |
webhook_environments[<key>] | Solo eventos de envíos que incluyeron environment: "<key>" en el body (override: las demás URLs no reciben ese envío) |
Sin environment custom, cada evento outbound se entrega a todas las URLs configuradas (Producción y Desarrollo a la vez) — el routing no filtra por modo del envío. El header X-RQE-Environment indica el slot de cada POST (live / dev) y el flag is_test del payload te dice si el envío usó sk_test_*.
Si configuras inbound_webhook_url / inbound_webhook_url_dev, esas URLs reemplazan a webhook_url / webhook_url_dev y reciben todos los eventos (outbound + inbound) — modelo "una URL recibe todo".
Los webhooks inbound (replies) se entregan a una sola URL según el modo del envío original (ver fallback abajo). El override por environment usa inbound_webhook_environments[<key>].
Fallback automático
Aplica solo a email.inbound — los eventos outbound no tienen fallback porque van a todas las URLs configuradas. RQE usa la primera URL no vacía de esta cadena:
- Envío original con
sk_test_*:inbound_webhook_url_dev→webhook_url_dev→inbound_webhook_url→webhook_url - Envío original live:
inbound_webhook_url→webhook_url→inbound_webhook_url_dev→webhook_url_dev
Si ninguna URL está configurada (outbound o inbound), el evento no se entrega.
Para environments custom, no hay fallback — si el environment declarado en el envío no está configurado, el envío entero falla con 400 ENVIRONMENT_NOT_CONFIGURED. Ver Webhook environments.
is_test en el payload
{
"event": "email.delivery",
"is_test": true,
"data": { "...": "..." }
}Como los eventos outbound llegan a todas las URLs configuradas, basta con configurar solo webhook_url para recibir todo en una sola URL — distingue el tráfico test con el flag is_test.
Inbound (replies con adjuntos)
Cuando un destinatario responde tu email, RQE captura la respuesta y la dispara como evento email.inbound.
Cuándo dispara
Solo cuando el email outbound original fue enviado vía API (POST /v1/send-email, POST /send-email o POST /v1/send-batch). En ese caso el Reply-To del email lleva un token único:
Reply-To: "Tu Empresa" <r-{token}@rqe.inbound.reallyquickemails.com>Los clientes de email (Gmail, Outlook, Apple Mail) muestran el nombre del remitente en lugar de la dirección técnica. Cuando el destinatario responde, la respuesta se enruta automáticamente a RQE y dispara el webhook.
No dispara para envíos desde la UI de RQE (campañas, automatizaciones, "enviar prueba") — esos usan el Reply-To del sender humano para que reciba en su inbox.
Body del payload inbound
{
"event": "email.inbound",
"timestamp": "2026-04-28T22:45:32.299Z",
"project_id": "uuid",
"is_test": false,
"data": {
"message_id": "<gmail-message-id@mail.gmail.com>",
"thread_id": "uuid",
"in_reply_to": "<original-message-id@us-east-1.amazonses.com>",
"references": ["<...>"],
"from": { "email": "cliente@empresa.com", "name": "Cliente Externo" },
"to": [
{ "email": "r-Rbxiu6RC@rqe.inbound.reallyquickemails.com", "name": null }
],
"cc": [],
"subject": "Re: Asunto original",
"text_body": "respuesta plana del cliente",
"html_body": "<div>respuesta html</div>",
"date": "2026-04-28T23:30:49.000Z",
"attachments": [
{
"filename": "factura.pdf",
"content_type": "application/pdf",
"size": 50826,
"storage_key": "...",
"download_url": "https://...?token=..."
}
],
"return_path_parsed": null,
"original_outbound": {
"message_id": "<original-message-id@us-east-1.amazonses.com>",
"activity_id": "uuid",
"campaign_id": null,
"email_type": null,
"thread_id": "uuid"
}
}
}Igual que en los eventos outbound, el payload incluye is_test y, si el envío original usó un environment custom, también environment.
Campos clave
| Campo | Descripción |
|---|---|
data.from | Quien envió la respuesta (cliente externo). |
data.to[] | Dirección token a la que respondió. |
data.text_body / data.html_body | Cuerpo de la respuesta. |
data.attachments[] | Adjuntos del reply. Cada uno incluye filename, content_type, size, storage_key y download_url firmada con TTL 7 días. Descarga y persiste si necesitas retención mayor. |
data.return_path_parsed | { project_id, activity_id } si la dirección respondida referencia directamente el envío original; null en caso contrario. |
data.original_outbound | Referencia al email original que disparó la conversación: message_id, activity_id, campaign_id, email_type, thread_id (null si no se pudo resolver). |
Límite de tamaño
150 KB total por reply (incluyendo adjuntos en MIME base64). Replies que excedan ese tamaño rebotan con "Message length exceeds limit set by recipient". Workaround: pídele al contacto que comparta archivos grandes vía link Drive/WeTransfer.
Retry y respuesta esperada
| Status | Comportamiento |
|---|---|
200–299 | Éxito. RQE marca el dispatch como exitoso. |
| Otro / timeout / error de red | RQE reintenta con backoff exponencial (30s → 1m → 2m → 4m). Después de 5 intentos fallidos, no se reintenta más. |
- Timeout: 10 segundos por intento.
- Intentos (eventos outbound): 5 en total — 1 inicial + 4 reintentos con backoff exponencial.
email.inbound: un solo intento, sin reintentos.- Idempotencia: los retries pueden re-entregar el mismo evento; si entregas a ambas URLs y solo una falla, el reintento re-entrega a las dos. Usa el par
(event, data.activity_id, data.event_timestamp)para deduplicar lado-cliente.
Próximos pasos
- Tracking — qué eventos disparan webhook y cómo se generan.
- Test mode — separar tráfico dev/prod vía
sk_test_*. - Webhook environments — N URLs por proyecto vía campo
environment. - Send email — cómo disparar emails que generen estos eventos.