RQE Docs
API Reference

Webhooks

Recepción de eventos SES, Shopify e inbound replies.

ReallyQuickEmails recibe notificaciones externas de AWS SES (entrega, rebotes, quejas, emails entrantes) y de Shopify (carritos, checkouts, ordenes).

Base URL: https://api.reallyquickemails.com


POST /webhooks/ses

Recibe notificaciones de entrega, rebote y quejas desde AWS SES via SNS. Estos webhooks actualizan automaticamente el estado de cada email enviado.

Autenticacion

Ninguna. La autenticidad se valida verificando la firma del mensaje SNS.

Eventos procesados

Evento SESEvento en DBDescripcionEfecto en activity
SendsentEmail aceptado por SES
DeliverydeliveredEmail entregado al servidor del destinatariocurrent_statusdelivered, setea delivered_at
BouncebounceEmail rebotado (hard o soft bounce)current_statusbounced, setea bounced_at
ComplaintcomplaintDestinatario marco como spamcurrent_statuscomplained, setea complained_at
RejectrejectSES rechazo el envio
DeliveryDelaydelivery_delayRetraso temporal en la entrega

Flujo

AWS SES envia email (con ConfigurationSet: reallyquickemails-events)
  → Evento de entrega/rebote/queja
    → AWS SNS (topic: ses-email-events) notifica
      → POST /webhooks/ses
        → Parsea body text/plain como JSON
          → Verifica firma SNS
            → Encola en queue ses-webhook (20 concurrentes)
              → Inserta en email_events
              → Actualiza activity (status + timestamps)
              → Despacha webhook firmado al proyecto

Nota: SNS envia el body como Content-Type: text/plain. El servidor parsea este formato automaticamente antes de procesarlo.

Open y Click Tracking (Self-hosted)

El tracking de aperturas y clics se maneja mediante endpoints propios, sin costo adicional:

EventoMecanismoEndpointEfecto en activity
openPixel 1x1 GIF inyectado en el HTMLGET /t/o/:activityIdSetea opened_first_at (solo la primera vez)
clickLinks reescritos con redirectGET /t/c/:activityId?url=...Setea clicked_first_at (solo la primera vez)
Email HTML contiene:
  → Pixel: <img src="https://api.reallyquickemails.com/t/o/{activityId}" />
  → Links: href="https://api.reallyquickemails.com/t/c/{activityId}?url={urlOriginal}"

Cuando el destinatario:
  → Abre el email → su cliente carga el pixel → registra open
  → Hace clic en un link → pasa por /t/c/ → redirect 302 al link original → registra click

Nota: El tracking de aperturas depende de que el cliente de email cargue imagenes externas. Algunos clientes (como Outlook desktop) bloquean imagenes por defecto. El tracking de clics es confiable en ~99% de los casos.

Correlacion con emails enviados

Cada email enviado tiene un activity_id (retornado en la respuesta de send-email y send-batch). Los webhooks de SES y los endpoints de tracking actualizan el registro de actividad correspondiente, permitiendo rastrear el estado de cada email:

  • queuedsentdeliveredopenedclicked (entrega exitosa con engagement)
  • queuedsentbounced (rebote)
  • queuedsentcomplained (marcado como spam)

POST /webhooks/shopify/:topic

Recibe webhooks de Shopify para atribucion de ingresos y automatizaciones de carritos abandonados.

Autenticacion

Verificacion HMAC (si SHOPIFY_API_SECRET esta configurado). Deduplicacion automatica via x-shopify-webhook-id en Redis.

Headers requeridos

HeaderDescripcion
x-project-idID del proyecto en ReallyQuickEmails
x-shopify-hmac-sha256Firma HMAC del webhook
x-shopify-webhook-idID unico del webhook (para deduplicacion)
x-shopify-shop-domainDominio de la tienda Shopify

Topics soportados

TopicQueue de procesamientoDescripcion
cart/updateprocess-shopify-cartCarrito actualizado — para automatizaciones de carrito abandonado
checkout/createprocess-shopify-checkoutCheckout iniciado — captura datos de pago
order/createprocess-shopify-orderOrden completada — atribucion de ingresos a campanas

Respuesta exitosa (200)

{
  "success": true,
  "message": "Webhook processed"
}

Codigos de Error

CodigoDescripcion
400Payload invalido o topic no soportado
401Firma HMAC invalida
409Webhook duplicado (ya procesado)
500Error interno

POST /api/inbound/ses

Recibe notificaciones de correos electronicos entrantes a traves de AWS SNS. Permite procesar respuestas a tus correos y mantener hilos de conversacion.

Autenticacion

Ninguna. La autenticidad se valida verificando la firma del mensaje SNS.

Flujo de Correo Entrante

Remitente externo
  → AWS SES (recepcion)
    → Regla de recepcion SES (almacena en S3)
      → AWS SNS (notificacion)
        → POST /api/inbound/ses (este endpoint)
          → Procesa, almacena y asocia al hilo correspondiente

Tipos de Mensaje SNS

Tipo de MensajeDescripcion
SubscriptionConfirmationSolicitud de confirmacion de suscripcion al topico SNS. Se confirma automaticamente.
NotificationNotificacion de un correo entrante. Contiene la referencia al objeto en S3 con el correo MIME completo.
UnsubscribeConfirmationConfirmacion de cancelacion de suscripcion al topico SNS.

Procesamiento de Notificaciones

Cuando se recibe una notificacion de tipo Notification:

  1. Obtener correo desde S3 — Descarga el archivo MIME completo del bucket S3.

  2. Parsear correo — Extrae: remitente, destinatario, asunto, cuerpo (texto plano y HTML), adjuntos y headers.

  3. Resolver hilo — Identifica el proyecto y la actividad asociada. La resolucion de hilos sigue una jerarquia de metodos, en orden de prioridad:

    1. Reply token (formato actual): "Nombre Sender" <r-{token}@rqe.inbound.reallyquickemails.com> — Token corto de 8 caracteres mapeado en Redis al proyecto y actividad.
    2. Return-Path legacy: reply+{projectId}-{activityId}@rqe.inbound.reallyquickemails.com — Formato anterior, soportado para compatibilidad.
    3. Header In-Reply-To — Coincide el Message-ID del correo original.
    4. Header References — Busca en la cadena de Message-IDs referenciados.
    5. Asunto normalizado — Ultimo recurso, coincide por asunto (sin prefijos Re:/Fwd:).
  4. Subir adjuntos — Si el correo contiene adjuntos, se suben a Supabase Storage.

  5. Almacenar mensaje — Guarda el mensaje en la base de datos, asociado al proyecto y hilo.

Headers Relevantes

HeaderDescripcion
x-amz-sns-message-typeTipo de mensaje SNS (ver tabla de tipos arriba).
x-amz-sns-message-idID unico del mensaje SNS.
x-amz-sns-topic-arnARN del topico SNS de origen.

Ejemplo de Payload (Notification)

{
  "Type": "Notification",
  "MessageId": "22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324",
  "TopicArn": "arn:aws:sns:us-east-1:123456789012:ses-inbound-emails",
  "Subject": "Amazon SES Email Receipt Notification",
  "Message": "{\"notificationType\":\"Received\",\"mail\":{\"source\":\"cliente@ejemplo.com\",\"destination\":[\"r-x7K9mP2q@rqe.inbound.reallyquickemails.com\"],\"messageId\":\"abc123def456\"},\"receipt\":{\"action\":{\"type\":\"S3\",\"bucketName\":\"rqe-inbound-emails\",\"objectKey\":\"emails/abc123def456\"}}}"
}

Respuesta exitosa (200)

{
  "success": true,
  "messageId": "abc123def456",
  "threadId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "projectId": "d4e5f6a7-b8c9-0123-def4-567890abcdef"
}

Codigos de Error

CodigoDescripcion
200Procesamiento exitoso o confirmacion de suscripcion completada.
400Payload invalido o no se pudo parsear el mensaje SNS.
500Error interno al procesar el correo entrante.

Live vs Test routing

Cada proyecto tiene cuatro URLs de webhook configurables, agrupadas en dos pares (outbound y inbound), y RQE elige a cuál disparar segun el prefijo de la API Key con la que se envió el email.

Tipo de webhookURL para LiveURL para TestDisparado por
Outbound (delivered, bounced, opened, clicked)webhook_urlwebhook_url_devEventos SES + tracking propio
Inbound (replies con adjuntos)inbound_webhook_urlinbound_webhook_url_devRespuestas a emails enviados por API

Cómo se decide el modo

El modo se determina únicamente por el prefijo de la API Key con la que se hizo el POST /v1/send-email (o send-batch / send-template-email):

  • Live (sk_proj_* o sk_live_*) → eventos van a las URLs sin sufijo (webhook_url, inbound_webhook_url).
  • Test (sk_test_*) → eventos van a las URLs con sufijo _dev.

No existe un parámetro mode en el body ni un header especial — el modo viaja en la key.

Comportamiento si la URL _dev está vacía

Si tu proyecto no tiene webhook_url_dev configurada y enviás con sk_test_*:

  • Los eventos outbound de ese envío no se entregan a ningún webhook. Quedan registrados en la base de datos (tabla email_events, activity con is_test=true) para inspección manual, pero RQE no hace fallback al webhook_url de live para evitar contaminar tu sistema productivo con tráfico de pruebas.
  • Mismo principio para inbound: si inbound_webhook_url_dev está vacío y el email original fue enviado con sk_test_*, la respuesta del cliente queda almacenada pero no se reenvía.

Configuración

En la app: Project Settings → Integraciones → Webhooks. Verás cuatro inputs separados (Producción, Producción - inbound, Test, Test - inbound), cada uno con su propio botón de Guardar.

Ejemplo end-to-end

1. Tu app envía email con sk_test_xyz
   POST /v1/send-email con Authorization: Bearer sk_test_xyz
   →  apiKeyAuth setea req.isTestMode = true
   →  emailSend.processor marca activity.is_test = true, skip counters
   →  SES envía el email real

2. Destinatario abre el email
   →  pixel hit → tracking.processor → email_events INSERT
   →  webhook dispatcher: lee project.webhook_url_dev → POST a esa URL
   →  payload incluye is_test: true para que tu sistema lo identifique

3. Destinatario responde con un adjunto
   →  SES inbound captura el reply
   →  RQE resuelve el reply token al activity_id original
   →  Como el original fue is_test=true: lee project.inbound_webhook_url_dev → POST

Test mode en el payload del webhook

Todos los webhooks (outbound e inbound) incluyen is_test: boolean en el body para que tu sistema pueda identificar el origen sin tener que mantener URLs separadas si no quieres:

{
  "event": "email.delivered",
  "is_test": true,
  "data": { "...": "..." }
}

Útil si preferís recibir todo en una sola URL (webhook_url) y filtrar en tu lado, dejando webhook_url_dev vacío. Pero mantenerlas separadas es lo recomendado para evitar accidentes.


Inbound Webhook (Forward de respuestas)

Cuando un cliente externo (sistema integrador, ej. Nexor) configura una inbound_webhook_url en su proyecto, RQE hace POST automatico a esa URL cada vez que un destinatario responde a un email enviado via API. Permite continuar conversaciones de forma programatica desde el sistema externo.

Configuracion

En la app: Project Settings → Integraciones → Webhook de respuestas → pegar URL → Guardar. Para tráfico generado con sk_test_* configurá el campo de Webhook de respuestas (test) — ver Live vs Test routing arriba.

Internamente se persiste en projects.inbound_webhook_url y projects.inbound_webhook_url_dev. Si el campo correspondiente está vacío, el reply queda almacenado en DB pero no se reenvía (no hay fallback cross-mode para evitar contaminar live con tráfico de test).

Cuando dispara

Solo cuando el email outbound fue enviado via API (POST /send-email con Authorization: Bearer sk_proj_..., sin header x-source). En ese caso el Reply-To del email lleva un token unico:

Reply-To: r-{token}@rqe.inbound.reallyquickemails.com

Cuando el destinatario responde, SES inbound captura el reply, RQE resuelve el token al proyecto + actividad original, y dispara el webhook.

No dispara para envios desde la UI (campanas, automatizaciones, "enviar prueba") — esos usan source=platform y el Reply-To apunta al sender humano para que reciba en su inbox.

Endpoint que recibe el cliente

El cliente expone un endpoint HTTP POST (cualquier URL publica) y lo configura en RQE. RQE hace POST con:

Headers

Content-Type: application/json
User-Agent: ReallyQuickEmails-Webhook/1.0
X-RQE-Signature: sha256={hmac_hex}

X-RQE-Signature es HMAC-SHA256 del body raw, usando la api_key del proyecto como secreto. El cliente debe verificar la firma antes de procesar.

Ejemplo verificacion en Node:

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');
}

Body

{
  "event": "email.inbound",
  "timestamp": "2026-04-28T22:45:32.299Z",
  "project_id": "uuid",
  "data": {
    "message_id": "<gmail-message-id@mail.gmail.com>",
    "thread_id": "uuid",
    "in_reply_to": "<original-ses-message-id@email.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": "{message_id_sanitized}/factura.pdf",
        "download_url": "https://bjzcfsazxnommbiiqoux.supabase.co/storage/v1/object/sign/email-attachments/...?token=..."
      }
    ],
    "return_path_parsed": null,
    "original_outbound": {
      "message_id": "<original-ses-message-id@us-east-1.amazonses.com>",
      "activity_id": "uuid",
      "campaign_id": null,
      "email_type": "individual",
      "thread_id": "uuid"
    }
  }
}

Campos clave

CampoDescripcion
eventDiscriminador. Siempre "email.inbound" para este evento. Diseno extensible a otros eventos (email.delivered, email.bounced, etc).
data.fromQuien envio la respuesta (cliente externo).
data.to[]Direccion token a la que respondio (r-{token}@rqe.inbound...).
data.text_body / data.html_bodyCuerpo de la respuesta.
data.attachments[]Adjuntos del reply. Cada uno incluye download_url firmada (Supabase Storage, TTL 7 dias). Descargar y persistir si se necesita retencion mayor.
data.original_outboundReferencia al email original que disparo la conversacion (activity_id, thread_id, campaign_id si aplica). Permite enlazar el reply al contexto original.

Retry y logs

  • Timeout: 10 segundos por intento.
  • Reintentos: 5 intentos con backoff exponencial (30s → 1m → 2m → 4m → 8m).
  • Todos los intentos quedan en la tabla webhook_logs con: status HTTP, response body (truncado a 5000 chars), duracion, error message si aplica. Visible solo via DB (no UI dashboard aun).

Limites conocidos

  • 150 KB total por reply (incluyendo adjuntos en MIME base64). SES inbound publica el correo via SNS topic, que tiene limit hard de 150 KB. Replies que excedan rebotan con "Message length exceeds limit set by recipient". Workaround: el cliente externo le pide a sus contactos que compartan archivos grandes via link Drive/WeTransfer en vez de adjuntar.
  • Solo API sends: como se menciona arriba, sends desde UI no disparan el webhook (el Reply-To va al sender humano).

Respuesta esperada del cliente

StatusComportamiento
200-299Exito. RQE marca el dispatch como exitoso en webhook_logs.
Otro / timeout / error de redRQE reintenta con backoff exponencial hasta 5 veces. Despues de 5 fallos, queda registrado en logs pero no se reintenta mas.

On this page