# Introducción (/) ReallyQuickEmails (RQE) es la API para enviar emails transaccionales y campañas masivas: una llamada HTTP y tu primer email está en camino. ## Tu primer email ```bash curl -X POST https://api.reallyquickemails.com/v1/send-email \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient_email": "usuario@ejemplo.com", "sender_email": "hola@tudominio.com", "subject": "Hola desde ReallyQuickEmails", "html_body": "

Hola

Mi primer email con RQE.

" }' ``` Respuesta exitosa (`200`): ```json { "message": "Email sent successfully", "email_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "123e4567-e89b-12d3-a456-426614174000" } ``` `email_id` identifica el envío: úsalo para consultar su estado en [Activity](/api-reference/activity) y correlacionar los eventos que llegan a tus [webhooks](/api-reference/webhooks). El `sender_email` debe pertenecer a un [dominio verificado](/api-reference/domains). Para desarrollo usa tu key `sk_test_*`: el email se envía de verdad (puedes probar render y deliverability), pero queda marcado con `is_test: true` y separado de tus métricas de producción. Detalle en [Modos Live y Test](/concepts/test-mode). ## Tres pasos al primer email ### Obtén tu API key En el [dashboard](https://app.reallyquickemails.com), entra a tu proyecto y abre **Configuración → Integraciones → API Keys**. Copia la **Test key** (`sk_test_*`) para desarrollo, o la **Live key** (`sk_proj_*` / `sk_live_*`) para producción. Más detalle en [API Keys](/guides/api-keys). ### Envía el curl Reemplaza `sender_email` por un remitente de tu [dominio verificado](/api-reference/domains) y `recipient_email` por tu propio email. Si la respuesta es `200` con `email_id`, el envío quedó en camino. ### Verifica el resultado Consulta el estado del envío en **Activity** del dashboard o vía `GET /v1/activity/:id` ([referencia](/api-reference/activity)). Para recibir los eventos en tu servidor (entrega, apertura, click, respuesta), configura tus [webhooks](/api-reference/webhooks). ## Lo esencial | | | |---|---| | **Base URL** | `https://api.reallyquickemails.com` | | **Autenticación** | Header `Authorization: Bearer ` en todos los endpoints | | **Formato** | JSON — `Content-Type: application/json` | | **API keys** | `sk_proj_*` y `sk_live_*` (producción) · `sk_test_*` ([modo de prueba](/concepts/test-mode)) | | **SDK oficial** | [`@reallyquickemails/sdk`](/guides/sdk-nodejs) para Node.js / TypeScript | ## Qué puedes construir ### Emails transaccionales Lo que acabas de ver: `POST /v1/send-email` envía a hasta 50 destinatarios con `cc`/`bcc` y adjuntos, y `POST /v1/send-template-email` usa plantillas pre-configuradas con variables. Referencia completa en [API Pública v1](/api-reference/public-api). ### Campañas y envío masivo `POST /v1/send-batch` acepta hasta **10,000 destinatarios por request**, con variables Handlebars por destinatario vía `recipients[].data`: ```bash curl -X POST https://api.reallyquickemails.com/v1/send-batch \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "sender": "noticias@tudominio.com", "senderName": "Mi Empresa", "subject": "Oferta especial para ti, {{nombre}}", "html": "

Hola {{nombre}}!

Tenemos una oferta en {{producto}}.

", "recipients": [ { "email": "juan@ejemplo.com", "data": { "nombre": "Juan", "producto": "Plan Pro" } }, { "email": "maria@ejemplo.com", "data": { "nombre": "Maria", "producto": "Plan Business" } } ] }' ``` Soporta idempotencia (header `Idempotency-Key`), previsualización con `dry_run` y envío programado con `scheduled_at` (ISO 8601). Ver [API Pública v1](/api-reference/public-api) y [Programar envíos](/guides/scheduling). ### Automatizaciones Dispara flujos de emails (onboarding, recuperación de carritos, secuencias) desde tu backend con el [SDK de Node.js](/guides/sdk-nodejs): ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); // Enroll idempotente — pasa trigger_reference_id para que múltiples calls // con el mismo external_id NO creen runs duplicados, sino que actualicen el run existente await rqe.automations.enroll('automation-uuid', { email: 'user@example.com', data: { first_name: 'Adriano', plan: 'pro' }, trigger_reference_id: 'order-12345', }); ``` ### Respuestas entrantes (inbound) Los emails enviados vía API (`POST /v1/send-email`, `POST /send-email` y `POST /v1/send-batch`) incluyen automáticamente un `Reply-To` con token único: ``` Reply-To: "Tu Empresa" ``` Los clientes de correo muestran el nombre del remitente, no la dirección técnica. Cuando el destinatario responde, la respuesta llega a tu webhook como evento `email.inbound` — adjuntos incluidos (payload resumido; esquema completo en [Webhooks](/api-reference/webhooks#inbound-replies-con-adjuntos)): ```json { "event": "email.inbound", "timestamp": "2026-04-28T22:45:32.299Z", "project_id": "uuid", "is_test": false, "data": { "from": { "email": "cliente@empresa.com", "name": "Cliente Externo" }, "subject": "Re: Asunto original", "text_body": "respuesta del cliente", "attachments": [ { "filename": "factura.pdf", "content_type": "application/pdf", "size": 50826, "download_url": "https://..." } ], "original_outbound": { "activity_id": "uuid", "thread_id": "uuid" } } } ``` Solo aplica a envíos hechos vía API — los envíos desde la UI de RQE (campañas, automatizaciones, "enviar prueba") usan el Reply-To del remitente humano para que reciba la respuesta en su inbox. ## Endpoints principales | Método y ruta | Qué hace | Referencia | |---|---|---| | `POST /v1/send-email` | Envío individual (hasta 50 destinatarios), cc/bcc, adjuntos | [API v1](/api-reference/public-api) | | `POST /v1/send-template-email` | Envío con plantilla pre-configurada y variables | [API v1](/api-reference/public-api) | | `POST /v1/send-batch` | Hasta 10,000 destinatarios por request, idempotencia, dry run, `scheduled_at` | [API v1](/api-reference/public-api) | | `POST /send-email` | API avanzada: envío programado con lenguaje natural y timezone | [Enviar Email](/api-reference/send-email) · [Scheduling](/guides/scheduling) | | `/v1/leads` (POST · GET · PUT · DELETE) | CRUD de contactos, segmentos, tags y atributos custom | [Leads](/api-reference/leads) | | `POST /v1/events` · `POST /v1/events/bulk` | Tracking de eventos custom (bulk hasta 1,000) | [Events](/api-reference/events) | | `GET /v1/activity` · `GET /v1/activity/:id` | Estado de envíos, timeline de eventos, filtros | [Activity](/api-reference/activity) | | `POST /domains/register` · `POST /domains/:domain/verify` | Alta y verificación de dominios (DKIM/SPF/DMARC) | [Dominios](/api-reference/domains) | Los eventos de entrega (`email.send`, `email.delivery`, `email.bounce`, `email.open`, `email.click`, `email.inbound`, entre otros) llegan a tu servidor vía [webhooks](/api-reference/webhooks) firmados con HMAC-SHA256 (header `X-RQE-Signature`). ## Errores comunes | Código | Mensaje | Solución | |---|---|---| | `401` | `Missing or invalid API key. Use: Authorization: Bearer sk_proj_...` | Verifica el header `Authorization` y el prefijo de la key | | `400` | `Missing required fields: recipient_email, sender_email, html_body` | Revisa los campos requeridos del endpoint | | `400` | `ENVIRONMENT_NOT_CONFIGURED` | El `environment` enviado no está configurado en el proyecto — ver [Webhook environments](/concepts/webhook-environments) | | `404` | `Template not found with ID ...` | El `template_id` no existe en el proyecto de esa API key | Un `sender_email` de dominio no verificado **no** produce error síncrono: el request responde `200` y el envío falla en segundo plano. Revisa el estado en [Activity](/api-reference/activity) y verifica tu dominio en [Dominios](/api-reference/domains). ## Para agentes IA Esta documentación está optimizada para consumo por LLMs: - [`/llms.txt`](/llms.txt) — índice de todas las páginas con sus descripciones. - [`/llms-full.txt`](/llms-full.txt) — todo el contenido de la documentación en un solo archivo. - Agrega el sufijo `.mdx` a la URL de cualquier página para obtener su markdown crudo — por ejemplo, [`/api-reference/public-api.mdx`](/api-reference/public-api.mdx). Los clientes que envían `Accept: text/markdown` reciben markdown directamente. ## Siguientes pasos } title="Quickstart" description="De cero a tu primer email enviado en menos de 5 minutos, con la Test key." href="/guides/quickstart" /> } title="SDK de Node.js" description="@reallyquickemails/sdk — emails, leads, automations, events y senders con tipos TypeScript." href="/guides/sdk-nodejs" /> } title="API Keys (Live vs Test)" description="Prefijos sk_proj_ / sk_live_ / sk_test_, idempotencia y dry run." href="/guides/api-keys" /> } title="Webhooks" description="Eventos de entrega e inbound replies, verificación HMAC y routing Live/Test." href="/api-reference/webhooks" /> } title="Templates y variables" description="Variables, condicionales, loops y helpers que se resuelven al momento del envío." href="/guides/templates" /> } title="Envío masivo" description="POST /v1/send-batch — hasta 10,000 destinatarios con variables por destinatario." href="/api-reference/public-api" /> } title="Programar envíos" description="scheduled_at con ISO 8601 o lenguaje natural en inglés y zonas IANA." href="/guides/scheduling" /> ¿Preguntas? Escríbenos a [soporte@reallyquickemails.com](mailto:soporte@reallyquickemails.com) o consulta las [preguntas frecuentes](/guides/faq). # Activity (/api-reference/activity) Consulta el estado de los emails enviados con ReallyQuickEmails — entregas, aperturas, clicks, rebotes y reclamos — por ID o con filtros. ## Autenticacion Incluye tu API key del proyecto en cada request: ``` Authorization: Bearer sk_proj_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Sin una API key válida, la respuesta es `401` con `{ "error": "..." }`. Ver más en [Public API](public-api#autenticacion). --- ## Endpoints ### GET /v1/activity/:id Obtén el detalle de un envío específico por su ID. **Parámetros:** | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | `id` (path) | uuid | Sí | ID del envío. Lo recibes como `activity_id` en los webhooks `email.*`, o como `email_id` en la respuesta de [POST /v1/send-email](send-email) | | `include` (query) | string | No | `events`, `html` o `events,html` — incluye el timeline completo de eventos y/o el HTML renderizado del email | **Request:** ```bash curl https://api.reallyquickemails.com/v1/activity/e5522dde-33c7-4644-bf0a-28ec73830616 \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` **Response (200):** ```json { "success": true, "data": { "id": "e5522dde-33c7-4644-bf0a-28ec73830616", "created_at": "2026-04-17T16:40:20.623Z", "email_type": "automation", "campaign_id": null, "automation_run_id": "e42c853c-2e28-40cc-aad8-5de7d5395ede", "template_id": "bb164399-3ac3-475f-9acb-dc049a20d0d3", "message_id": "0100019d9c5098e5-888cac99-cd05-4882-9442-d293c98631fc-000000", "subject": "Test", "sender_email": "antonia@capitaria.com", "sender_name": "Antonia", "recipient_primary": "harold@dropout.cl", "current_status": "delivered", "delivered_at": "2026-04-17T16:40:21.813Z", "bounced_at": null, "complained_at": null, "opened_first_at": "2026-04-17T16:41:55.099Z", "clicked_first_at": "2026-04-17T17:30:17.645Z" } } ``` **Campos:** | Campo | Tipo | Descripción | |---|---|---| | `id` | uuid | `activity_id` — úsalo para llamadas posteriores | | `current_status` | string | Estado actual: `queued`, `retrying`, `sent`, `delivered`, `bounced`, `complained`, `failed`, `suppressed` | | `delivered_at` | ISO8601 \| null | Timestamp de entrega confirmada | | `opened_first_at` | ISO8601 \| null | Primera apertura (puede haber varias, ver `?include=events`) | | `clicked_first_at` | ISO8601 \| null | Primer click (idem) | | `bounced_at` | ISO8601 \| null | Timestamp del rebote (hard o soft) | | `complained_at` | ISO8601 \| null | Timestamp cuando el destinatario marcó como spam | | `message_id` | string | Identificador del mensaje asignado por la infraestructura de envío — útil para correlacionar eventos | Estados posibles de `current_status`: | Estado | Significado | |---|---| | `queued` | Aceptado, en cola de envío | | `retrying` | Reintentando tras un fallo temporal | | `sent` | Enviado al servidor del destinatario | | `delivered` | Entrega confirmada | | `bounced` | Rebotó (hard o soft) | | `complained` | El destinatario lo marcó como spam | | `failed` | Falló de forma permanente | | `suppressed` | Bloqueado por la lista de supresión del proyecto | **Con `?include=events`** — agrega un array `events` con el timeline completo, ordenado por `event_timestamp` ascendente: ```json { "success": true, "data": { "id": "e5522dde-33c7-4644-bf0a-28ec73830616", "current_status": "delivered", "events": [ { "id": "0c0ffe2e-1111-2222-3333-444455556666", "event_type": "delivered", "event_timestamp": "2026-04-17T16:40:21.813Z", "user_agent": null, "ip_address": null, "url_clicked": null, "bounce_type": null, "bounce_subtype": null, "complaint_feedback_type": null, "created_at": "2026-04-17T16:40:22.000Z" }, { "id": "1d1aabbc-1111-2222-3333-444455556666", "event_type": "open", "event_timestamp": "2026-04-17T16:41:55.099Z", "user_agent": "Mozilla/5.0 ...", "ip_address": "66.249.84.135", "url_clicked": null, "bounce_type": null, "bounce_subtype": null, "complaint_feedback_type": null, "created_at": "2026-04-17T16:42:00.000Z" }, { "id": "2e2bbccd-1111-2222-3333-444455556666", "event_type": "click", "event_timestamp": "2026-04-17T17:30:17.645Z", "user_agent": "Mozilla/5.0 ...", "ip_address": "190.0.0.1", "url_clicked": "https://landing.capitaria.com/masterclass", "bounce_type": null, "bounce_subtype": null, "complaint_feedback_type": null, "created_at": "2026-04-17T17:30:20.000Z" } ] } } ``` `event_type` puede ser: `sent`, `delivered`, `bounce`, `complaint`, `reject`, `open`, `click`, `rendering_failure`, `delivery_delay`. **Con `?include=html`** — agrega el campo `html_content` con el HTML completo renderizado del email (puede ser grande, solo pedirlo cuando se necesite). **Errores:** | Código | Causa | |---|---| | 401 | API key inválida o ausente | | 404 | `activity_id` no existe o no pertenece al proyecto | | 500 | Error interno | Las respuestas de error tienen la forma `{ "error": "mensaje" }`. --- ### GET /v1/activity Lista envíos con filtros para obtener datasets históricos y analizar patrones de entrega. **Parámetros (query):** | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | `email` | string | No | Filtra por `recipient_primary` (se normaliza a lowercase) | | `status` | string | No | Filtra por `current_status`: `queued`, `retrying`, `sent`, `delivered`, `bounced`, `complained`, `failed`, `suppressed` | | `automation_run_id` | uuid | No | Todos los emails generados por un enrollment específico | | `campaign_id` | uuid | No | Todos los emails enviados como parte de una campaña | | `since` | ISO8601 | No | `created_at >= since` | | `until` | ISO8601 | No | `created_at <= until` | | `page` | int | No | Default 1 | | `per_page` | int | No | Default 50, max 200 | **Caso típico con `automation_run_id`:** después de llamar `POST /v1/automations/:id/enroll`, guarda el `automation_run_id` devuelto. Más tarde, cuando el flujo haya ejecutado N emails, puedes obtenerlos todos filtrando por ese ID. **Request:** ```bash curl "https://api.reallyquickemails.com/v1/activity?email=jane@acme.com&status=delivered&since=2026-04-01" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` Filtrar por `automation_run_id`: ```bash curl "https://api.reallyquickemails.com/v1/activity?automation_run_id=e42c853c-2e28-40cc-aad8-5de7d5395ede" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` **Response (200):** ```json { "success": true, "data": [ { "id": "e5522dde-33c7-4644-bf0a-28ec73830616", "subject": "Bienvenida", "recipient_primary": "jane@acme.com", "current_status": "delivered" }, { "id": "f6633eef-44d8-5755-c01b-39fd84941727", "subject": "Recordatorio", "recipient_primary": "jane@acme.com", "current_status": "sent" } ], "pagination": { "page": 1, "per_page": 50, "total": 1423, "total_pages": 29 } } ``` Cada elemento de `data` incluye los mismos campos base que `GET /v1/activity/:id` (aquí abreviados). Orden: `created_at DESC` (más reciente primero). **Errores:** | Código | Causa | |---|---| | 401 | API key inválida o ausente | | 500 | Error interno | Las respuestas de error tienen la forma `{ "error": "mensaje" }`. --- ## Pull vs Webhook - **Pull** (este endpoint): tu sistema consulta cuando lo necesite. Simple de integrar, pero con latencia — las aperturas y clicks se procesan en segundo plano y pueden tardar unos segundos (~5s) en reflejarse en `opened_first_at` / `clicked_first_at` y en el timeline de eventos. - **Webhook**: eventos push en tiempo real cuando ocurren. Mejor para triggers reactivos. Para analizar patrones de entrega en batch, pull es el patrón correcto. Para automatizaciones reactivas en tiempo real, usa webhooks. Ver más en [Webhooks](webhooks). ## Rate limits - Max 200 resultados por página en `GET /v1/activity` (`per_page` mayor a 200 se ajusta a 200). # Campañas (/api-reference/campaigns) **Base URL:** `https://api.reallyquickemails.com` --- ## Como enviar correos masivos Hay dos formas de enviar correos masivos con ReallyQuickEmails: | Método | Mejor para | Límite por request | |---|---|---| | **API: `POST /v1/send-batch`** | Integraciones programáticas, envíos desde tu backend | 10,000 destinatarios | | **API: `POST /v1/campaigns` o Dashboard** | Envíos grandes con segmentos, lotes y warming | Hasta 10,000,000 destinatarios (procesamiento automático por lotes) | Ver más en [`POST /v1/send-batch`](./public-api#post-v1send-batch). --- ## POST /v1/campaigns Crea una campaña de envío masivo. ### Parámetros del body | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | `name` | string | Sí | Nombre de la campaña. | | `templateId` | string | Sí | ID (UUID) del template a enviar. | | `senderProfileId` | string | Sí | ID del perfil de remitente (dominio verificado). | | `recipientIds` | string[] | Condicional | IDs de destinatarios. Requerido cuando `selectAllRecipients` es `false`. | | `selectAllRecipients` | boolean | No | Enviar a todos los destinatarios del proyecto. Default: `false`. | | `recipientCount` | number | Condicional | Total de destinatarios. Requerido cuando `selectAllRecipients` es `true`. | | `recipientFilter` | object | No | Filtro de destinatarios (solo con `selectAllRecipients: true`). | | `subject` | string | No | Asunto de la campaña. Si se omite, se usa el asunto del template. | | `previewText` | string | No | Texto de preview (preheader). | | `templateInternalId` | string \| number | No | ID interno numérico del template (alternativo al UUID). | | `scheduledFor` | string | No | Timestamp ISO para programar el envío. | | `scheduleType` | string | No | Tipo de programación. Default: `scheduled`. Con `delay` requiere `delayAmount` y `delayUnit`. | | `delayAmount` | number | No | Cantidad de retraso (solo con `scheduleType: "delay"`). | | `delayUnit` | string | No | Unidad del retraso (solo con `scheduleType: "delay"`). | | `batchMode` | boolean | No | Activa el envío por lotes. Default: `false`. | | `batchSize` | number | No | Destinatarios por lote (con `batchMode: true`). | | `batchIntervalHours` | number | No | Horas entre lotes. Default: `24`. | | `sendOverHours` | number | No | Distribuye el envío uniformemente en N horas. | | `variableConfig` | object | No | `{ "defaults": {...}, "mappings": {...} }` — valores por defecto y mapeo de campos del destinatario a variables del template. | | `confirmDuplicate` | boolean | No | Omite la detección de campañas duplicadas (ver respuesta `409`). | ### Request example Requiere autenticación con Bearer token (`Authorization: Bearer sk_proj_...`). Ver más en [Autenticacion](./public-api#autenticacion). ```bash curl -X POST https://api.reallyquickemails.com/v1/campaigns \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "name": "Newsletter Junio", "templateId": "9f1c2b34-5678-4abc-9def-012345678901", "senderProfileId": "1a2b3c4d-5678-4abc-9def-012345678901", "recipientIds": ["c0ffee00-1111-4222-8333-444455556666"], "subject": "Novedades de junio", "batchMode": true, "batchSize": 5000, "batchIntervalHours": 24 }' ``` ### Response example (200) ```json { "success": true, "campaign": { "id": "7e8d9c0b-1234-4abc-9def-012345678901", "name": "Newsletter Junio", "status": "initializing", "subject": "Novedades de junio", "template_id": "9f1c2b34-5678-4abc-9def-012345678901", "sender_profile_id": "1a2b3c4d-5678-4abc-9def-012345678901", "total_recipients": 1, "batch_mode": true, "batch_size": 5000, "batch_interval_hours": 24, "total_batches": 1, "current_batch": 0, "utm_campaign": "rqe_7e8d9c0b12344abc9def012345678901" } } ``` La campaña se devuelve de inmediato en estado `initializing`; los registros de destinatarios se crean en segundo plano (típicamente 2-30 segundos según el tamaño de la audiencia) y la campaña pasa a `pending`. El envío comienza automáticamente. El objeto `campaign` incluye la fila completa de la campaña (también `created_at`, `next_batch_at`, `select_all_recipients`, `send_over_hours`, `variable_config`, `scheduled_for`, etc.). Ver más en [Estados de una campana](#estados-de-una-campana). ### Detección de duplicados (409) Para evitar envíos duplicados accidentales, la API responde `409` en dos casos: - `subject_match_60min` — ya existe una campaña con el mismo asunto creada en los últimos 60 minutos. - `name_template_24h` — ya existe una campaña con el mismo nombre y template en las últimas 24 horas en estado no completado (`pending`, `paused`, `processing`, `initializing`). ```json { "warning": "duplicate_campaign", "reason": "subject_match_60min", "message": "Ya existe una campaña \"Newsletter Junio\" con el mismo asunto enviada hace 12 minutos", "existingCampaignId": "7e8d9c0b-1234-4abc-9def-012345678901", "existingCampaignStatus": "processing" } ``` Si realmente quieres crear la campaña, reenvía el request con `"confirmDuplicate": true`. ### Códigos de error | Código | Error | |---|---| | `400` | `Missing required fields: name, templateId, senderProfileId` | | `400` | `recipientIds is required when selectAllRecipients is false` | | `400` | `recipientCount is required when selectAllRecipients is true` | | `400` | `scheduledFor must be a string (ISO timestamp)` | | `401` | API key inválida o ausente. | | `409` | `duplicate_campaign` (ver arriba). | | `500` | `Failed to create campaign` | --- ## Campanas desde el Dashboard El Dashboard de RQE ofrece un asistente de 5 pasos para crear y enviar campañas sin código. ### Flujo completo ``` 1. Crear template 2. Crear campaña 3. Se envía automáticamente (Editor visual) (Asistente 5 pasos) (Procesamiento en segundo plano) | | | Editor visual Destinatarios, Lotes automáticos, con variables template, sender, rate limiting, {{nombre}} variables, schedule reintentos ``` ### Paso 1: Crear un template Desde **Templates → Nuevo Template**, usa el editor visual de campañas para diseñar tu email. Los templates soportan variables dinámicas con sintaxis Handlebars: ```html

¡Hola {{nombre}}!

Tu código de descuento es: {{codigo}}

{{default producto "nuestro catálogo"}}

``` **Sintaxis de variables:** | Sintaxis | Descripción | |---|---| | `{{variable}}` | Sustitución básica | | `{{default variable "fallback"}}` | Valor por defecto si la variable no existe | | `{{#each items}}...{{/each}}` | Loop sobre arrays | **Helpers disponibles:** | Helper | Descripción | |---|---| | `{{formatCurrency precio "USD"}}` | Formatea un número como moneda. | | `{{formatDate fecha "long"}}` | Formatea una fecha (`short` por defecto). | | `{{multiply cantidad precio}}` | Multiplica dos números (2 decimales). | | `{{capitalize texto}}` / `{{uppercase texto}}` / `{{lowercase texto}}` | Transformación de texto. | **Variables integradas:** cada destinatario recibe automáticamente `{{name}}`, `{{email}}` y `{{unsubscribe_url}}`, además de los campos de datos del contacto. La búsqueda de claves de primer nivel también funciona en minúsculas (si tus datos de CSV/CRM traen claves como `NOMBRE`, el template puede usar `{{nombre}}`). Si el template no incluye `{{unsubscribe_url}}`, se agrega un footer de cancelación de suscripción automáticamente y los emails incluyen headers `List-Unsubscribe`. ### Paso 2: Crear la campana Desde **Campañas → Nueva Campaña**, el asistente te guía en 5 pasos: 1. **Destinatarios** — Selecciona manualmente, usa "Seleccionar todos" o filtra por segmento. 2. **Template y Remitente** — Elige el template creado y el perfil de remitente (dominio verificado). 3. **Variables** (opcional) — Configura valores por defecto, mapea campos del destinatario a variables del template, o sube un CSV con valores personalizados por email. 4. **Configuración de envío** — Activa modo por lotes, define tamaño de lote e intervalo, o distribuye el envío en N horas. 5. **Revisar y Enviar** — Confirma todo y lanza la campaña. ### Paso 3: Procesamiento automatico Una vez creada la campaña: 1. Se crean los registros de destinatarios (estado: `pending`) 2. La campaña pasa de `initializing` a `pending` 3. Los destinatarios se procesan de forma asíncrona en segundo plano: se preparan las variables de cada uno, se encolan para envío y pasan a estado `queued` 4. Cada email se renderiza con Handlebars, se envía a través de nuestra infraestructura de envío y pasa a estado `sent` 5. Los eventos de delivery/bounce/complaint actualizan el estado final y disparan tus webhooks Ver más en [Webhooks](./webhooks). ### Configuracion avanzada #### Modo por lotes Divide el envío en lotes con intervalos entre ellos con `batchMode`, `batchSize` y `batchIntervalHours`. Útil para warming de dominio o para no saturar a tus destinatarios. **Ejemplo:** 50,000 destinatarios con lotes de 10,000 cada 24 horas = 5 días de envío gradual. Ver más en [`POST /v1/campaigns`](#post-v1campaigns). #### Distribucion horaria Distribuye el envío uniformemente en un período de tiempo con el campo `sendOverHours`. **Ejemplo:** 3,000 emails distribuidos en 6 horas = ~500 emails por hora. #### Variables por destinatario (CSV) Personaliza cada email subiendo un CSV con columnas: ```csv email,nombre,codigo_descuento,producto juan@ejemplo.com,Juan,JUAN20,Plan Pro maria@ejemplo.com,Maria,MARIA15,Plan Business ``` Las variables del CSV tienen prioridad sobre los valores por defecto. --- ## Warming de dominio Para envíos grandes (>50,000 emails) desde un dominio nuevo o con poco historial, envía gradualmente: | Día | Volumen sugerido | |---|---| | Día 1 | ~50,000 | | Día 2 | ~75,000 | | Día 3+ | Resto | Usa el **modo por lotes** para automatizar esto. Enviar todo de golpe desde un dominio frío puede resultar en clasificación como spam por Gmail, Outlook y otros proveedores. Ver más en [Deliverability y autenticación](/concepts/deliverability). --- ## Estados de una campana | Estado | Descripción | |---|---| | `initializing` | Creando registros de destinatarios (transitorio, típicamente 2-30 segundos) | | `pending` | Lista para ser procesada | | `processing` | Enviando lotes activamente | | `paused` | Pausada manualmente por el usuario | | `rate_limited` | Pausada automáticamente por límite de tasa | | `completed` | Todos los emails fueron procesados | | `failed` | La campaña terminó con errores | | `cancelled` | Cancelada por el usuario | # Dominios (/api-reference/domains) Endpoints de ReallyQuickEmails para la gestión completa de dominios de envío: registro, verificación DNS, consulta de estado y configuración de perfil de remitente. **Base URL:** `https://api.reallyquickemails.com` --- ## Autenticacion Todos los endpoints de dominios requieren autenticación mediante Secret Key: | Header | Tipo | Requerido | Descripción | | --------------- | ------ | --------- | --------------------------------- | | `Authorization` | string | Sí | `Bearer sk_proj_...` | | `Content-Type` | string | Sí* | `application/json` (en POST/PUT). | --- ## POST /domains/register Registra un nuevo dominio de envío: crea la identidad de envío, genera los registros DNS necesarios y crea el perfil de remitente asociado. ### Request Body | Campo | Tipo | Requerido | Descripción | | -------------- | ------ | --------- | ------------------------------------------------------------------ | | `domain` | string | Sí | Dominio a registrar (ej. `mitienda.com`). | | `sender_name` | string | Sí | Nombre del remitente (ej. `Mi Tienda`). | | `sender_email` | string | Sí | Dirección de correo del remitente. Debe terminar en `@{domain}` (ej. `ventas@mitienda.com`). | | `external_key` | string | No | Clave externa para integraciones de terceros. | ### Ejemplo ```bash curl -X POST https://api.reallyquickemails.com/domains/register \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -d '{ "domain": "mitienda.com", "sender_name": "Mi Tienda", "sender_email": "ventas@mitienda.com" }' ``` ### Respuesta exitosa (201 Created) ```json { "success": true, "identity_id": "8a1f6e2c-3b4d-4f5a-9c6e-7d8f9a0b1c2d", "domain": "mitienda.com", "verification_status": "pending", "sender_profile_id": "123e4567-e89b-12d3-a456-426614174000", "sender": { "from_name": "Mi Tienda", "from_email": "ventas@mitienda.com", "domain_authenticated": false }, "verification_token": "abcdef1234567890abcdef1234567890", "dkim_tokens": ["abc123", "def456", "ghi789"], "dns_records": [ { "id": "f1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607", "record_type": "TXT", "name": "_amazonses", "value": "abcdef1234567890abcdef1234567890", "purpose": "ses_verification", "status": "not_set", "ttl_hint": 300 }, { "id": "a2b3c4d5-e6f7-4081-92a3-b4c5d6e7f809", "record_type": "CNAME", "name": "abc123._domainkey", "value": "abc123.dkim.amazonses.com", "purpose": "dkim", "status": "not_set", "ttl_hint": 1800 }, { "id": "b3c4d5e6-f708-4192-a3b4-c5d6e7f8091a", "record_type": "CNAME", "name": "def456._domainkey", "value": "def456.dkim.amazonses.com", "purpose": "dkim", "status": "not_set", "ttl_hint": 1800 }, { "id": "c4d5e6f7-0819-42a3-b4c5-d6e7f8091a2b", "record_type": "CNAME", "name": "ghi789._domainkey", "value": "ghi789.dkim.amazonses.com", "purpose": "dkim", "status": "not_set", "ttl_hint": 1800 }, { "id": "d5e6f708-192a-43b4-c5d6-e7f8091a2b3c", "record_type": "TXT", "name": "@", "value": "v=spf1 include:amazonses.com ~all", "purpose": "spf", "status": "not_set", "ttl_hint": 300 }, { "id": "e6f70819-2a3b-44c5-d6e7-f8091a2b3c4d", "record_type": "TXT", "name": "_dmarc", "value": "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@mitienda.com", "purpose": "dmarc", "status": "not_set", "ttl_hint": 300 }, { "id": "f708192a-3b4c-45d6-e7f8-091a2b3c4d5e", "record_type": "CNAME", "name": "bounce", "value": "feedback-smtp.us-east-1.amazonses.com", "purpose": "return_path", "status": "not_set", "ttl_hint": 300 } ] } ``` Cada registro DNS incluye: `id`, `record_type` (`TXT` o `CNAME`), `name`, `value`, `purpose` (`ses_verification`, `dkim`, `spf`, `dmarc`, `return_path`), `status` y `ttl_hint` (TTL sugerido en segundos). Valores posibles de `status` de un registro DNS: `not_set`, `propagating`, `mismatch`, `verified`. ### Registros DNS generados | # | Tipo | Nombre | Propósito | Notas | |---|------|--------|-----------|-------| | 1 | TXT | `_amazonses` | Verificación de dominio | Valor generado automáticamente al registrar | | 2-4 | CNAME | `{token}._domainkey` | DKIM | 3 registros, tokens generados automáticamente | | 5 | TXT | `@` (raíz del dominio) | SPF | `v=spf1 include:amazonses.com ~all` | | 6 | TXT | `_dmarc` | DMARC | `v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@{domain}` | | 7 | CNAME | `bounce` | Return-Path | Apunta a `feedback-smtp.us-east-1.amazonses.com` | > **Nota:** Los nombres de los registros son relativos al dominio. Por ejemplo, si tu dominio es `mitienda.com`, el registro `_amazonses` se configura como `_amazonses.mitienda.com` en tu proveedor DNS. Algunos proveedores agregan el dominio automáticamente, por lo que solo necesitas ingresar `_amazonses`. Configura los registros DNS en tu proveedor y luego llama a `POST /domains/{domain}/verify` para validar. Ver más en [Deliverability](/concepts/deliverability). ### Codigos de Error | Código | Descripción | | ------ | -------------------------------------------------------- | | `400` | Campos requeridos faltantes, dominio con formato inválido, o `sender_email` no termina en `@{domain}`. | | `401` | API Key inválida. | | `409` | El dominio ya está registrado en este proyecto. | | `500` | Error interno al registrar el dominio. | --- ## GET /domains/:domain/dns-records Retorna los registros DNS almacenados para un dominio, con su último estado conocido y sin realizar consultas externas. ### Parametros de Ruta | Parámetro | Tipo | Descripción | | --------- | ------ | ------------------------------------ | | `domain` | string | Dominio registrado (ej. `mitienda.com`). | ### Ejemplo ```bash curl -X GET https://api.reallyquickemails.com/domains/mitienda.com/dns-records \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ### Respuesta exitosa (200 OK) ```json { "success": true, "identity_id": "8a1f6e2c-3b4d-4f5a-9c6e-7d8f9a0b1c2d", "domain": "mitienda.com", "verification_status": "pending", "dns_records": [ { "id": "f1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607", "record_type": "TXT", "name": "_amazonses", "value": "abcdef1234567890abcdef1234567890", "purpose": "ses_verification", "status": "not_set", "ttl_hint": 300, "last_checked_at": null }, { "id": "a2b3c4d5-e6f7-4081-92a3-b4c5d6e7f809", "record_type": "CNAME", "name": "abc123._domainkey", "value": "abc123.dkim.amazonses.com", "purpose": "dkim", "status": "not_set", "ttl_hint": 1800, "last_checked_at": null }, { "id": "d5e6f708-192a-43b4-c5d6-e7f8091a2b3c", "record_type": "TXT", "name": "@", "value": "v=spf1 include:amazonses.com ~all", "purpose": "spf", "status": "not_set", "ttl_hint": 300, "last_checked_at": null }, { "id": "f708192a-3b4c-45d6-e7f8-091a2b3c4d5e", "record_type": "CNAME", "name": "bounce", "value": "feedback-smtp.us-east-1.amazonses.com", "purpose": "return_path", "status": "not_set", "ttl_hint": 300, "last_checked_at": null } ] } ``` `status` por registro: `not_set`, `propagating`, `mismatch` o `verified`. `last_checked_at` es `null` hasta que ejecutas la primera verificación con `POST /domains/{domain}/verify`. > **Nota:** Los registros SPF y DMARC siempre aparecen con `status: "not_set"` porque su propagación no se valida automáticamente. ### Codigos de Error | Código | Descripción | | ------ | ----------------------------------------------- | | `401` | API Key inválida. | | `404` | Dominio no encontrado en el proyecto. | --- ## POST /domains/:domain/verify Verifica el estado DNS del dominio consultando directamente la infraestructura de envío, actualiza el estado almacenado de los registros y retorna si el dominio puede enviar correos. ### Parametros de Ruta | Parámetro | Tipo | Descripción | | --------- | ------ | ------------------------------------ | | `domain` | string | Dominio registrado (ej. `mitienda.com`). | ### Ejemplo ```bash curl -X POST https://api.reallyquickemails.com/domains/mitienda.com/verify \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ### Respuesta exitosa (200 OK) ```json { "success": true, "identity_id": "8a1f6e2c-3b4d-4f5a-9c6e-7d8f9a0b1c2d", "domain": "mitienda.com", "verification_status": "verified", "sender_profile": { "id": "123e4567-e89b-12d3-a456-426614174000", "from_name": "Mi Tienda", "from_email": "ventas@mitienda.com", "domain_authenticated": true }, "details": { "domain_verification": "Success", "dkim_verification": "Success", "mail_from_status": "Pending" }, "dns_records": [ { "id": "f1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607", "record_type": "TXT", "name": "_amazonses", "value": "abcdef1234567890abcdef1234567890", "purpose": "ses_verification", "status": "verified", "last_checked_at": "2025-03-10T18:45:00.000Z" }, { "id": "a2b3c4d5-e6f7-4081-92a3-b4c5d6e7f809", "record_type": "CNAME", "name": "abc123._domainkey", "value": "abc123.dkim.amazonses.com", "purpose": "dkim", "status": "verified", "last_checked_at": "2025-03-10T18:45:00.000Z" }, { "id": "f708192a-3b4c-45d6-e7f8-091a2b3c4d5e", "record_type": "CNAME", "name": "bounce", "value": "feedback-smtp.us-east-1.amazonses.com", "purpose": "return_path", "status": "propagating", "last_checked_at": "2025-03-10T18:45:00.000Z" } ], "verification_token": "abcdef1234567890abcdef1234567890", "dkim_tokens": ["abc123", "def456", "ghi789"], "can_send": true } ``` La verificación comprueba tres aspectos: 1. **Verificación de dominio** — El registro TXT de verificación (`_amazonses`) está configurado. 2. **DKIM** — Los 3 registros CNAME de DKIM están configurados y propagados. 3. **MAIL FROM** — El registro CNAME de Return-Path (`bounce`) está configurado. Campos clave: | Campo | Descripción | |---|---| | `verification_status` | `verified` (dominio y DKIM verificados), `failed` (alguna verificación falló) o `pending`. | | `details.domain_verification` | Estado crudo de la verificación de dominio: `Success`, `Pending`, `Failed`, `TemporaryFailure` o `NotStarted`. | | `details.dkim_verification` | Estado crudo de DKIM (mismos valores). | | `details.mail_from_status` | Estado crudo de MAIL FROM (mismos valores). | | `sender_profile` | Perfil de remitente activo del dominio, o `null` si no existe. | | `can_send` | `true` cuando dominio y DKIM están verificados. | El campo `can_send` es `true` cuando `details.domain_verification` y `details.dkim_verification` son `Success` (equivalente a `verification_status: "verified"`). La verificación de MAIL FROM no es obligatoria para enviar, pero se recomienda para mejorar la entregabilidad. ### Codigos de Error | Código | Descripción | | ------ | ------------------------------------------------- | | `401` | API Key inválida. | | `404` | Dominio no encontrado en el proyecto. | | `500` | Error al comunicarse con la infraestructura de envío. | --- ## GET /domains/:domain/status Retorna el estado de verificación almacenado, sin consultar la infraestructura de envío — un endpoint rápido ideal para polling desde la interfaz. ### Parametros de Ruta | Parámetro | Tipo | Descripción | | --------- | ------ | ------------------------------------ | | `domain` | string | Dominio registrado (ej. `mitienda.com`). | ### Ejemplo ```bash curl -X GET https://api.reallyquickemails.com/domains/mitienda.com/status \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ### Respuesta exitosa (200 OK) ```json { "success": true, "identity_id": "8a1f6e2c-3b4d-4f5a-9c6e-7d8f9a0b1c2d", "domain": "mitienda.com", "verification_status": "verified", "external_key": null, "sender_profile": { "id": "123e4567-e89b-12d3-a456-426614174000", "from_name": "Mi Tienda", "from_email": "ventas@mitienda.com", "domain_authenticated": true }, "dns_records_summary": [ { "purpose": "ses_verification", "records_count": 1, "all_verified": true }, { "purpose": "dkim", "records_count": 3, "all_verified": true }, { "purpose": "spf", "records_count": 1, "all_verified": false }, { "purpose": "dmarc", "records_count": 1, "all_verified": false }, { "purpose": "return_path", "records_count": 1, "all_verified": true } ], "can_send": true, "last_verified_at": "2025-03-10T18:45:00.000Z" } ``` `dns_records_summary` agrupa los registros DNS por propósito: `records_count` es la cantidad de registros de ese propósito y `all_verified` indica si todos están en `status: "verified"`. `can_send` es `true` cuando `verification_status` es `verified`. `last_verified_at` corresponde a la última actualización del estado del dominio. ### Codigos de Error | Código | Descripción | | ------ | ----------------------------------------------- | | `401` | API Key inválida. | | `404` | Dominio no encontrado en el proyecto. | --- ## GET /domains Busca un dominio específico por nombre y retorna su información básica junto con el perfil de remitente. ### Query Parameters | Parámetro | Tipo | Requerido | Descripción | | --------- | ------ | --------- | ------------------------------------ | | `domain` | string | Sí | Nombre del dominio a buscar. | ### Ejemplo ```bash curl -X GET "https://api.reallyquickemails.com/domains?domain=mitienda.com" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ### Respuesta exitosa (200 OK) ```json { "success": true, "identity_id": "8a1f6e2c-3b4d-4f5a-9c6e-7d8f9a0b1c2d", "domain": "mitienda.com", "verification_status": "verified", "external_key": null, "can_send": true, "sender_profile": { "id": "123e4567-e89b-12d3-a456-426614174000", "from_name": "Mi Tienda", "from_email": "ventas@mitienda.com", "domain_authenticated": true }, "created_at": "2025-03-01T12:00:00.000Z" } ``` ### Codigos de Error | Código | Descripción | | ------ | -------------------------------------------------------- | | `400` | Parámetro `domain` no proporcionado. | | `401` | API Key inválida. | | `404` | Dominio no encontrado en el proyecto. | --- ## POST /domains/:domain/recreate Elimina y vuelve a crear la identidad de envío del dominio — útil cuando la verificación de DKIM queda atascada en estado fallido. ### Parametros de Ruta | Parámetro | Tipo | Descripción | | --------- | ------ | ------------------------------------ | | `domain` | string | Dominio registrado (ej. `mitienda.com`). | ### Ejemplo ```bash curl -X POST https://api.reallyquickemails.com/domains/mitienda.com/recreate \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ### Respuesta exitosa (200 OK) ```json { "success": true, "identity_id": "8a1f6e2c-3b4d-4f5a-9c6e-7d8f9a0b1c2d", "domain": "mitienda.com", "verification_status": "pending", "verification_token": "abcdef1234567890abcdef1234567890", "dkim_tokens": ["abc123", "def456", "ghi789"], "dns_records": [ { "id": "f1a2b3c4-d5e6-4f70-8a91-b2c3d4e5f607", "record_type": "TXT", "name": "_amazonses", "value": "abcdef1234567890abcdef1234567890", "purpose": "ses_verification", "status": "not_set", "ttl_hint": 300 } ], "message": "Domain identity recreated in SES. DNS records may already be propagated - verify in a few seconds." } ``` La recreación conserva el dominio registrado y el perfil de remitente, regenera los registros DNS y reinicia `verification_status` a `pending`. Después de recrear, llama a `POST /domains/{domain}/verify`. Si los registros DNS ya estaban propagados, la verificación puede completarse en segundos. ### Codigos de Error | Código | Descripción | | ------ | ----------------------------------------------- | | `401` | API Key inválida. | | `404` | Dominio no encontrado en el proyecto. | | `500` | Error al recrear la identidad de envío. | --- ## DELETE /domains/:domain Elimina un dominio inmediatamente de la infraestructura de envío y desactiva el perfil de remitente asociado. ### Parametros de Ruta | Parámetro | Tipo | Descripción | | --------- | ------ | ------------------------------------ | | `domain` | string | Dominio a eliminar (ej. `mitienda.com`). | ### Ejemplo ```bash curl -X DELETE https://api.reallyquickemails.com/domains/mitienda.com \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ### Respuesta exitosa (200 OK) ```json { "success": true, "domain": "mitienda.com", "message": "Domain and associated sender profile removed" } ``` El mismo dominio puede registrarse de nuevo más adelante. > **Advertencia:** Una vez eliminado, deberás volver a registrar el dominio y configurar los registros DNS nuevamente para poder enviar desde él. ### Codigos de Error | Código | Descripción | | ------ | ----------------------------------------------- | | `401` | API Key inválida. | | `404` | Dominio no encontrado en el proyecto. | --- ## PUT /domains/:domain/sender Actualiza el perfil de remitente asociado a un dominio. ### Parametros de Ruta | Parámetro | Tipo | Descripción | | --------- | ------ | ------------------------------------ | | `domain` | string | Dominio asociado (ej. `mitienda.com`). | ### Request Body | Campo | Tipo | Requerido | Descripción | | -------------- | ------ | --------- | ---------------------------------------------- | | `sender_name` | string | Sí | Nombre visible del remitente. | | `sender_email` | string | Sí | Dirección de correo del remitente. Debe terminar en `@{domain}`. | | `reply_email` | string | No | Dirección de correo para respuestas (Reply-To).| ### Ejemplo ```bash curl -X PUT https://api.reallyquickemails.com/domains/mitienda.com/sender \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -d '{ "sender_name": "Equipo Mi Tienda", "sender_email": "ventas@mitienda.com", "reply_email": "soporte@mitienda.com" }' ``` ### Respuesta exitosa (200 OK) ```json { "success": true, "sender_profile_id": "123e4567-e89b-12d3-a456-426614174000", "from_name": "Equipo Mi Tienda", "from_email": "ventas@mitienda.com", "domain_authenticated": true } ``` > **Nota:** El campo `reply_email` se guarda pero no se incluye en la respuesta. ### Codigos de Error | Código | Descripción | | ------ | -------------------------------------------------------- | | `400` | Falta `sender_name` o `sender_email`, o `sender_email` no coincide con el dominio. | | `401` | API Key inválida. | | `404` | Dominio o perfil de remitente no encontrado en el proyecto. | --- ## Verificacion de Email (Magic Link) Alternativa a la verificación de dominio completo. Permite verificar un email individual como remitente sin necesidad de configurar DNS. Se envía un email de verificación al remitente — al hacer clic en el enlace, el email queda verificado para enviar. > **Nota:** La verificación por email es más rápida pero no incluye DKIM/SPF. Para mejor deliverability, se recomienda verificar el dominio completo. Cuando el estado de verificación cambia (de `pending` a `verified` o `failed`), RQE emite el evento de webhook `sender.verified` o `sender.failed`. Ver más en [Webhooks](/api-reference/webhooks). --- ### POST /domains/verify-email Inicia la verificación de un email como remitente enviando un enlace de verificación al email proporcionado. #### Request Body | Campo | Tipo | Requerido | Descripción | | -------------- | ------ | --------- | ---------------------------------------------- | | `sender_email` | string | Sí | Email a verificar como remitente. | | `sender_name` | string | Sí | Nombre visible del remitente. | | `external_key` | string | No | Clave externa para integraciones. | #### Ejemplo ```bash curl -X POST https://api.reallyquickemails.com/domains/verify-email \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "sender_email": "ventas@gmail.com", "sender_name": "Mi Empresa" }' ``` #### Respuesta exitosa (201) ```json { "success": true, "identity_id": "550e8400-e29b-41d4-a716-446655440000", "sender_email": "ventas@gmail.com", "verification_status": "pending", "sender_profile_id": "123e4567-e89b-12d3-a456-426614174000", "sender": { "from_name": "Mi Empresa", "from_email": "ventas@gmail.com", "email_verified": false, "domain_authenticated": false }, "message": "Verification email sent. The sender must click the link in the email to verify." } ``` El remitente recibirá un email con un enlace. Al hacer clic, su email queda verificado. #### Errores | Código | Descripción | |---|---| | `400` | `sender_email` o `sender_name` faltantes, o formato de email inválido | | `401` | API Key inválida | | `409` | Email ya registrado en este proyecto | --- ### GET /domains/verify-email/status Consulta el estado de verificación de un email contra la infraestructura de envío en tiempo real. #### Query Parameters | Parámetro | Tipo | Requerido | Descripción | | --------- | ------ | --------- | ------------------------------ | | `email` | string | Sí | Email a consultar. | #### Ejemplo ```bash curl "https://api.reallyquickemails.com/domains/verify-email/status?email=ventas@gmail.com" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` #### Respuesta exitosa (200) ```json { "success": true, "identity_id": "550e8400-e29b-41d4-a716-446655440000", "sender_email": "ventas@gmail.com", "verification_status": "verified", "sender_profile": { "id": "123e4567-e89b-12d3-a456-426614174000", "from_name": "Mi Empresa", "from_email": "ventas@gmail.com", "email_verified": true, "domain_authenticated": false }, "can_send": true } ``` `sender_profile` puede ser `null` si no existe un perfil de remitente activo. `can_send` es `true` cuando `verification_status` es `verified`. #### Estados de verificacion | Estado | Descripción | |---|---| | `pending` | Email de verificación enviado, esperando clic del remitente | | `verified` | Remitente verificado, listo para enviar | | `failed` | Verificación fallida | #### Errores | Código | Descripción | |---|---| | `400` | `email query parameter is required` | | `401` | API Key inválida | | `404` | Email no encontrado en el proyecto | --- ### POST /domains/verify-email/resend Reenvía el email de verificación al remitente — solo es válido mientras la verificación está en estado `pending`. #### Request Body | Campo | Tipo | Requerido | Descripción | | -------------- | ------ | --------- | ------------------------------ | | `sender_email` | string | Sí | Email al que reenviar. | #### Ejemplo ```bash curl -X POST https://api.reallyquickemails.com/domains/verify-email/resend \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "sender_email": "ventas@gmail.com" }' ``` #### Respuesta exitosa (200) ```json { "success": true, "message": "Verification email resent", "sender_email": "ventas@gmail.com" } ``` #### Errores | Código | Descripción | |---|---| | `400` | `sender_email` faltante, o la verificación no está en estado `pending` | | `401` | API Key inválida | | `404` | Email no encontrado en el proyecto | --- ### DELETE /domains/verify-email Elimina un email verificado como remitente; el mismo email puede registrarse de nuevo más adelante. #### Query Parameters | Parámetro | Tipo | Requerido | Descripción | | --------- | ------ | --------- | ------------------------------ | | `email` | string | Sí | Email a eliminar. | #### Ejemplo ```bash curl -X DELETE "https://api.reallyquickemails.com/domains/verify-email?email=ventas@gmail.com" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` #### Respuesta exitosa (200) ```json { "success": true, "sender_email": "ventas@gmail.com", "message": "Email identity and associated sender profile removed" } ``` #### Errores | Código | Descripción | |---|---| | `400` | `email query parameter is required` | | `401` | API Key inválida | | `404` | Email no encontrado en el proyecto | # Events (/api-reference/events) Trackea eventos desde tu aplicación hacia ReallyQuickEmails para segmentación y automatizaciones. Los eventos se asocian automáticamente a leads — si el email no existe, RQE crea un lead nuevo con datos vacíos. ## Autenticacion ``` Authorization: Bearer sk_proj_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Ver más en [API Pública v1](/api-reference/public-api#autenticacion). --- ## Endpoints ### POST /v1/events Trackea un evento individual. **Parámetros del body:** | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | email | string | Sí | Email del lead. Se normaliza (trim + minúsculas) y se valida el formato | | event | string | Sí | Nombre del evento — nomenclatura libre, usa la que tenga sentido en tu app | | properties | object | No | Propiedades custom del evento. Debe ser un objeto, no un array | | timestamp | ISO 8601 | No | Cuándo ocurrió (default: ahora) | **Request:** ```bash curl -X POST https://api.reallyquickemails.com/v1/events \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "email": "jane@acme.com", "event": "bought_stuff", "properties": { "item": "shoes", "total": 89.99, "category": "running" }, "timestamp": "2026-04-10T15:00:00Z" }' ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: 'sk_proj_xxxxxxxxxxxx' }); const { data, error } = await rqe.events.track({ email: 'jane@acme.com', event: 'bought_stuff', properties: { item: 'shoes', total: 89.99, category: 'running' }, timestamp: '2026-04-10T15:00:00Z', }); ``` **Response (201):** ```json { "success": true, "event_id": "uuid", "recipient_id": "uuid", "created_lead": true } ``` `created_lead: true` = el email no existía y se creó un lead nuevo automáticamente. **Errores:** | Código | Significado | |---|---| | 400 | Validación fallida: campo faltante, email inválido o `properties` no es objeto | Ver más en [Leads](/api-reference/leads). --- ### POST /v1/events/bulk Trackea hasta 1,000 eventos en una sola petición. **Parámetros del body:** | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | events | array | Sí | Hasta 1,000 eventos. Cada elemento acepta los mismos campos que `POST /v1/events`: `email` y `event` requeridos; `properties` y `timestamp` opcionales | **Request:** ```bash curl -X POST https://api.reallyquickemails.com/v1/events/bulk \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "email": "jane@acme.com", "event": "signed_up" }, { "email": "mike@store.co", "event": "bought_stuff", "properties": { "total": 50 } }, { "email": "sarah@clinic.com", "event": "appt_done", "properties": { "service": "cleaning" } } ] }' ``` ```ts const { data, error } = await rqe.events.bulk([ { email: 'jane@acme.com', event: 'signed_up' }, { email: 'mike@store.co', event: 'bought_stuff', properties: { total: 50 } }, { email: 'sarah@clinic.com', event: 'appt_done', properties: { service: 'cleaning' } }, ]); ``` **Response (201):** ```json { "success": true, "total": 3, "created_leads": 1 } ``` - `total`: cantidad de eventos registrados. - `created_leads`: cantidad de leads nuevos creados automáticamente (emails únicos que no existían). **Errores:** | Código | Significado | |---|---| | 400 | Más de 1,000 eventos, o un elemento inválido. La validación es por elemento: la respuesta indica el índice (por ejemplo, `events[2].email is required`) y no se registra ningún evento | --- ### GET /v1/events Lista eventos con filtros; los resultados se ordenan por `timestamp` descendente (más recientes primero). **Parámetros de query:** | Parámetro | Tipo | Default | Descripción | |---|---|---|---| | email | string | — | Filtrar por email (se normaliza antes de comparar) | | event | string | — | Filtrar por nombre de evento | | since | ISO 8601 | — | Eventos desde esta fecha (inclusive) | | until | ISO 8601 | — | Eventos hasta esta fecha (inclusive) | | page | number | 1 | Página | | per_page | number | 50 | Resultados por página (mín 1, máx 200) | **Request:** ```bash curl "https://api.reallyquickemails.com/v1/events?email=jane@acme.com&event=bought_stuff&since=2026-04-01" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ```ts const { data, error } = await rqe.events.list({ email: 'jane@acme.com', event: 'bought_stuff', since: '2026-04-01', }); ``` **Response (200):** ```json { "success": true, "data": [ { "id": "uuid", "email": "jane@acme.com", "event": "bought_stuff", "properties": { "item": "shoes", "total": 89.99 }, "timestamp": "2026-04-10T15:00:00Z", "recipient_id": "uuid", "created_at": "2026-04-10T15:00:01Z" } ], "pagination": { "page": 1, "per_page": 50, "total": 142, "total_pages": 3 } } ``` Ver más en [SDK de Node.js](/guides/sdk-nodejs). --- ## Errores Las respuestas de error tienen el formato `{ "error": "mensaje" }`. Códigos comunes a todos los endpoints: | Código | Significado | |---|---| | 401 | API key faltante o inválida | | 500 | Error interno (incluye campo adicional `details`) | # Leads (/api-reference/leads) Gestiona los leads (contactos) de tu proyecto de ReallyQuickEmails vía API: CRUD, segmentos, tags y atributos. ## Autenticacion Todas las peticiones requieren un Bearer token: ``` Authorization: Bearer sk_proj_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Ver más en [API Pública v1](/api-reference/public-api#autenticacion). --- ## Endpoints ### POST /v1/leads Crea o actualiza uno o muchos leads. Si el email ya existe en el proyecto, se actualiza (upsert). | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | email | string | Sí (si no hay `leads`) | Email del lead | | data | object | No | Atributos custom (nombre, teléfono, etc.) | | segment_ids | string[] | No | UUIDs de segmentos a asignar | | leads | array | Sí (si no hay `email`) | Array de leads para bulk (máximo 1,000) | Si el mismo email se repite dentro de una petición bulk, se deduplica y gana la última ocurrencia. ```bash # Individual curl -X POST https://api.reallyquickemails.com/v1/leads \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "email": "jane@acme.com", "data": { "name": "Jane", "phone": "+1234567890" }, "segment_ids": ["uuid-segmento-1"] }' # Bulk (hasta 1,000) curl -X POST https://api.reallyquickemails.com/v1/leads \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "leads": [ { "email": "jane@acme.com", "data": { "name": "Jane" } }, { "email": "mike@store.co", "data": { "name": "Mike" }, "segment_ids": ["uuid"] } ] }' ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); // Individual await rqe.leads.upsert({ email: 'jane@acme.com', data: { name: 'Jane', phone: '+1234567890' }, segment_ids: ['uuid-segmento-1'], }); // Bulk (hasta 1,000) await rqe.leads.upsertMany({ leads: [ { email: 'jane@acme.com', data: { name: 'Jane' } }, { email: 'mike@store.co', data: { name: 'Mike' }, segment_ids: ['uuid'] }, ], }); ``` **Response (201):** ```json { "success": true, "total": 2, "leads": [ { "id": "uuid", "email": "jane@acme.com", "created": true }, { "id": "uuid", "email": "mike@store.co", "created": false } ] } ``` `created: true` = lead nuevo. `created: false` = lead existente actualizado. Si algún `segment_id` no existe en el proyecto, los leads se crean igual y la respuesta incluye un campo adicional `warnings` (array de strings) con los segmentos no encontrados. --- ### GET /v1/leads Lista leads con paginación, ordenados por fecha de creación descendente. | Parámetro | Tipo | Default | Descripción | |---|---|---|---| | page | number | 1 | Página | | per_page | number | 50 | Resultados por página (máx. 200) | | search | string | — | Buscar por email o nombre | | segment_id | UUID | — | Filtrar por segmento | ```bash curl "https://api.reallyquickemails.com/v1/leads?page=1&per_page=50" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.leads.list({ page: 1, per_page: 50 }); ``` **Response (200):** ```json { "success": true, "data": [ { "id": "uuid", "email": "jane@acme.com", "data": { "name": "Jane" }, "created_at": "...", "updated_at": "..." } ], "pagination": { "page": 1, "per_page": 50, "total": 1234, "total_pages": 25 } } ``` --- ### GET /v1/leads/:id Obtén un lead por ID (UUID), incluyendo sus segmentos. ```bash curl https://api.reallyquickemails.com/v1/leads/uuid-del-lead \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.leads.get('uuid-del-lead'); ``` **Response (200):** ```json { "success": true, "lead": { "id": "uuid", "email": "jane@acme.com", "data": { "name": "Jane" }, "created_at": "...", "updated_at": "...", "shopify_customer_id": null, "segment_ids": ["uuid-1", "uuid-2"] } } ``` `shopify_customer_id` es el ID del cliente en Shopify cuando el lead proviene de la sincronización con Shopify; `null` en caso contrario. --- ### PUT /v1/leads/:id Actualiza un lead. Al menos uno de `email`, `data`, o `segment_ids` es requerido. | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | email | string | No* | Nuevo email del lead | | data | object | No* | **Reemplaza** el objeto completo de atributos (no hace merge) | | segment_ids | string[] | No* | **Reemplaza** todas las memberships existentes. Los IDs de segmentos que no existen en el proyecto se ignoran | \*Al menos uno de los tres campos es requerido. Para merge parcial de atributos, ver más en [Attributes](#attributes). ```bash curl -X PUT https://api.reallyquickemails.com/v1/leads/uuid-del-lead \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "email": "new@acme.com", "data": { "name": "Jane Updated", "plan": "pro" }, "segment_ids": ["uuid-1"] }' ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.leads.update('uuid-del-lead', { email: 'new@acme.com', data: { name: 'Jane Updated', plan: 'pro' }, segment_ids: ['uuid-1'], }); ``` **Response (200):** ```json { "success": true, "lead": { "id": "uuid", "email": "new@acme.com", "data": { "name": "Jane Updated", "plan": "pro" }, "created_at": "...", "updated_at": "...", "segment_ids": ["uuid-1"] } } ``` **Errores:** si el nuevo `email` ya existe en otro lead del proyecto, responde `409`. --- ### DELETE /v1/leads/:id Elimina un lead y sus memberships de segmentos. ```bash curl -X DELETE https://api.reallyquickemails.com/v1/leads/uuid-del-lead \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.leads.delete('uuid-del-lead'); ``` **Response (200):** ```json { "success": true } ``` --- ## Segmentos ### POST /v1/leads/:id/segments Agrega un lead a uno o más segmentos. | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | segment_ids | string[] | Sí | UUIDs de los segmentos a agregar | ```bash curl -X POST https://api.reallyquickemails.com/v1/leads/uuid-del-lead/segments \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "segment_ids": ["uuid-1", "uuid-2"] }' ``` **Response (200):** ```json { "success": true, "added": 2 } ``` **Errores:** si algún segmento no existe en el proyecto, responde `404` y no agrega ninguno. --- ### DELETE /v1/leads/:id/segments/:segmentId Quita un lead de un segmento. ```bash curl -X DELETE https://api.reallyquickemails.com/v1/leads/uuid-del-lead/segments/uuid-del-segmento \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` **Response (200):** ```json { "success": true } ``` **Errores:** si el lead no pertenece al segmento, responde `404`. --- ## Tags Tags son etiquetas simples de texto. Se almacenan como un array bajo la clave reservada `_tags` dentro de los atributos (`data`) del lead. El `:email` en la ruta debe ir URL-encoded (por ejemplo, `jane%40acme.com`). ### POST /v1/leads/:email/tags Agrega tags a un lead; se combinan con los existentes, sin duplicados. | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | tags | string[] | Sí | Tags a agregar | ```bash curl -X POST https://api.reallyquickemails.com/v1/leads/jane%40acme.com/tags \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "tags": ["vip", "spanish_speaker", "hot_lead"] }' ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.leads.addTags('jane@acme.com', ['vip', 'spanish_speaker', 'hot_lead']); ``` **Response (200):** ```json { "success": true, "tags": ["vip", "spanish_speaker", "hot_lead"] } ``` --- ### DELETE /v1/leads/:email/tags Quita tags de un lead. | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | tags | string[] | Sí | Tags a quitar | ```bash curl -X DELETE https://api.reallyquickemails.com/v1/leads/jane%40acme.com/tags \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "tags": ["hot_lead"] }' ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.leads.removeTags('jane@acme.com', ['hot_lead']); ``` **Response (200):** ```json { "success": true, "tags": ["vip", "spanish_speaker"] } ``` --- ### GET /v1/leads/:email/tags Lista los tags de un lead. ```bash curl https://api.reallyquickemails.com/v1/leads/jane%40acme.com/tags \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.leads.getTags('jane@acme.com'); ``` **Response (200):** ```json { "success": true, "tags": ["vip", "spanish_speaker"] } ``` --- ## Attributes Atributos custom almacenados en el campo `data` del lead. Se hace merge (shallow) con los existentes; la clave reservada `_tags` se preserva. El `:email` en la ruta debe ir URL-encoded. ### POST /v1/leads/:email/attributes Establece atributos custom en un lead. El body es un objeto JSON plano con los atributos a establecer. ```bash curl -X POST https://api.reallyquickemails.com/v1/leads/jane%40acme.com/attributes \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "plan": "pro", "mrr": 49, "team_size": 12 }' ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.leads.setAttributes('jane@acme.com', { plan: 'pro', mrr: 49, team_size: 12 }); ``` **Response (200):** ```json { "success": true, "data": { "plan": "pro", "mrr": 49, "team_size": 12, "name": "Jane" } } ``` --- ### GET /v1/leads/:email/attributes Devuelve los atributos del lead, excluyendo la clave reservada `_tags`. ```bash curl https://api.reallyquickemails.com/v1/leads/jane%40acme.com/attributes \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.leads.getAttributes('jane@acme.com'); ``` **Response (200):** ```json { "success": true, "data": { "plan": "pro", "mrr": 49, "team_size": 12, "name": "Jane" } } ``` --- ## Errores | Código | Significado | |---|---| | 400 | Validación fallida (campo requerido, formato inválido, ID no es UUID) | | 401 | API key inválida o faltante | | 404 | Lead o segmento no encontrado | | 409 | Email ya existe en otro lead del proyecto | | 500 | Error interno | Formato de error: ```json { "error": "Descripción del error" } ``` Los errores `500` pueden incluir un campo adicional `details` con el detalle del error. Ver más en la guía del [SDK de Node.js](/guides/sdk-nodejs). # API Pública v1 (/api-reference/public-api) La API pública v1 de ReallyQuickEmails (RQE) expone endpoints autenticados mediante Bearer token para integraciones externas. **Base URL:** `https://api.reallyquickemails.com` --- ## Autenticacion Todos los endpoints de la API pública requieren autenticación mediante Bearer token con una clave del proyecto: ``` Authorization: Bearer sk_proj_xxxxxxxxxxxx ``` Se aceptan tres prefijos de clave: | Prefijo | Modo | | ------------ | ----------------------------------------- | | `sk_proj_...` | Producción (live) | | `sk_live_...` | Producción (live) | | `sk_test_...` | [Modo de prueba](/concepts/test-mode) | Las claves se generan desde el panel de administración del proyecto. Si la clave es inválida o no se proporciona, el servidor responde con `401 Unauthorized`. Todos los requests con body requieren además el header `Content-Type: application/json`. ### Respuesta de error de autenticacion ```json { "error": "Missing or invalid API key. Use: Authorization: Bearer sk_proj_..." } ``` Si la clave tiene un prefijo válido pero no corresponde a ningún proyecto, la respuesta es: ```json { "error": "Invalid API key" } ``` --- ## POST /v1/send-email Envía un email individual a hasta 50 destinatarios. ### Request Body | Campo | Tipo | Requerido | Descripción | | ----------------- | ------------------- | --------- | ----------------------------------------------------------------------------------------------------------------- | | `recipient_email` | string \| string[] | Si | Dirección(es) del destinatario. Acepta string (1 destinatario) o array (máx 50). Para volúmenes mayores usa `/v1/send-batch`. | | `sender_email` | string | Si | Dirección del remitente (dominio verificado). | | `sender_name` | string | No | Nombre visible del remitente. Si se omite, el inbox muestra solo `sender_email`. | | `html_body` | string | Si | Contenido HTML del correo. | | `text_body` | string | No | Versión plain text del correo (multipart fallback). Recomendado para mejorar deliverability y accessibility. | | `subject` | string | No | Asunto del correo. Si se omite, se usa `"Email from ReallyQuickEmails"`. | | `cc` | string \| string[] | No | Dirección(es) en copia. Máx 10. | | `bcc` | string \| string[] | No | Dirección(es) en copia oculta. Máx 10. | | `attachments` | array | No | Lista de adjuntos (máx 10; máx 10 MB por adjunto y 10 MB en total). Estructura: `{ filename, url \| content (base64), contentType? }`. | | `environment` | string | No | Enruta los webhooks de este envío al environment configurado en el proyecto. Formato `[a-zA-Z0-9_.-]`, máx 64 caracteres. Si la key no está pre-configurada en el dashboard, retorna 400 `ENVIRONMENT_NOT_CONFIGURED`. Ver [Webhook environments](/concepts/webhook-environments). | > El tracking de aperturas y clics se aplica en segundo plano (los remitentes en modo transaccional omiten el tracking). Este endpoint **no** agrega footer de unsubscribe ni headers `List-Unsubscribe` — esos se agregan automáticamente en campañas y en `/v1/send-template-email`; en `/v1/send-batch` puedes pasarlos mediante `custom_headers`. ### Ejemplo simple ```bash curl -X POST https://api.reallyquickemails.com/v1/send-email \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient_email": "usuario@ejemplo.com", "sender_email": "hola@miapp.com", "sender_name": "Mi Empresa", "subject": "Bienvenido a nuestra plataforma", "html_body": "

Bienvenido!

Gracias por registrarte.

" }' ```
### Ejemplo con múltiples destinatarios + cc + adjunto ```bash curl -X POST https://api.reallyquickemails.com/v1/send-email \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient_email": ["a@empresa.com", "b@empresa.com"], "cc": "supervisor@empresa.com", "sender_email": "notificaciones@miapp.com", "sender_name": "Mi Empresa", "subject": "Reporte diario", "html_body": "

Reporte

Adjunto encontrarás el detalle.

", "attachments": [ { "filename": "reporte.pdf", "url": "https://storage.miapp.com/reportes/2026-04-28.pdf", "contentType": "application/pdf" } ] }' ```
### Respuesta exitosa (200) ```json { "message": "Email sent successfully", "email_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "123e4567-e89b-12d3-a456-426614174000" } ``` `email_id` es el ID de actividad del envío — úsalo para correlacionar los eventos que llegan a tus [webhooks](./webhooks). Puede ser `null` si el registro de actividad no pudo crearse. > **Supresión:** la verificación de supresión ocurre al procesar el envío en segundo plano. Si el destinatario está suprimido (rebote, queja o baja), el email no se envía y la actividad queda con estado `suppressed`. Este endpoint no devuelve una respuesta `skipped` síncrona — `/v1/send-template-email` sí verifica la supresión antes de responder. ### Codigos de Error | Código | Descripción | | ------ | ----------------------------------------------------------------------- | | `400` | `Missing required fields: recipient_email, sender_email, html_body` | | `400` | `recipient_email must be a non-empty string or array of strings` | | `400` | `Too many recipients in recipient_email (N). Maximum is 50. For larger sends use /v1/send-batch.` | | `400` | `cc accepts at most 10 addresses` / `bcc accepts at most 10 addresses` | | `400` | `attachments must be an array` / `attachments accepts at most 10 items` | | `400` | `ENVIRONMENT_NOT_CONFIGURED` — el `environment` no está configurado en el proyecto. | | `401` | API Key inválida o no proporcionada. | | `500` | Error interno del servidor. | | `502` | `Failed to process request` — error al procesar la solicitud internamente. | ¿Necesitas envío programado, `dry_run` o plantillas en envíos individuales? Ver más en [API avanzada de envío](./send-email). --- ## POST /v1/send-template-email Envía un correo utilizando una plantilla pre-configurada con sustitución de variables (soporta loops `{{#each}}`). ### Request Body | Campo | Tipo | Requerido | Descripción | | ---------------------- | ------ | --------- | ---------------------------------------------------------------- | | `template_id` | string | Si* | UUID de la plantilla. | | `template_internal_id` | number | Si* | ID interno de la plantilla (alternativa a `template_id`). | | `html_body` | string | Si* | Contenido HTML directo con variables (modo sin plantilla). Requerido si no envías `template_id` ni `template_internal_id`. | | `recipient_email` | string | Si | Dirección de correo del destinatario. | | `sender_email` | string | Si | Dirección de correo del remitente (dominio verificado). | | `sender_name` | string | No | Nombre visible del remitente. | | `subject` | string | No | Asunto del correo. Admite variables. Si se omite con plantilla, se usa el de la plantilla. | | `variables` | object | No | Variables para sustitución en la plantilla. | | `environment` | string | No | Igual que en `/v1/send-email`: enruta los webhooks al environment configurado. | > *Debes enviar `template_id` O `template_internal_id`. Si no envías ninguno, `html_body` (junto con `recipient_email` y `sender_email`) es requerido. Este endpoint agrega automáticamente: - Un footer de cancelación de suscripción si el HTML no incluye la URL de unsubscribe. - Los headers `List-Unsubscribe` y `List-Unsubscribe-Post` (RFC 8058). - Las variables `view_in_browser_url` y `unsubscribe_url` (si no las envías tú). Las respuestas del destinatario se enrutan a la bandeja del remitente — ver [Reply-To Automático](#reply-to-automatico-inbound-email). ### Ejemplo con template UUID ```bash curl -X POST https://api.reallyquickemails.com/v1/send-template-email \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "template_id": "550e8400-e29b-41d4-a716-446655440000", "recipient_email": "nuevo.usuario@ejemplo.com", "sender_email": "onboarding@miapp.com", "variables": { "nombre": "Carlos", "plan": "Pro", "trial_days": 14, "dashboard_url": "https://miapp.com/dashboard" } }' ``` ### Ejemplo con template internal ID Cada template tiene un ID interno auto-incrementado dentro del proyecto. Útil para integraciones que prefieren IDs numéricos: ```bash curl -X POST https://api.reallyquickemails.com/v1/send-template-email \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "template_internal_id": 5, "recipient_email": "cliente@ejemplo.com", "sender_email": "noreply@miapp.com", "variables": { "nombre": "Maria" } }' ``` ### Respuesta exitosa (200) ```json { "message": "Email sent successfully", "email_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "123e4567-e89b-12d3-a456-426614174000", "template_id": "550e8400-e29b-41d4-a716-446655440000", "template_internal_id": null, "variables_used": ["nombre", "plan", "trial_days", "dashboard_url", "view_in_browser_url", "unsubscribe_url"], "used_cached_html": true } ``` `variables_used` incluye también las variables inyectadas automáticamente (`view_in_browser_url`, `unsubscribe_url`). `used_cached_html` es `true` cuando se usó el HTML pre-renderizado de la plantilla. ### Respuesta cuando el destinatario esta suprimido (200) Si el destinatario se ha dado de baja o ha rebotado, el email se omite automáticamente: ```json { "message": "Email skipped — recipient is suppressed", "skipped": true, "suppression_reason": "unsubscribed", "recipient": "cliente@ejemplo.com" } ``` ### Codigos de Error | Código | Descripción | | ------ | ----------------------------------------------------------------------- | | `400` | `Missing required fields: template_id/template_internal_id, OR recipient_email + sender_email + html_body` | | `400` | `Missing required fields: recipient_email, sender_email` | | `400` | `template_internal_id requires valid project context` | | `400` | `ENVIRONMENT_NOT_CONFIGURED` — el `environment` no está configurado en el proyecto. | | `401` | API Key inválida o no proporcionada. | | `404` | `Template not found with ID ...` — plantilla no encontrada para el proyecto asociado a la API Key. | | `422` | `TEMPLATE_EMPTY` — la plantilla renderiza a contenido vacío. Vuelve a guardar la plantilla en el editor para regenerar el HTML. | | `500` | Error interno del servidor. | Ver más en [Plantillas](/guides/templates). --- ## POST /v1/send-batch Envía correos masivos en un solo request — hasta **10,000 destinatarios** por llamada. ### Request Body | Campo | Tipo | Requerido | Descripción | | ---------------- | ----------------- | --------- | ------------------------------------------------------------------- | | `sender` | string | Si | Dirección de correo del remitente (dominio verificado). | | `senderName` | string | No | Nombre visible del remitente. | | `subject` | string | Si* | Asunto del correo. Requerido si no se usa `templateId`. | | `html` | string | Si* | Contenido HTML. Requerido si no se usa `templateId`. | | `templateId` | string | Si* | UUID del template. Alternativa a `subject` + `html`. | | `email_type` | string | No | Tipo de correo. Default: `"marketing"`. | | `scheduled_at` | string | No | Fecha ISO 8601 para envío programado. | | `custom_headers` | object | No | Headers a nivel batch. Solo se aplican `List-Unsubscribe` y `List-Unsubscribe-Post`. | | `environment` | string | No | Enruta los webhooks al environment configurado. Formato `[a-zA-Z0-9_.-]`, máx 64 caracteres. | | `dry_run` | boolean | No | Previsualiza el batch sin encolar emails ni crear registros de actividad. Ver [Dry run](#dry-run). | | `recipients` | array | Si | Lista de destinatarios (máximo 10,000). | > *Debes enviar `templateId` O ambos `subject` + `html`. **Estructura de cada recipient:** | Campo | Tipo | Requerido | Descripción | | ---------------- | ------ | --------- | ---------------------------------------- | | `email` | string | Si | Dirección de correo del destinatario. | | `data` | object | No | Variables personalizadas (Handlebars). | | `custom_headers` | object | No | Headers por destinatario. Sobrescriben los del batch. | ### Headers | Header | Tipo | Requerido | Descripción | | ----------------- | ------ | --------- | --------------------------------- | | `Idempotency-Key` | string | No | Clave de idempotencia (1–256 caracteres). Ver [Idempotencia](#idempotencia). | ### Ejemplo con HTML directo ```bash curl -X POST https://api.reallyquickemails.com/v1/send-batch \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "sender": "noreply@tudominio.com", "senderName": "Mi Empresa", "subject": "Oferta especial para ti, {nombre}", "html": "

Hola {nombre}!

Tenemos una oferta especial en {producto}.

", "recipients": [ { "email": "juan@ejemplo.com", "data": { "nombre": "Juan", "producto": "Plan Pro" } }, { "email": "maria@ejemplo.com", "data": { "nombre": "Maria", "producto": "Plan Business" } } ] }' ```
```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); const { data, error } = await rqe.emails.sendBatch({ sender: 'noreply@tudominio.com', senderName: 'Mi Empresa', subject: 'Oferta especial para ti, {nombre}', html: '

Hola {nombre}!

Tenemos una oferta especial en {producto}.

', recipients: [ { email: 'juan@ejemplo.com', data: { nombre: 'Juan', producto: 'Plan Pro' } }, { email: 'maria@ejemplo.com', data: { nombre: 'Maria', producto: 'Plan Business' } }, ], }); if (error) console.error(error); else console.log('Batch encolado:', data.batch_id); ```
### Ejemplo con template ```bash curl -X POST https://api.reallyquickemails.com/v1/send-batch \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "sender": "noreply@tudominio.com", "senderName": "Mi Empresa", "templateId": "550e8400-e29b-41d4-a716-446655440000", "recipients": [ { "email": "juan@ejemplo.com", "data": { "nombre": "Juan" } }, { "email": "maria@ejemplo.com", "data": { "nombre": "Maria" } } ] }' ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.emails.sendBatch({ sender: 'noreply@tudominio.com', senderName: 'Mi Empresa', templateId: '550e8400-e29b-41d4-a716-446655440000', recipients: [ { email: 'juan@ejemplo.com', data: { nombre: 'Juan' } }, { email: 'maria@ejemplo.com', data: { nombre: 'Maria' } }, ], }); ``` ### Ejemplo con envio programado ```bash curl -X POST https://api.reallyquickemails.com/v1/send-batch \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "sender": "noreply@tudominio.com", "subject": "Newsletter semanal", "html": "

Newsletter

Las noticias de esta semana...

", "scheduled_at": "2026-03-25T10:00:00Z", "recipients": [ { "email": "sub1@ejemplo.com" }, { "email": "sub2@ejemplo.com" } ] }' ```
```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); await rqe.emails.sendBatch({ sender: 'noreply@tudominio.com', subject: 'Newsletter semanal', html: '

Newsletter

Las noticias de esta semana...

', scheduled_at: '2026-03-25T10:00:00Z', recipients: [ { email: 'sub1@ejemplo.com' }, { email: 'sub2@ejemplo.com' }, ], }); ```
### Respuesta exitosa (200) ```json { "batch_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "total": 2, "queued": 2, "scheduled": false, "scheduled_for": null, "activities": [ { "email": "juan@ejemplo.com", "activity_id": "uuid-1" }, { "email": "maria@ejemplo.com", "activity_id": "uuid-2" } ] } ``` | Campo | Tipo | Descripción | |---|---|---| | `batch_id` | string | UUID único del batch para referencia. | | `total` | number | Cantidad total de destinatarios. | | `queued` | number | Cantidad de emails encolados para envío. | | `scheduled` | boolean | `true` si `scheduled_at` está en el futuro. | | `scheduled_for` | string \| null | El valor de `scheduled_at` enviado, o `null` si es inmediato. | | `activities` | array | Lista con `email` y `activity_id` por destinatario para rastreo. | El `activity_id` de cada destinatario permite rastrear el estado de entrega mediante webhooks. > **Supresión:** igual que en `/v1/send-email`, la supresión se aplica al procesar cada envío en segundo plano — los destinatarios suprimidos se omiten y su actividad queda con estado `suppressed`. ### Idempotencia Envía el header opcional `Idempotency-Key` (1–256 caracteres) para evitar envíos duplicados ante reintentos. Las respuestas con status 2xx o 4xx se cachean por **24 horas**, con alcance por proyecto. Si repites la misma clave dentro de ese plazo, recibes la respuesta cacheada sin re-procesar el batch, junto con el header `Idempotency-Replayed: true`. En el SDK de Node.js, pasa `{ idempotencyKey }` como segundo argumento de `rqe.emails.sendBatch(params, { idempotencyKey })`. Ver más en [SDK de Node.js](/guides/sdk-nodejs). ### Dry run Con `"dry_run": true` la API devuelve una previsualización sin encolar emails ni crear registros de actividad: ```json { "dry_run": true, "test_mode": false, "recipients_count": 2, "would_send": { "template_id": null, "subject": "Oferta especial para ti, {nombre}", "from": "Mi Empresa ", "first_recipient": "juan@ejemplo.com" } } ``` ### Limites | Límite | Valor | |---|---| | Destinatarios por request | 10,000 | | Cache de idempotencia | 24 horas | > **Tip:** Para envíos superiores a 10,000 destinatarios, usa múltiples llamadas o crea una campaña desde el Dashboard de RQE, que maneja automáticamente lotes y reintentos. Ver más en [Campañas](./campaigns). ### Codigos de Error | Código | Descripción | | ------ | ----------------------------------------------------------------------- | | `400` | `sender is required` | | `400` | `subject or templateId is required` | | `400` | `html or templateId is required` | | `400` | `recipients array is required and must not be empty` | | `400` | `Too many recipients: X. Maximum is 10,000 per batch.` | | `400` | `recipients[N].email is required` | | `400` | `environment must be a string of [a-zA-Z0-9_.-] up to 64 chars` | | `400` | `ENVIRONMENT_NOT_CONFIGURED` — el `environment` no está configurado en el proyecto. | | `401` | API Key inválida o no proporcionada. | | `500` | `Failed to create activity records` / `Failed to process batch` | --- ## Reply-To Automatico (Inbound Email) Los correos enviados vía `/v1/send-email` y `/v1/send-batch` incluyen automáticamente un header `Reply-To` con el nombre del remitente: ``` Reply-To: "Mi Empresa" ``` Los clientes de correo (Gmail, Outlook, Apple Mail) muestran el **nombre del remitente**, no la dirección técnica. Cuando el destinatario responde, la respuesta se enruta automáticamente a RQE y se asocia al hilo de conversación original. Este comportamiento es automático y no requiere configuración. Para recibir notificaciones de respuestas entrantes, configura un [webhook de inbound email](./webhooks#inbound-replies-con-adjuntos). En `/v1/send-template-email` las respuestas van directo a la bandeja del remitente: el `Reply-To` usa la dirección de respuesta configurada en el perfil del remitente, o el propio `sender_email` si no hay una. --- ## APIs de Gestion de Datos Además de los endpoints de envío, la API v1 incluye endpoints para gestionar leads, eventos, tags y atributos. Consulta la documentación completa en: - **[Leads API](./leads)** — CRUD de leads, segmentos, tags y atributos - **[Events API](./events)** — Tracking de eventos custom desde tus apps ### Resumen rapido | Categoría | Endpoint | Descripción | |---|---|---| | **Leads** | `POST /v1/leads` | Crear/actualizar leads (single o bulk hasta 1,000) | | | `GET /v1/leads` | Listar con paginación y filtros | | | `GET /v1/leads/:id` | Detalle de un lead con segmentos | | | `PUT /v1/leads/:id` | Actualizar lead | | | `DELETE /v1/leads/:id` | Eliminar lead | | **Segmentos** | `POST /v1/leads/:id/segments` | Agregar lead a segmentos | | | `DELETE /v1/leads/:id/segments/:segmentId` | Quitar de segmento | | **Tags** | `POST /v1/leads/:email/tags` | Agregar tags | | | `DELETE /v1/leads/:email/tags` | Quitar tags | | | `GET /v1/leads/:email/tags` | Listar tags | | **Atributos** | `POST /v1/leads/:email/attributes` | Set/merge atributos custom | | | `GET /v1/leads/:email/attributes` | Obtener atributos | | **Eventos** | `POST /v1/events` | Trackear un evento | | | `POST /v1/events/bulk` | Trackear hasta 1,000 eventos | | | `GET /v1/events` | Listar eventos con filtros | # Enviar Email (/api-reference/send-email) `POST /send-email` Encola un correo para envío inmediato o lo programa para una fecha futura. > **Nota:** Esta es la **API avanzada** de ReallyQuickEmails (campos `recipient`/`sender`/`html`): es la única que soporta envío programado (`scheduled_at`), plantillas (`templateId`) y `dry_run`. Para integraciones simples se recomienda la [API v1](./public-api) (`POST /v1/send-email`), con nombres de campo en convención REST (`recipient_email`/`sender_email`/`html_body`) y contrato estable. ## Autenticacion Autentica con Bearer token usando una API key del proyecto: ``` Authorization: Bearer sk_proj_xxxxxxxxxxxx ``` Se aceptan claves `sk_proj_*`, `sk_live_*` (modo Live) y `sk_test_*` (modo Test). El `project_id` se infiere automáticamente desde la key — no es necesario pasarlo como header. Ver más en [Autenticación](/guides/authentication). ## Request Body | Campo | Tipo | Requerido | Descripción | | ------------------ | ----------------- | --------- | --------------------------------------------------------------------------- | | `html` | string | Sí* | Contenido HTML del correo. Requerido si no se usa `templateId`. | | `subject` | string | Sí | Asunto del correo. | | `recipient` | string \| string[] | Sí | Dirección de correo del destinatario, o un array de direcciones. | | `sender` | string | Sí | Dirección de correo del remitente. | | `senderName` | string | No | Nombre visible del remitente. | | `templateId` | string | No | ID de una plantilla almacenada. Si se proporciona, se usa en lugar de `html`. | | `data` | object | No | Objeto con variables para sustitución Handlebars en la plantilla o HTML. | | `email_type` | string | No | Tipo de correo (ej. `transactional`, `marketing`, `automation`). | | `campaign_id` | string | No | UUID de la campaña asociada. | | `automation_run_id`| string | No | UUID de la ejecución de automatización asociada. | | `scheduled_at` | string | No | Fecha/hora de envío programado. Ver [Programacion de Envio](#programacion-de-envio). | | `timezone` | string | No | Zona horaria para interpretar `scheduled_at`. Ver [Programacion de Envio](#programacion-de-envio). | | `cc` | string \| string[] | No | Dirección(es) de correo en copia. | | `bcc` | string \| string[] | No | Dirección(es) de correo en copia oculta. | | `attachments` | array | No | Lista de objetos de adjuntos. Máximo 10 adjuntos. Ver [Adjuntos](#adjuntos). | | `text` | string | No | Versión texto plano del correo (alternativa MIME al HTML). | | `custom_headers` | object | No | Headers adicionales para el correo. | | `in_reply_to` | string | No | Message-ID al que responde este correo (threading). | | `references` | string \| string[] | No | Message-IDs de la cadena de threading (header `References`). | | `thread_id` | string | No | ID de hilo para agrupar correos relacionados. | | `dry_run` | boolean | No | Si es `true`, renderiza el correo (template + variables) sin enviarlo ni registrar actividad. Ver [Dry Run](#dry-run). | | `environment` | string | No | Nombre de un webhook environment configurado en el proyecto. Rutea los webhooks de este envío a esa URL. Ver [Webhook Environments](/concepts/webhook-environments). | ## Adjuntos Cada objeto en el array `attachments` tiene la siguiente estructura: | Campo | Tipo | Requerido | Descripción | | ------------- | ------ | --------- | ------------------------------------------------------------------ | | `filename` | string | Sí | Nombre del archivo (ej. `factura.pdf`). | | `url` | string | No* | URL pública del archivo. Requerido si no se proporciona `content`. | | `content` | string | No* | Contenido del archivo codificado en Base64. Requerido si no se proporciona `url`. | | `contentType` | string | No | Tipo MIME del archivo (ej. `application/pdf`). Se infiere del nombre si no se proporciona. | **Límites:** - Máximo **10 adjuntos** por correo. - Máximo **10 MB** por adjunto individual. - Máximo **10 MB** en total por correo (límite de la infraestructura de envío). Los adjuntos se procesan en segundo plano (no en la respuesta HTTP): si un adjunto excede los límites o su URL no es accesible, el envío falla de forma asíncrona y queda registrado en el activity record. Ver más en [Adjuntos](/guides/attachments). ## Programacion de Envio El campo `scheduled_at` soporta múltiples formatos: ### Timestamp ISO 8601 ``` "scheduled_at": "2025-03-15T14:30:00Z" ``` ### Lenguaje natural (ingles) ``` "scheduled_at": "tomorrow at 3pm" "scheduled_at": "in 2 hours" "scheduled_at": "next monday at 9am" ``` ### Zona horaria (`timezone`) El campo `timezone` se usa para interpretar correctamente `scheduled_at`. Soporta: - **Nombre IANA:** `"America/Santiago"`, `"US/Eastern"`, `"Europe/Madrid"` - **Offset:** `"+09:00"`, `"-05:00"`, `"UTC-3"`, `"GMT+9"` - **Numérico entero:** `-3`, `9` (sin fracciones — para zonas de media hora como India usa el formato `"+05:30"`) Si no se proporciona `timezone`, se usa la zona horaria por defecto del proyecto (`default_timezone`). Si el proyecto no tiene zona horaria configurada, se asume UTC. Ver más en [Programar Envíos](/guides/scheduling). ## Dry Run Con `"dry_run": true` el endpoint renderiza el correo (resuelve la plantilla y sustituye variables `{{var}}` y bloques `{{#each arr}}`) **sin enviarlo**: no envía, no encola, no crea registro de actividad ni consume cuota. Útil para validar el payload y las variables antes de un envío real. ```json { "dry_run": true, "test_mode": false, "sample_cart_items_injected": false, "would_send": { "to": ["cliente@ejemplo.com"], "cc": [], "bcc": [], "from": "Mi Tienda ", "subject": "Tu pedido está en camino", "html_preview": "

Hola Juan

Tu pedido #12345 está en camino.

", "template_id": null, "variables_used": ["nombre", "pedido"] } } ``` `html_preview` se trunca a 8000 caracteres. Las variables sin valor en `data` se dejan como `{{variable}}` en el preview. ## Headers | Header | Tipo | Requerido | Descripción | | --------------- | ------ | --------- | ------------------------------------------------ | | `Authorization` | string | Sí | `Bearer sk_proj_...` / `sk_live_...` / `sk_test_...`. | | `Content-Type` | string | Sí | Debe ser `application/json`. | | `Idempotency-Key` | string | No | Key arbitraria 1-256 chars. Mismo `(project_id, key)` dentro de 24h retorna respuesta cacheada. | | `x-source` | string | No | Identificador del origen de la solicitud (`api`, `platform`, `test`). | Ver más sobre `Idempotency-Key` en [API Keys](/guides/api-keys). ## Ejemplo de Solicitud ### Envio inmediato ```bash curl -X POST https://api.reallyquickemails.com/send-email \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "html": "

Hola {{nombre}}

Tu pedido #{{pedido}} está en camino.

", "subject": "Tu pedido está en camino", "recipient": "cliente@ejemplo.com", "sender": "ventas@mitienda.com", "senderName": "Mi Tienda", "data": { "nombre": "Juan", "pedido": "12345" }, "email_type": "transactional" }' ```
```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); const { data, error } = await rqe.emails.send({ html: '

Hola {{nombre}}

Tu pedido #{{pedido}} está en camino.

', subject: 'Tu pedido está en camino', recipient: 'cliente@ejemplo.com', sender: 'ventas@mitienda.com', senderName: 'Mi Tienda', data: { nombre: 'Juan', pedido: '12345', }, email_type: 'transactional', }); if (error) console.error(error); else console.log('Encolado:', data.email_id); ```
### Envio programado con adjuntos ```bash curl -X POST https://api.reallyquickemails.com/send-email \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "subject": "Reporte mensual - Marzo 2025", "recipient": ["gerente@empresa.com", "director@empresa.com"], "sender": "reportes@empresa.com", "senderName": "Sistema de Reportes", "templateId": "reporte-mensual", "data": { "mes": "Marzo", "anio": "2025" }, "scheduled_at": "next monday at 9am", "timezone": "America/Santiago", "attachments": [ { "filename": "reporte-marzo-2025.pdf", "url": "https://storage.ejemplo.com/reportes/marzo-2025.pdf", "contentType": "application/pdf" } ] }' ``` ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); const { data, error } = await rqe.emails.send({ subject: 'Reporte mensual - Marzo 2025', recipient: ['gerente@empresa.com', 'director@empresa.com'], sender: 'reportes@empresa.com', senderName: 'Sistema de Reportes', templateId: 'reporte-mensual', data: { mes: 'Marzo', anio: '2025', }, scheduled_at: 'next monday at 9am', timezone: 'America/Santiago', attachments: [ { filename: 'reporte-marzo-2025.pdf', content: base64Pdf, // contenido del archivo en Base64 content_type: 'application/pdf', }, ], }); ``` > En el SDK los adjuntos se envían como `content` en Base64 con `content_type` (snake_case); el campo `url` solo está disponible vía API directa. Ver más en [SDK de Node.js](/guides/sdk-nodejs). ## Respuestas ### Envio inmediato exitoso El envío es **asíncrono**: el endpoint encola el correo y responde de inmediato con `200`. El envío real ocurre en segundo plano. ```json { "success": true, "queued": true, "jobId": "email-1780676307345-6da66195", "email_id": "d116a543-9b13-41b4-93cf-647539018275", "activityId": "d116a543-9b13-41b4-93cf-647539018275", "message": "Email queued for sending" } ``` | Campo | Tipo | Descripción | |---|---|---| | `queued` | boolean | Siempre `true` en envío inmediato. El correo quedó encolado. | | `jobId` | string | ID del job en la cola de envío. | | `email_id` | string \| null | UUID del registro de actividad. Mismo valor que `activityId`. | | `activityId` | string \| null | UUID del registro de actividad, pre-creado con estado `queued`. | El resultado final del envío (delivered, bounced, failed) **no viene en esta respuesta**: se consulta vía la [Activity API](./activity) usando `email_id`, o se recibe vía [webhooks](./webhooks) (`email.send`, `email.delivery`, `email.bounce`, etc.). ### Envio programado exitoso ```json { "success": true, "scheduled": true, "scheduled_send_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "scheduled_for": "2025-03-17T12:00:00.000Z", "scheduled_for_local": "2025-03-17 09:00 (America/Santiago)", "timezone": "America/Santiago", "message": "Correo programado para 2025-03-17 09:00 (America/Santiago)" } ``` | Campo | Tipo | Descripción | |---|---|---| | `scheduled_send_id` | string | UUID del correo programado. | | `scheduled_for` | string | Fecha y hora de envío en UTC (ISO 8601). | | `scheduled_for_local` | string | Fecha y hora de envío en la zona horaria especificada (formato legible). | | `timezone` | string | Zona horaria utilizada para la programación. | ## Reply-To Automatico (Inbound Email) Todos los envíos vía API incluyen automáticamente un header `Reply-To` con el nombre del remitente: ``` Reply-To: "Mi Tienda" ``` Los clientes de correo (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 un webhook `email.inbound` con el reply + adjuntos. Ver más en [Webhooks → Inbound](./webhooks#inbound-replies-con-adjuntos). ## Verificacion de Remitente El `sender` debe estar verificado antes de poder enviar. Hay dos formas: 1. **Verificar el email individual** vía magic link (`POST /domains/verify-email`). 2. **Verificar el dominio completo** vía DNS (`POST /domains/register`) — recomendado, mejora deliverability y permite enviar desde cualquier dirección del dominio. La verificación **no se valida sincrónicamente** en este endpoint: como el envío es encolado, un sender no verificado igual recibe `200` con `queued: true`, y el rechazo ocurre después, durante el procesamiento en segundo plano, cuando la infraestructura de envío rechaza la identidad. El fallo queda registrado en el activity record (`current_status: "failed"`, con el detalle en `error_message`), consultable vía la [Activity API](./activity) con el `email_id` de la respuesta. Ver más en [Domains API](./domains). ## Codigos de Error Como el envío es asíncrono, los únicos errores **síncronos** (en la respuesta HTTP) son de validación: | Código | Descripción | | ------ | --------------------------------------------------------------------------- | | `400` | Solicitud inválida. Faltan campos requeridos (`recipient`/`sender`, o `html`+`subject` sin `templateId`), `scheduled_at` no parseable, o `environment` no configurado. | | `401` | API key inválida o ausente. | | `404` | Plantilla no encontrada (solo en modo `dry_run`; con envío real, una plantilla inexistente falla de forma asíncrona en segundo plano). | | `500` | Error interno del servidor (incluye fallo al insertar el envío programado). | Los errores de **envío** (sender no verificado, adjunto inválido, rechazo de la infraestructura de envío, rate limit) ocurren después, durante el procesamiento en segundo plano: el activity record pasa a `current_status: "failed"` con el detalle en `error_message`. ### Ejemplo de error ```json { "error": "Either templateId (with projectId) or both html and subject are required" } ``` # Webhooks (/api-reference/webhooks) 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](#routing-live--test--environment). ### Expón tu endpoint local Para desarrollo, crea un túnel hacia tu servidor local con una herramienta como [ngrok](https://ngrok.com) (`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](#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](#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`. ```json { "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](/concepts/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.send` y `email.delivery` no incluyen un campo `delivered_at` separado — usa `event_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-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](/concepts/webhook-environments) si aplica. Solo en eventos outbound — el POST de `email.inbound` no incluye este header | ### Verificación HMAC ```javascript 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[]` | Solo eventos de envíos que incluyeron `environment: ""` 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[]`. ### 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](/concepts/webhook-environments). ### `is_test` en el payload ```json { "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" ``` 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 ```json { "event": "email.inbound", "timestamp": "2026-04-28T22:45:32.299Z", "project_id": "uuid", "is_test": false, "data": { "message_id": "", "thread_id": "uuid", "in_reply_to": "", "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": "
respuesta html
", "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": "", "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](/concepts/tracking) — qué eventos disparan webhook y cómo se generan. - [Test mode](/concepts/test-mode) — separar tráfico dev/prod vía `sk_test_*`. - [Webhook environments](/concepts/webhook-environments) — N URLs por proyecto vía campo `environment`. - [Send email](/api-reference/send-email) — cómo disparar emails que generen estos eventos. # Deliverability y autenticación (/concepts/deliverability) Esta página explica cómo ReallyQuickEmails (RQE) autentica tus envíos y los mecanismos detrás de "¿llega al inbox?". No es un tutorial paso a paso (para eso ver [Dominios](/api-reference/domains)) — es la teoría que necesitas para diagnosticar problemas y decidir qué configurar. ## El stack de autenticación Cuando un servidor de correo (Gmail, Outlook) recibe un email, hace 4 chequeos para decidir si va al inbox o a spam: | Chequeo | Qué verifica | Quién lo configura | |---|---|---| | **SPF** | El servidor que envió el email tiene permiso para enviar desde tu dominio | DNS de tu dominio | | **DKIM** | El email fue firmado criptográficamente con una clave que publicaste en DNS | DNS + RQE genera y firma | | **DMARC** | Política sobre qué hacer si SPF/DKIM fallan (none/quarantine/reject) | DNS de tu dominio | | **MAIL FROM domain** | El "envelope sender" (Return-Path) coincide con tu dominio, no con `amazonses.com` | DNS + RQE | Cuando los 4 pasan, tu email tiene **alignment estricto** y la deliverability sube significativamente. ## SPF (Sender Policy Framework) Un record TXT en tu DNS que lista qué servidores pueden enviar emails desde tu dominio. Cuando RQE envía un email desde `noreply@tudominio.com`, Gmail consulta el SPF de `tudominio.com` y verifica que el servidor de envío esté en la lista. Ejemplo de record (el que genera RQE al registrar tu dominio): ``` tudominio.com. TXT "v=spf1 include:amazonses.com ~all" ``` `~all` (soft fail): los servidores no listados se marcan como sospechosos sin rechazarse de plano. Puedes endurecerlo a `-all` (hard fail) una vez que confirmes que todo tu tráfico legítimo sale por servidores listados. Si ya tienes SPF configurado para otro proveedor (Mailgun, Sendgrid, Google Workspace), tienes que **mergear** los `include:` en un solo record, no agregar otro TXT. Múltiples records SPF rompen la verificación. ## DKIM (DomainKeys Identified Mail) Una firma criptográfica del contenido del email, hecha con una clave privada de RQE. Tu dominio publica la clave pública en DNS vía 3 records CNAME. Cuando el email llega, Gmail toma la firma del header, descarga la clave pública del CNAME, y valida. ``` abc123._domainkey.tudominio.com. CNAME abc123.dkim.amazonses.com. def456._domainkey.tudominio.com. CNAME def456.dkim.amazonses.com. ghi789._domainkey.tudominio.com. CNAME ghi789.dkim.amazonses.com. ``` Los tokens (`abc123`, ...) se generan al registrar tu dominio y vienen en la respuesta de [`POST /domains/register`](/api-reference/domains#post-domainsregister). DKIM también permite que tu dominio aparezca en el `signed-by:` del header en Gmail web, lo cual es señal de legitimidad para el usuario final. ## DMARC La política que indica qué debería pasar si SPF o DKIM fallan. Es el más fuerte de los 3 en términos de protección anti-spoofing. | Política | Qué hace si falla SPF/DKIM | Cuándo usar | |---|---|---| | `p=none` | Solo reporta, no rechaza | Primera implementación, mientras analizas reportes | | `p=quarantine` | Manda a spam | Después de 1–2 semanas de `none` sin issues | | `p=reject` | Rechaza el email completamente | Producción estable, máxima protección | Ejemplo: ``` _dmarc.tudominio.com. TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@tudominio.com; pct=100" ``` `pct=100` aplica la política al 100% del tráfico. Empezar con `pct=10` y subir gradualmente es una práctica común. Si pasas `rua=`, los servidores receptores te mandan reportes diarios con qué % de tu tráfico pasó SPF/DKIM. Útil para detectar suplantación o configuraciones rotas. Hay servicios gratuitos para parsear los reportes (Postmark, dmarcian). ## MAIL FROM domain (custom) Cuando envías un email sin configuración extra, el `mailed-by:` que ve Gmail es `amazonses.com` — el dominio del proveedor, no el tuyo. Eso baja el SPF alignment de strict a relaxed. Configurando un **Custom MAIL FROM domain** (en RQE, el subdominio `bounce.tudominio.com`), el `mailed-by:` pasa a ser tu dominio y SPF queda alineado strict. RQE lo configura con un único record CNAME, incluido en los 7 que se generan al registrar el dominio: ``` bounce.tudominio.com. CNAME feedback-smtp.us-east-1.amazonses.com. ``` Custom MAIL FROM no es estrictamente necesario — `can_send` no lo requiere y los emails llegan igual sin él. Pero mejora el alignment SPF, sobre todo en clientes estrictos como ProtonMail u Outlook empresarial. Recomendado para volúmenes grandes. ## El proceso de verificación en RQE ### Registrar el dominio `POST /domains/register` con `{ "domain": "tudominio.com", "sender_name": "Mi Empresa", "sender_email": "noreply@tudominio.com" }` (los tres campos son requeridos). RQE genera los 7 records DNS y los devuelve en la respuesta. Ver [Dominios](/api-reference/domains). ### Configurar los 7 records En tu proveedor DNS (Cloudflare, GoDaddy, Route 53, etc): - 1 TXT verificación de dominio - 3 CNAME DKIM - 1 TXT SPF (mergear con existente si aplica) - 1 TXT DMARC - 1 CNAME Return-Path (Custom MAIL FROM) ### Esperar propagación 5–10 minutos en Cloudflare/Route 53. Hasta 48h en proveedores lentos. ### Verificar `POST /domains/:domain/verify` comprueba la verificación del dominio, los 3 CNAME de DKIM y el CNAME de MAIL FROM (SPF y DMARC no se validan automáticamente). Cuando dominio y DKIM están verificados, la respuesta incluye `can_send: true` y puedes enviar emails desde `*@tudominio.com`. ## Por qué los emails llegan a spam (más allá de la auth) Aunque la auth técnica esté OK, hay factores reputacionales: | Factor | Impacto | |---|---| | Dominio nuevo (< 30 días) | Alto — hay que hacer warming | | Tasa de bounces > 2% | Alto — limpia tu lista regularmente | | Tasa de complaints > 0.1% | Crítico — Gmail/Outlook degradan reputación rápido | | Sin `List-Unsubscribe` header | Alto — RQE lo agrega automático en campañas y en `/v1/send-template-email`; en envíos directos pásalo con `custom_headers` (`POST /send-email` y `/v1/send-batch`) | | Contenido sospechoso | Medio — palabras como "GRATIS", "URGENTE" en mayúsculas, ratio HTML/texto malo | | Listas compradas | Crítico — la mayoría son spam traps | Si tu dominio es nuevo o no envió en > 60 días, hay que hacer **warming gradual**: día 1 ~50k emails, día 2 ~75k, día 3+ resto. El [modo por lotes](/api-reference/campaigns) automatiza esto. ## Próximos pasos - [Dominios](/api-reference/domains) — endpoints para registrar y verificar. - [Tracking](/concepts/tracking) — cómo funciona el open/click tracking. - [FAQ — ¿por qué van a spam?](/guides/faq) — checklist práctico. # Modos Live y Test (/concepts/test-mode) ReallyQuickEmails separa tráfico productivo y de desarrollo a través del **prefijo de la API key**, no de un parámetro del request. Esto evita errores típicos donde un dev olvida un flag y manda tráfico de pruebas como si fuera producción. ## El modelo | | Live | Test | |---|---|---| | **Prefijo de key** | `sk_proj_*` o `sk_live_*` | `sk_test_*` | | **Cuota mensual** | cuenta | **también cuenta** — el envío es real | | **Payload `is_test`** | `false` | `true` | | **Métricas del dashboard** | producción | separadas (filtro Live/Test) | | **Webhook outbound** | `webhook_url` | `webhook_url_dev` | | **Webhook inbound (replies)** | `inbound_webhook_url` | `inbound_webhook_url_dev` | | **Envío real al inbox** | sí | **sí, también** | | **Rate limit y suppression list** | compartidos | compartidos | El email sale de verdad y llega al inbox del destinatario: puedes probar deliverability, render visual y comportamiento de los clientes de email igual que en producción. La diferencia está en *métricas y routing de webhooks*, no en el envío en sí. Por lo mismo, los envíos test también consumen cuota mensual. ## Cómo se decide el modo El modo viaja en la key. No hay parámetro `mode` en el body, ni headers especiales, ni flag por endpoint: ```bash curl -X POST https://api.reallyquickemails.com/v1/send-email \ -H "Authorization: Bearer sk_live_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient_email": "cliente@empresa.com", "sender_email": "noreply@tudominio.com", "subject": "Confirmación de pedido", "html_body": "

Tu pedido fue confirmado.

" }' ``` → webhook a `webhook_url` → payload con `is_test: false` → actividad visible en las métricas de producción
```bash curl -X POST https://api.reallyquickemails.com/v1/send-email \ -H "Authorization: Bearer sk_test_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient_email": "qa@empresa.com", "sender_email": "noreply@tudominio.com", "subject": "QA — confirmación", "html_body": "

Test desde staging.

" }' ``` → webhook a `webhook_url_dev` → payload con `is_test: true` → actividad marcada como test, separada de tus métricas live
## Webhook routing Cada proyecto tiene **cuatro URLs configurables** (en pares outbound/inbound, una para cada modo): ### Si la URL `_dev` está vacía Si tu proyecto **no tiene** `webhook_url_dev` configurada y envías con `sk_test_*`, los eventos caen a `webhook_url` — el payload llega con `is_test: true` para que puedas distinguirlos. Si ambas URLs están vacías, el evento no se entrega. El email sigue saliendo al inbox real. Ver el detalle del routing en [Webhooks](/api-reference/webhooks). ### `is_test` en el payload Todos los webhooks (outbound e inbound) incluyen `is_test: boolean` en el body. Eso te permite, si quieres, recibir todo en una sola URL (`webhook_url`) y filtrar lado-cliente, dejando `webhook_url_dev` vacío: ```json { "event": "email.delivery", "is_test": true, "data": { "...": "..." } } ``` Pero **mantenerlas separadas es lo recomendado** para evitar accidentes operativos. ## Caso de uso: setup multi-environment ```bash # .env.local RQE_API_KEY=sk_test_xxxxxxxxxxxx ``` Tu app local usa `sk_test_`. Los emails que mandes durante desarrollo: - Llegan al inbox real (pruebas render, deliverability) - Quedan marcados con `is_test: true` — no mezclan métricas con prod - Webhooks (si configuras `webhook_url_dev` apuntando a un ngrok o servicio de testing) funcionan idéntico ```bash # Variables del ambiente staging RQE_API_KEY=sk_test_xxxxxxxxxxxx ``` Mismo `sk_test_*` que local. Tu staging environment manda emails reales marcados como test. Si tienes webhook listener en tu staging, configura `webhook_url_dev` apuntando a esa URL. ```bash # Variables del ambiente prod RQE_API_KEY=sk_live_xxxxxxxxxxxx ``` Producción usa `sk_live_*` (o `sk_proj_*`). Webhooks a `webhook_url` (production listener). La elección de modo es solo cuestión de qué env var lees. Los call sites a la API son idénticos. Esto deja el switch en las variables de entorno de tu hosting y no en código, donde es más fácil olvidar un flag. ## Casos extremos ### Suppression list Es **compartida** entre live y test. Si un destinatario se dio de baja por un envío live, los envíos test al mismo recipient también se omiten: en `/v1/send-template-email` recibes `200` con `skipped: true`; en `/v1/send-email` la supresión se aplica en segundo plano y la actividad queda con estado `suppressed`. Esto previene que tests de QA reactiven la entrega a usuarios que ya no quieren tus emails. ### Rate limit Live y test comparten el rate limit y la cuota mensual de tu proyecto. Un spike de tests consume cuota real y afecta tu envío productivo. ### Regenerar keys Live y test se regeneran independientemente desde el dashboard. Regenerar la live no afecta la test, y viceversa. La key anterior queda invalidada al instante. ## Próximos pasos - [API Keys](/guides/api-keys) — referencia completa con ejemplos de idempotency y dry-run. - [Webhooks](/api-reference/webhooks) — formato de payload y verificación HMAC. - [Quickstart](/guides/quickstart) — primer envío end-to-end con `sk_test_*`. # Tracking de eventos (/concepts/tracking) Por cada email que envías, ReallyQuickEmails te dispara un webhook con eventos a medida que ocurren. Algunos vienen del proveedor de envío (entrega, rebote), otros los detecta RQE directamente (apertura, click). ## Eventos disponibles | Evento webhook | Cuándo dispara | Confiabilidad | |---|---|---| | `email.send` | RQE aceptó el envío y lo despachó | 100% | | `email.delivery` | El servidor del destinatario aceptó el email | ~99% | | `email.bounce` | Email rebotado (hard o soft) | 100% | | `email.complaint` | Destinatario marcó como spam | 100% | | `email.deliverydelay` | Entrega temporalmente demorada | 100% | | `email.open` | Destinatario abrió el email | ~70% | | `email.click` | Destinatario hizo click en un link | ~99% | | `email.inbound` | Destinatario respondió tu email (solo envíos vía API) | 100% | También existe `email.reject` (envío rechazado antes de salir). El detalle completo de cada evento y sus campos extra está en la [referencia de Webhooks](/api-reference/webhooks#eventos). ## Bounces — soft vs hard | Tipo | Descripción | Acción de RQE | |---|---|---| | **Hard bounce** | Dirección no existe (`550 5.1.1`) | Agrega el destinatario a la suppression list — los próximos envíos se omiten automáticamente | | **Soft bounce** | Mailbox lleno, server temporalmente caído | Marca el evento, no agrega a suppression. Puedes volver a enviar | Las quejas (`email.complaint`) también agregan al destinatario a la suppression list de forma automática. Si tu tasa de bounces supera **2%**, tu deliverability se degrada. Si supera **5%**, el proveedor puede suspender tu envío. Limpia tu lista regularmente — un email viejo es un bounce esperando suceder. ## Open tracking RQE inyecta un pixel 1x1 transparente en el HTML del email. Cuando el cliente del destinatario carga las imágenes, dispara el evento `email.open`. ### Por qué la confiabilidad es ~70% - **Outlook desktop** bloquea imágenes externas por default → no detecta open - **Apple Mail iOS 15+** con MPP (Mail Privacy Protection) carga el pixel automáticamente al recibir → reporta open antes de que el usuario abra realmente - **Clientes de texto plano** → no cargan imágenes Los opens son señal de tendencia (¿este send lo abre más gente que aquel?), no de comportamiento individual exacto. Para conversión real, mira los clicks. ## Click tracking Antes de enviar, RQE reescribe los `` del HTML para que pasen por un endpoint de tracking que hace 302 redirect al destino original. El usuario no nota nada — el redirect es instantáneo. Solo falla si copia/pega el href manual (extremadamente raro). ## Webhook payload Tu `webhook_url` recibe POST con HMAC-SHA256 signature en el header `X-RQE-Signature`, firmada con tu API key del proyecto. Body de ejemplo para un `email.delivery`: ```json { "event": "email.delivery", "is_test": false, "timestamp": "2026-04-29T15:30:42.123Z", "project_id": "...", "data": { "activity_id": "...", "message_id": "...", "recipient": "user@example.com", "event_type": "delivery", "event_timestamp": "2026-04-29T15:30:42.000Z" } } ``` Para el shape completo del payload por tipo de evento ver [Webhooks → Payload outbound](/api-reference/webhooks#payload-outbound). ### Verificación HMAC en Node ```javascript 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'); } ``` ## Próximos pasos - [Webhooks](/api-reference/webhooks) — referencia completa con todos los formatos. - [Test mode](/concepts/test-mode) — separar tracking dev vs prod. # Webhook environments (/concepts/webhook-environments) ReallyQuickEmails permite configurar **N URLs de webhook** por proyecto, una por cada ambiente custom (staging, qa, preview, etc.), y elegir a cuál disparar vía un campo `environment` opcional en el body del envío. Útil cuando una misma key Live se usa desde múltiples ambientes (mismo billing) y quieres que cada uno reciba sus propios webhooks. ## Diferencia con Live/Test | Mecanismo | Cómo se elige | Afecta `is_test` y métricas | Caso típico | |---|---|---|---| | **[Live vs Test](/concepts/test-mode)** | Prefijo de la key (`sk_live_*` vs `sk_test_*`) | Sí — los envíos quedan marcados `is_test: true`, separados de las métricas live | Devs probando emails reales sin contaminar métricas | | **Environments custom** (esta página) | Campo `environment` en body del envío | **No** — solo cambia la URL del webhook; la key sigue mandando | Misma key Live desde staging y producción del cliente | Los dos coexisten: puedes tener `sk_test_*` para QA local **y** environments custom para los ambientes intermedios del cliente. ## Configuración ### Definir los environments Dashboard → tu proyecto → **Configuración → Integraciones → Webhook Environments**. Agregas pares `nombre + URL`: Las claves (`staging`, `qa`, `preview-pr-42`) son arbitrarias — las eliges tú, no hay nombres reservados. Restricciones: `[a-zA-Z0-9_.-]` hasta 64 chars. ### Mandar el envío con `environment` ```bash curl -X POST https://api.reallyquickemails.com/v1/send-email \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "recipient_email": "user@example.com", "sender_email": "noreply@tudominio.com", "subject": "Confirmación", "html_body": "

Hola

", "environment": "staging" }' ```
```javascript await fetch('https://api.reallyquickemails.com/v1/send-email', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.RQE_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ recipient_email: 'user@example.com', sender_email: 'noreply@tudominio.com', subject: 'Confirmación', html_body: '

Hola

', environment: 'staging', }), }); ```
```python import os, requests requests.post( 'https://api.reallyquickemails.com/v1/send-email', headers={'Authorization': f"Bearer {os.environ['RQE_API_KEY']}"}, json={ 'recipient_email': 'user@example.com', 'sender_email': 'noreply@tudominio.com', 'subject': 'Confirmación', 'html_body': '

Hola

', 'environment': 'staging', }, ) ```
### Recibir el webhook con echo El payload que llega al endpoint configurado para `staging` incluye `environment` echo, así no necesitas parsear URLs para diferenciar: ```json { "event": "email.delivery", "environment": "staging", "is_test": false, "timestamp": "2026-04-29T15:30:42.123Z", "project_id": "...", "data": { "activity_id": "...", "message_id": "...", "recipient": "user@example.com", "event_type": "delivery", "event_timestamp": "2026-04-29T15:30:42.000Z" } } ``` Mismo modelo para inbound replies — el payload del `email.inbound` también incluye `environment`.
## Comportamiento Cuando se envía con `environment` configurado, el webhook va **solo** a esa URL. Si el `environment` no existe en `webhook_environments`, RQE responde **400 `ENVIRONMENT_NOT_CONFIGURED`** — no hace fallback silencioso al `webhook_url` default. Así los errores de configuración son visibles desde el primer envío. | Caso | Routing | |---|---| | Body con `environment: "staging"` y `webhook_environments.staging` existe | → solo a `webhook_environments.staging` | | Body con `environment: "staging"` pero no está en `webhook_environments` | → **400 error**, no se encola el envío | | Body sin `environment` y key es `sk_live_*` | → `webhook_url` (live) | | Body sin `environment` y key es `sk_test_*` | → `webhook_url_dev` (test) | | Body con `environment` y key es `sk_test_*` | → solo a `webhook_environments[env]` (override gana) | ## Idempotencia con `environment` `Idempotency-Key` funciona por `(projectId, key)`, **no incluye `environment`** en el scope. Si reintentas el mismo envío con distinto `environment` y misma `Idempotency-Key` dentro de 24h, vas a recibir el resultado cacheado del primer request — incluyendo el `environment` original. Para distinguir envíos por ambiente, usa `Idempotency-Key` distinta. ## Cuándo usarlo **Buen fit:** - Misma app cliente con varios ambientes (prod, staging, qa) que comparten billing y métricas pero quieren callbacks separados. - Preview deployments con URLs efímeras (`preview-pr-N`). - Multi-tenant SaaS que quiere identificar tráfico por sub-cliente sin separar proyectos RQE. **No es la herramienta para:** - Separar métricas y actividad dev/prod — usa [Test mode](/concepts/test-mode) (`sk_test_*`). - Aislar suppression list — la suppression es por proyecto, compartida entre todos los environments. - Replicar 100% un setup multi-cliente — para eso crea proyectos RQE separados. ## Próximos pasos - [Webhooks (referencia)](/api-reference/webhooks) — formato completo del payload, verificación HMAC, retry logic. - [Test mode](/concepts/test-mode) — diferencias con `sk_test_*`. - [Send email](/api-reference/send-email) — request body completo. # API Keys (/guides/api-keys) ReallyQuickEmails utiliza Secret Keys con prefijo para autenticar todas las solicitudes a la API. Cada proyecto tiene **dos keys distintas**: una para tráfico productivo (Live) y otra para desarrollo (Test). --- ## Live vs Test | Modo | Prefijo | Cuándo usarla | Comportamiento | |---|---|---|---| | **Live** | `sk_proj_*` o `sk_live_*` | Tráfico productivo: clientes reales, emails reales, métricas reales | Cuenta contra tu cuota mensual. Webhook outbound dispara a `webhook_url`. Inbound replies a `inbound_webhook_url`. | | **Test** | `sk_test_*` | Desarrollo, staging, sandbox, suites E2E | Envía emails reales y **también** consume cuota mensual. Activity queda marcada con `is_test=true` y se filtra del dashboard. Webhooks van a `webhook_url_dev` / `inbound_webhook_url_dev`. | > `sk_proj_*` y `sk_live_*` funcionan idéntico — ambos son **modo live** y son válidos indefinidamente. ### Comportamiento detallado de Test Mode Las keys `sk_test_*` te permiten desarrollar e iterar sin contaminar tus métricas productivas: - **Envíos reales**: el email llega al inbox del destinatario igual que en live. Esto es deliberado para que pruebes deliverability, render visual y comportamiento de los clientes de email. - **Consume cuota mensual**: como el envío es real, cuenta contra el límite mensual del plan igual que un envío live. - **Activity con `is_test=true`**: cada envío queda registrado en Activity con la flag `is_test: true`. El dashboard filtra estos registros automáticamente cuando el toggle Live/Test está en modo Live. - **Webhooks separados**: tanto los eventos outbound (`email.delivery`, `email.bounce`, `email.open`, `email.click`) como los inbound replies se enrutan a las URLs `*_dev`. Si la URL `_dev` no está configurada, el evento simplemente no se entrega — **no** se hace fallback al `webhook_url` de live. - **Toggle en dashboard**: el sidebar tiene un switch global Live/Test que filtra Activity, Campaigns y métricas según el modo seleccionado. Ver [Modos Live y Test](/concepts/test-mode) para el detalle completo del routing de webhooks y casos extremos. --- ## Donde encontrar tus Secret Keys 1. Ingresa al [dashboard de RQE](https://app.reallyquickemails.com). 2. Navega al proyecto donde deseas obtener las credenciales. 3. En el menú lateral, abre **Integraciones → API Keys**. 4. Ahí encontrarás: - **Producción** (Live) — `sk_live_*` o `sk_proj_*`. - **Test** — `sk_test_*`. Generable/regenerable independientemente de la live. Cada key tiene controles separados de mostrar/copiar/regenerar. Regenerar la live **no** afecta la test, y viceversa. --- ## Uso Pasa la key como Bearer token en el header `Authorization`: ``` Authorization: Bearer sk_live_tu_secret_key ``` ```bash # Live curl -X POST https://api.reallyquickemails.com/v1/send-email \ -H "Authorization: Bearer sk_live_tu_secret_key" \ -H "Content-Type: application/json" \ -d '{ "recipient_email": "destinatario@ejemplo.com", "sender_email": "noreply@tudominio.com", "subject": "Hola", "html_body": "

Hola!

" }' # Test (mismo endpoint, solo cambia el prefijo de la key) curl -X POST https://api.reallyquickemails.com/v1/send-email \ -H "Authorization: Bearer sk_test_tu_test_key" \ -H "Content-Type: application/json" \ -d '{ "recipient_email": "qa@tudominio.com", "sender_email": "noreply@tudominio.com", "subject": "Smoke test", "html_body": "

Test desde staging

" }' ``` > **Importante:** todos los endpoints de la API aceptan keys live y test. RQE determina el modo del envío únicamente por el prefijo de la key — **no** existe un parámetro `mode` ni headers especiales para forzar un modo distinto. ### Errores de prefijo inválido Si pasas una key con un prefijo desconocido (ej. `sk_dev_`), recibes `401 Unauthorized`: ```json { "error": "Invalid API key prefix. Expected sk_proj_*, sk_live_* or sk_test_*." } ``` --- ## Idempotency Los endpoints `POST /send-email` ([API avanzada](/api-reference/send-email)) y `POST /v1/send-batch` soportan idempotencia opcional vía header `Idempotency-Key`: ``` Idempotency-Key: pedido-12345-confirmacion ``` - La key es un string arbitrario de 1 a 256 caracteres. - La respuesta del primer request se cachea por **24 horas**, scoped a `(project_id, idempotency_key)`. - Si llega otro request con la misma key dentro de la ventana, RQE devuelve la **respuesta cacheada** del primer request, con header `Idempotency-Replayed: true`. - Las respuestas `5xx` no se cachean — puedes reintentar con la misma key. - Útil para retries de red sin riesgo de doble envío. Funciona idéntico en live y test. > `POST /v1/send-email` y `POST /v1/send-template-email` **no** soportan `Idempotency-Key`. Si necesitas idempotencia en envíos individuales, usa la [API avanzada](/api-reference/send-email). --- ## Dry Run Para validar payload y variables sin enviar el email, usa `POST /send-email` ([API avanzada](/api-reference/send-email), campos `recipient`/`sender`/`html`) con `dry_run: true` en el body: ```bash curl -X POST https://api.reallyquickemails.com/send-email \ -H "Authorization: Bearer sk_test_tu_test_key" \ -H "Content-Type: application/json" \ -d '{ "recipient": "destinatario@ejemplo.com", "sender": "noreply@tudominio.com", "subject": "Hola {{nombre}}", "html": "

Hola {{nombre}}

", "data": { "nombre": "María" }, "dry_run": true }' ``` Respuesta: ```json { "dry_run": true, "test_mode": true, "sample_cart_items_injected": false, "would_send": { "to": ["destinatario@ejemplo.com"], "cc": [], "bcc": [], "from": "noreply@tudominio.com", "subject": "Hola María", "html_preview": "

Hola María

", "template_id": null, "variables_used": ["nombre"] } } ``` No se envía nada, no se encola, no se registra actividad ni consume cuota. Útil para CI o para validar templates antes de un envío masivo. `dry_run` **no** está disponible en `/v1/send-email`. Ver [detalles del dry run](/api-reference/send-email#dry-run). --- ## Seguridad - **Nunca expongas tus Secret Keys en código del lado del cliente** (frontend, apps móviles, repos públicos). Las keys deben usarse exclusivamente desde backend. - **Variables de entorno** — almacena en `.env`, nunca hardcodeadas: ```bash # .env RQE_LIVE_KEY=sk_live_tu_secret_key RQE_TEST_KEY=sk_test_tu_test_key ``` ```javascript // Ejemplo Node.js — usar la key correcta según el ambiente const apiKey = process.env.NODE_ENV === 'production' ? process.env.RQE_LIVE_KEY : process.env.RQE_TEST_KEY; const response = await fetch("https://api.reallyquickemails.com/v1/send-email", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ recipient_email: "cliente@ejemplo.com", sender_email: "noreply@tudominio.com", subject: "Hola", html_body: "

Hola!

", }), }); ``` - **Rota tus keys periódicamente.** Si sospechas que una fue comprometida, regenera desde el dashboard inmediatamente. La key anterior queda invalidada en el momento. - **Agrega `.env` a tu `.gitignore`** para evitar que las credenciales se suban al repositorio. - **Mantén live y test aisladas** entre ambientes. Pasar accidentalmente la `sk_live_*` en staging puede contaminar métricas y consumir cuota. # Adjuntos (/guides/attachments) La API de ReallyQuickEmails permite enviar correos con archivos adjuntos usando el endpoint [`POST /send-email`](/api-reference/send-email). Los adjuntos se especifican en el campo `attachments` del cuerpo de la solicitud. ## Estructura del adjunto Cada elemento del arreglo `attachments` acepta los siguientes campos: | Campo | Tipo | Requerido | Descripción | |---|---|---|---| | `filename` | `string` | Sí | Nombre del archivo con extensión (ej. `"factura.pdf"`). | | `url` | `string` | Condicional | URL pública del archivo (HTTP o HTTPS). Requerido si no se usa `content`. | | `content` | `string` | Condicional | Contenido del archivo codificado en base64. Requerido si no se usa `url`. | | `contentType` | `string` | No | Tipo MIME del archivo. Se auto-detecta a partir de la extensión del `filename` si no se especifica. | Cada adjunto debe incluir `filename` y exactamente uno de `url` o `content`. ## Tipos permitidos Solo se permiten los siguientes tipos MIME: - `application/pdf` - `text/calendar` Si el tipo MIME del archivo no coincide con uno de estos, el envío falla durante el procesamiento en segundo plano (ver la sección de validación más abajo). ## Limites | Límite | Valor | |---|---| | Cantidad máxima de adjuntos | 10 por correo | | Tamaño máximo por archivo | 10 MB | | Tamaño máximo total | 10 MB | ## Adjuntos por URL Cuando se usa el campo `url`, el servidor descarga el archivo antes de enviarlo. Se aplican las siguientes medidas de seguridad: - **Solo HTTP/HTTPS:** Otros esquemas de URL son rechazados. - **Protección SSRF:** Las URLs que apuntan a direcciones IP privadas o reservadas son bloqueadas. - **Timeout:** La descarga tiene un tiempo límite de 30 segundos. Si el servidor remoto no responde a tiempo, el envío falla. ## Adjuntos por base64 Cuando se usa el campo `content`, el valor debe ser el contenido del archivo codificado en base64 sin prefijos (sin `data:...;base64,`). Solo el string base64 crudo. ## Validación asíncrona El endpoint `/send-email` es asíncrono: la respuesta HTTP confirma que el correo quedó encolado, pero los adjuntos se descargan y validan **en segundo plano**. Si un adjunto excede los límites, tiene un tipo MIME no permitido o su URL no es accesible, el envío falla de forma asíncrona y queda registrado en el historial de actividad (`current_status: "failed"`, con el detalle en `error_message`). Para confirmar el resultado, consulta la [Activity API](/api-reference/activity) con el `email_id` de la respuesta, o suscríbete a [webhooks](/api-reference/webhooks). ## Ejemplos ### Adjunto por URL ```bash curl -X POST https://api.reallyquickemails.com/send-email \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -d '{ "recipient": "cliente@ejemplo.com", "sender": "ventas@mitienda.com", "senderName": "Mi Tienda", "subject": "Tu factura de octubre", "html": "

Adjuntamos tu factura de octubre.

", "attachments": [ { "filename": "factura-octubre-2026.pdf", "url": "https://storage.ejemplo.com/facturas/factura-octubre-2026.pdf" } ] }' ``` Respuesta (el correo queda encolado; los adjuntos se procesan en segundo plano): ```json { "success": true, "queued": true, "jobId": "email-1780676307345-6da66195", "email_id": "d116a543-9b13-41b4-93cf-647539018275", "activityId": "d116a543-9b13-41b4-93cf-647539018275", "message": "Email queued for sending" } ``` ### Adjunto por base64 ```bash curl -X POST https://api.reallyquickemails.com/send-email \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -d '{ "recipient": "cliente@ejemplo.com", "sender": "ventas@mitienda.com", "senderName": "Mi Tienda", "subject": "Invitación a reunión", "html": "

Te invitamos a una reunion de equipo.

", "attachments": [ { "filename": "reunion.ics", "content": "QkVHSU46VkNBTEVOREFSClZFUlNJT046Mi4wCkJFR0lOOlZFVkVOVApEVFNUQVJUOjIwMjUxMDE2VDE1MDAwMFoKRFRFTkQ6MjAyNTEwMTZUMTYwMDAwWgpTVU1NQVJZOlJldW5pb24gZGUgZXF1aXBvCkVORDpWRVZFTlQKRU5EOlZDQUxFTkRBUg==", "contentType": "text/calendar" } ] }' ``` ### Multiples adjuntos ```bash curl -X POST https://api.reallyquickemails.com/send-email \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -d '{ "recipient": "cliente@ejemplo.com", "sender": "ventas@mitienda.com", "senderName": "Mi Tienda", "subject": "Documentos del proyecto", "html": "

Adjuntamos el contrato y la agenda de la reunion.

", "attachments": [ { "filename": "contrato.pdf", "url": "https://storage.ejemplo.com/docs/contrato.pdf" }, { "filename": "agenda.ics", "content": "QkVHSU46VkNBTEVOREFSClZFUlNJT046Mi4wCkVORDpWQ0FMRU5EQVI=", "contentType": "text/calendar" } ] }' ``` La respuesta tiene la misma estructura `queued` del primer ejemplo. El campo `attachments` no se valida en la respuesta HTTP — revisa el estado final con la [Activity API](/api-reference/activity). # Autenticación (/guides/authentication) Todos los endpoints de la API de ReallyQuickEmails usan el mismo método de autenticación: **Bearer Token** con tu Secret Key. --- ## Bearer Token Envía tu **Secret Key** (`sk_proj_...`) en el header `Authorization`: ``` Authorization: Bearer sk_proj_tu_secret_key ``` Este patrón funciona para **todos** los endpoints: - **Envío:** `POST /v1/send-email`, `POST /v1/send-template-email`, `POST /v1/send-batch`, `POST /send-email` - **Dominios:** `POST /domains/register`, `GET /domains/:domain/dns-records`, `POST /domains/:domain/verify`, `GET /domains/:domain/status`, `GET /domains?domain=`, `POST /domains/:domain/recreate`, `DELETE /domains/:domain`, `PUT /domains/:domain/sender`, `POST /domains/verify-email`, `GET /domains/verify-email/status` - **Activity:** `GET /v1/activity`, `GET /v1/activity/:id` - **Datos:** `POST /v1/leads`, `POST /v1/events` (y el resto de la [API de leads y eventos](/api-reference/leads)) ### Ejemplo ```bash curl -X POST https://api.reallyquickemails.com/v1/send-email \ -H "Authorization: Bearer sk_proj_tu_secret_key" \ -H "Content-Type: application/json" \ -d '{ "recipient_email": "cliente@ejemplo.com", "sender_email": "ventas@tudominio.com", "subject": "Confirmación de pedido", "html_body": "

Pedido confirmado

" }' ``` ### Como funciona 1. El servidor extrae la Secret Key del header `Authorization` 2. Valida la key y resuelve el proyecto asociado 3. Si la key es válida, el `projectId` se deriva automáticamente — no necesitas enviarlo --- ## Donde encontrar tu Secret Key 1. Ingresa al [dashboard de RQE](https://app.reallyquickemails.com). 2. Selecciona tu proyecto. 3. En el menú lateral, abre **Integraciones → API Keys**. 4. Copia tu **Secret Key** (comienza con `sk_proj_`). Ahí también encuentras la **Test key** (`sk_test_*`). Ver [API Keys](/guides/api-keys) para las diferencias entre modo Live y Test. --- ## Respuestas de error **Key faltante o formato incorrecto (401):** ```json { "error": "Missing or invalid API key. Use: Authorization: Bearer sk_proj_..." } ``` **Key inválida (401):** ```json { "error": "Invalid API key" } ``` --- ## Errores comunes | Código | Causa | Solución | | ------ | ------------------------------------------------- | ------------------------------------------------- | | `401` | Falta el header `Authorization` | Agrega `Authorization: Bearer sk_proj_...` | | `401` | Formato incorrecto (sin "Bearer " o sin prefijo `sk_`) | Asegúrate de usar `Bearer sk_proj_...` con espacio | | `401` | Key inválida o revocada | Regenérala desde **Integraciones → API Keys** en el dashboard | # Preguntas Frecuentes (/guides/faq) ## Envío de emails | Endpoint | Mejor para | Destinatarios | |---|---|---| | `POST /v1/send-email` | Emails individuales con HTML directo (transaccionales) | 1 | | `POST /v1/send-template-email` | Emails individuales usando un template guardado con variables | 1 | | `POST /v1/send-batch` | Envíos masivos a múltiples destinatarios | Hasta 10,000 | Reglas rápidas: - `send-email` cuando generas HTML desde tu sistema (confirmaciones, alertas). - `send-template-email` cuando tienes templates en el editor visual y solo envías variables. - `send-batch` para enviar el mismo email (con variables personalizadas) a muchos destinatarios. Sí. Tienes dos opciones: 1. **Múltiples llamadas a `/v1/send-batch`** — Divide destinatarios en lotes de hasta 10,000 y haz una llamada por lote. 2. **Campañas desde el Dashboard** — Crea una campaña sin límite. El sistema procesa automáticamente por lotes con rate limiting integrado. Ideal para 50k+ emails. | Límite | Valor | |---|---| | Destinatarios por batch request | 10,000 | | Destinatarios por envío individual (`/v1/send-email`) | 50 | | Adjuntos | 10 por correo, 10 MB total | Además aplican límites de volumen diario y mensual según tu cuenta. Si los alcanzas, los envíos se encolan y se reanudan automáticamente. Contacta soporte si necesitas más capacidad. Sí. Usa el campo `scheduled_at` con ISO 8601: ```json { "scheduled_at": "2026-04-01T10:00:00Z" } ``` Funciona en `/send-email` (acepta ISO 8601, lenguaje natural como `"tomorrow at 3pm"` y campo `timezone`) y en `/v1/send-batch` (solo ISO 8601). Ver [Programar Envíos](/guides/scheduling). El email se omite automáticamente. El comportamiento depende del endpoint: - **`/v1/send-template-email`**: la supresión se detecta al momento — responde `200` con `"skipped": true` y `suppression_reason` (ver [API v1](/api-reference/public-api)). - **`/v1/send-email`, `/v1/send-batch` y `/send-email`**: la API acepta la solicitud con `200` como cualquier envío, y la supresión se aplica al procesarla en segundo plano. El correo nunca se envía y el registro de actividad queda con `current_status: "suppressed"` — consultable vía la [Activity API](/api-reference/activity) con el `email_id` de la respuesta. La lista de supresión incluye destinatarios por: - **Baja** — clic en enlace de unsubscribe - **Hard bounce** — rebote permanente - **Queja** — marcado como spam Cada envío retorna un `activity_id` (o `email_id`). Los eventos de entrega actualizan el estado: ``` queued → sent → delivered (éxito) queued → sent → bounced (rebote) queued → sent → complained (spam) ``` Estado visible en Dashboard → Actividad, o consultable vía la [Activity API](/api-reference/activity). Para `send-batch`, hay un `activity_id` por destinatario. ## Dominios y deliverability Causas más comunes: 1. **Dominio no verificado** — DKIM, SPF, DMARC. Verifica con `POST /domains/:domain/verify`. 2. **Dominio frío** — necesita warming gradual: - Día 1: ~50,000 emails - Día 2: ~75,000 emails - Día 3+: resto 3. **Contenido sospechoso** — muchos enlaces, mayúsculas, palabras como "GRATIS"/"OFERTA"/"URGENTE", imágenes sin alt. 4. **Alta tasa rebotes/quejas** — limpia tu lista regularmente. >2% bounces o >0.1% complaints degrada reputación. 5. **Sin enlace de baja** — ReallyQuickEmails agrega `List-Unsubscribe` automáticamente, pero incluye un enlace visible. 1. Registra el dominio con `POST /domains/register` 2. Configura los 7 records DNS en tu proveedor: - 1 TXT verificación de dominio - 3 CNAME DKIM - 1 TXT SPF - 1 TXT DMARC - 1 CNAME Return-Path 3. Espera 5–10 min para propagación 4. Verifica con `POST /domains/:domain/verify` 5. Cuando `can_send: true`, listo Guía completa en [Dominios](/api-reference/domains). | Proveedor | Tiempo típico | |---|---| | Cloudflare | 1–5 min | | GoDaddy | 5–30 min | | Namecheap | 5–30 min | | Route 53 (AWS) | 1–5 min | | Otros | hasta 48 h | ## Templates | Sintaxis | Ejemplo | |---|---| | Doble llave | `{{nombre}}` | | Llave simple | `{nombre}` | También: - **Fallback** — `{nombre || "Cliente"}` - **HTML crudo** — `{!htmlContent}` - **Loops** — `{{#each productos}}...{{/each}}` - **Condicionales** — `{{#if premium}}...{{/if}}` Ver [Templates y Variables](/guides/templates). | Helper | Uso | Ejemplo | |---|---|---| | `formatCurrency` | Moneda | `{{formatCurrency total}}` → `$61,970.00` | | `multiply` | Multiplicación | `{{multiply cantidad precio}}` | | `formatDate` | Fecha legible | `{{formatDate fecha}}` → `15/3/2026` | | `formatDate "long"` | Formato largo | `{{formatDate fecha "long"}}` → `domingo, 15 de marzo de 2026` | | `default` | Default | `{{default nombre "Cliente"}}` | | `json` | Serializa a JSON | `{{json datos}}` | Sí. Los templates creados en el editor visual funcionan en: - `POST /v1/send-template-email` (individual, `template_id` + `variables`) - `POST /v1/send-batch` (masivo, `templateId` + `data` por destinatario) - Campañas desde Dashboard ## Campañas | Aspecto | send-batch (API) | Campaña (Dashboard) | |---|---|---| | Límite | 10,000 por request | Sin límite | | Creación | Código (curl, SDK) | Asistente visual de 5 pasos | | Template | HTML directo o templateId | Editor visual de campañas | | Variables | `data` por destinatario | CSV upload, mapeo, defaults | | Lotes | Manual | Automático | | Warming | Manual | Modo por lotes configurable | | Reintentos | No | Automático | | Stats | Vía activity_id individual | Dashboard con métricas agregadas | Regla: `send-batch` para integraciones programáticas. Campañas para envíos grandes desde UI. Sí. Desde el Dashboard puedes pausar una campaña en `processing`. Los emails ya encolados se siguen enviando pero no se despachan nuevos lotes. Reanuda desde el Dashboard. Se alcanzó el límite de envío de tu cuenta. Se reanuda automáticamente cuando el límite se restablece. También puedes reanudarla manualmente desde el Dashboard. ## API y autenticación 1. Dashboard → tu proyecto → **Configuración → Integraciones → API Keys** 2. Copia la key correspondiente: - **Live** (`sk_live_*` o `sk_proj_*`) → tráfico productivo - **Test** (`sk_test_*`) → desarrollo, no consume cuota Ver [API Keys](/guides/api-keys) para detalles. | Causa | Solución | |---|---| | Falta header `Authorization` | Agrega `Authorization: Bearer sk_live_...` | | Formato incorrecto | Verifica el espacio después de `Bearer` | | Prefijo no reconocido | Solo `sk_proj_*`, `sk_live_*`, `sk_test_*` | | Key inválida o revocada | Genera una nueva desde Configuración → API Keys | Los endpoints no tienen rate limiting propio. El límite lo impone la capacidad de envío de tu cuenta. Si alcanzas el límite, los emails se encolan y se envían automáticamente cuando hay capacidad. Pasa un header `Idempotency-Key: ` (1–256 caracteres) en `/send-email` o `/v1/send-batch`. Cualquier reintento con la misma key dentro de 24h devuelve la respuesta cacheada con `Idempotency-Replayed: true`. Ver [API Keys](/guides/api-keys#idempotency). ## Soporte Contáctanos en [soporte@reallyquickemails.com](mailto:soporte@reallyquickemails.com) incluyendo: Project ID, endpoint usado, request body, error/respuesta y timestamp aproximado. # Quickstart (/guides/quickstart) Esta guía te lleva de cero a tu primer email enviado con ReallyQuickEmails en menos de 5 minutos. Vamos a usar la **Test key** — envía emails reales, marcados como test para no contaminar tus métricas de producción. Si todavía no tienes cuenta, regístrate en [app.reallyquickemails.com](https://app.reallyquickemails.com). Necesitas un dominio verificado para enviar; ver [guía de dominios](/api-reference/domains). ### Obtener tu Test API Key 1. Ingresa al [dashboard](https://app.reallyquickemails.com). 2. Selecciona tu proyecto. 3. En el menú lateral, abre **Integraciones → API Keys**. 4. Copia la **Test key** (`sk_test_*`). Si después necesitas tráfico productivo, repite con la **Live key** (`sk_live_*` o `sk_proj_*`). Guárdala como variable de entorno: ```bash export RQE_API_KEY="sk_test_tu_test_key" ``` No hardcodees keys en código del lado del cliente. Úsalas solo desde backend. ### Enviar tu primer email Usa `POST /v1/send-email` con HTML directo: ```bash curl -X POST https://api.reallyquickemails.com/v1/send-email \ -H "Authorization: Bearer $RQE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "recipient_email": "tu@email.com", "sender_email": "noreply@tudominio.com", "sender_name": "Tu Empresa", "subject": "Hola desde ReallyQuickEmails", "html_body": "

¡Hola!

Este es mi primer email.

" }' ```
```javascript const res = await fetch('https://api.reallyquickemails.com/v1/send-email', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.RQE_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ recipient_email: 'tu@email.com', sender_email: 'noreply@tudominio.com', sender_name: 'Tu Empresa', subject: 'Hola desde ReallyQuickEmails', html_body: '

¡Hola!

Este es mi primer email.

', }), }); const data = await res.json(); console.log(data); // { message: 'Email sent successfully', email_id: '...', project_id: '...' } ```
```python import os, requests res = requests.post( 'https://api.reallyquickemails.com/v1/send-email', headers={ 'Authorization': f"Bearer {os.environ['RQE_API_KEY']}", 'Content-Type': 'application/json', }, json={ 'recipient_email': 'tu@email.com', 'sender_email': 'noreply@tudominio.com', 'sender_name': 'Tu Empresa', 'subject': 'Hola desde ReallyQuickEmails', 'html_body': '

¡Hola!

Este es mi primer email.

', }, ) print(res.json()) ```
### Validar la respuesta Una respuesta exitosa retorna `200 OK` con: ```json { "message": "Email sent successfully", "email_id": "550e8400-e29b-41d4-a716-446655440000", "project_id": "123e4567-e89b-12d3-a456-426614174000" } ``` Errores comunes: | Status | Causa | Solución | |---|---|---| | `401` | API key inválida o sin prefijo correcto | Verifica `Authorization: Bearer sk_test_...` | | `400` | Falta campo requerido | Revisa `recipient_email`, `sender_email`, `html_body` | Un `sender_email` no verificado **no** produce error síncrono: el request responde `200` y el envío falla en segundo plano. Revisa el estado del envío en Activity y verifica tu dominio en [Dominios](/api-reference/domains). ### Validar el envío en el dashboard En **Activity** del dashboard, verás el envío marcado con `is_test=true` (porque usaste `sk_test_*`). Cuando el destinatario abra el email, el evento `email.open` llega al `webhook_url_dev` de tu proyecto si está configurado. Ver [Webhooks](/api-reference/webhooks) para detalles.
## Próximos pasos Repite el envío con `sk_live_*` (o `sk_proj_*`) cuando quieras enviar a clientes reales. Configura `webhook_url` y `inbound_webhook_url` en tu proyecto. - **[Templates con variables](/guides/templates)** — Handlebars, `{{variables}}`, helpers. - **[Programar envíos](/guides/scheduling)** — fechas futuras con timezones. - **[Adjuntos](/guides/attachments)** — PDFs, .ics calendar events. - **[Send batch](/api-reference/public-api)** — hasta 10,000 destinatarios por request. - **[Webhooks](/api-reference/webhooks)** — recibir eventos `email.delivery`, `email.open`, `email.click`, `email.bounce`. # Programar Envíos (/guides/scheduling) La API de ReallyQuickEmails permite programar el envío de correos para una fecha y hora futura usando el endpoint [`POST /send-email`](/api-reference/send-email). Puedes especificar la fecha de envío con timestamps ISO 8601 o con lenguaje natural en inglés. > Para programar envíos masivos, `POST /v1/send-batch` también acepta `scheduled_at` en formato ISO 8601. Ver [API Pública v1](/api-reference/public-api). ## Campos relevantes | Campo | Tipo | Descripción | |---|---|---| | `scheduled_at` | `string` | Fecha y hora de envío. Acepta ISO 8601 o lenguaje natural en inglés. | | `timezone` | `string` | Nombre de zona horaria IANA. Opcional; solo aplica cuando `scheduled_at` es lenguaje natural. | ## Formato de `scheduled_at` ### Timestamps ISO 8601 Puedes especificar una fecha y hora exacta en formato ISO 8601. Incluye siempre el offset de zona horaria (o `Z` para UTC) — es la forma recomendada de programar un instante exacto: ``` "2026-10-16T15:00:00+09:00" "2026-12-25T09:00:00-03:00" "2027-01-15T08:30:00Z" ``` Cuando `scheduled_at` es un timestamp ISO 8601 con offset, el campo `timezone` se ignora: el offset del timestamp determina el instante de envío. ### Lenguaje natural También puedes usar expresiones en lenguaje natural (en inglés). La API las interpreta en relación al momento actual: ``` "tomorrow at 3pm" "in 2 hours" "next monday at 9am" ``` Cuando usas lenguaje natural, el campo `timezone` (nombre IANA) define la zona horaria en la que se confirma la programación (`scheduled_for_local` en la respuesta). Si no se especifica, se usa la zona horaria por defecto del proyecto (`default_timezone`); si el proyecto no tiene una configurada, se asume UTC. Verifica siempre el campo `scheduled_for` (UTC) de la respuesta para confirmar el instante exacto de envío. Si necesitas precisión absoluta, usa un timestamp ISO 8601 con offset. ## Formato de `timezone` El campo `timezone` acepta nombres de zona horaria IANA: | Formato | Ejemplo | |---|---| | Nombre IANA | `"America/Santiago"`, `"Asia/Tokyo"`, `"Europe/Madrid"` | | UTC | `"UTC"` | Para programar con un offset UTC fijo (por ejemplo `-03:00`), inclúyelo directamente en el timestamp ISO 8601 de `scheduled_at` en lugar de usar el campo `timezone`. Si no se incluye `timezone` en la solicitud, la API usa el valor de `default_timezone` configurado en el proyecto, o UTC si no hay uno configurado. ## Validacion La fecha programada debe ser en el futuro. Si `scheduled_at` resuelve a una fecha en el pasado, la API retorna `400 Bad Request` con el mensaje `scheduled_at must be in the future`. ## Procesamiento Los correos programados se procesan en segundo plano cada 30 segundos. Esto significa que un correo programado puede enviarse hasta 30 segundos después de la hora programada. ## Limitaciones - **Un destinatario por envío programado.** Si `recipient` es un array, solo se programa el envío al primer destinatario. - **`cc`, `bcc`, `attachments` y `senderName` no se conservan** en envíos programados — solo aplican a envíos inmediatos. - El contenido se define con `html` + `subject`, o con `templateId` (UUID de una plantilla). El objeto `data` con variables sí se conserva y se aplica al momento del envío. ## Respuesta Cuando se programa un correo exitosamente, la respuesta incluye los siguientes campos: | Campo | Descripción | |---|---| | `success` | `true` si la solicitud fue aceptada. | | `scheduled` | `true` cuando el correo quedó programado (en lugar de encolado para envío inmediato). | | `scheduled_send_id` | UUID del correo programado. | | `scheduled_for` | Fecha y hora de envío en UTC (ISO 8601). | | `scheduled_for_local` | Fecha y hora de envío en ISO 8601 con el offset de la zona horaria usada. | | `timezone` | Zona horaria utilizada: nombre IANA, o etiqueta de offset (ej. `"UTC+9"`) cuando viene del timestamp ISO. | | `message` | Mensaje de confirmación (en inglés). | ## Ejemplos ### Programar con timestamp ISO 8601 ```bash curl -X POST https://api.reallyquickemails.com/send-email \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -d '{ "recipient": "cliente@ejemplo.com", "sender": "ventas@mitienda.com", "subject": "Recordatorio de cita", "html": "

Te recordamos tu cita de manana a las 15:00.

", "scheduled_at": "2026-10-16T15:00:00+09:00" }' ``` Respuesta: ```json { "success": true, "scheduled": true, "scheduled_send_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "scheduled_for": "2026-10-16T06:00:00.000Z", "scheduled_for_local": "2026-10-16T15:00:00.000+09:00", "timezone": "UTC+9", "message": "Email scheduled for October 16, 2026 at 3:00 PM GMT+9" } ``` ### Programar con lenguaje natural y zona horaria ```bash curl -X POST https://api.reallyquickemails.com/send-email \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -d '{ "recipient": "cliente@ejemplo.com", "sender": "ventas@mitienda.com", "subject": "Oferta especial", "html": "

Aprovecha nuestra oferta especial de esta semana.

", "scheduled_at": "tomorrow at 3pm", "timezone": "America/Santiago" }' ``` Respuesta (valores ilustrativos — dependen del momento de la solicitud): ```json { "success": true, "scheduled": true, "scheduled_send_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "scheduled_for": "2026-06-06T19:00:00.000Z", "scheduled_for_local": "2026-06-06T15:00:00.000-04:00", "timezone": "America/Santiago", "message": "Email scheduled for June 6, 2026 at 3:00 PM GMT-4" } ``` ### Programar con offset numerico El campo `timezone` solo acepta nombres IANA. Para programar con un offset UTC fijo, especifica el offset directamente en el timestamp ISO 8601: ```bash curl -X POST https://api.reallyquickemails.com/send-email \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -d '{ "recipient": "cliente@ejemplo.com", "sender": "ventas@mitienda.com", "subject": "Seguimiento", "html": "

Hacemos seguimiento de tu solicitud.

", "scheduled_at": "2026-10-20T09:00:00-03:00" }' ``` Respuesta: ```json { "success": true, "scheduled": true, "scheduled_send_id": "c3d4e5f6-a7b8-9012-cdef-123456789012", "scheduled_for": "2026-10-20T12:00:00.000Z", "scheduled_for_local": "2026-10-20T09:00:00.000-03:00", "timezone": "UTC-3", "message": "Email scheduled for October 20, 2026 at 9:00 AM GMT-3" } ``` # SDK de Node.js (/guides/sdk-nodejs) SDK oficial de ReallyQuickEmails para Node.js / TypeScript. Cubre los 5 módulos de la API: `emails`, `leads`, `automations`, `events` y `senders`. - **Repo:** [github.com/DropoutCapital/reallyquickemails-sdk](https://github.com/DropoutCapital/reallyquickemails-sdk) - **npm:** [@reallyquickemails/sdk](https://www.npmjs.com/package/@reallyquickemails/sdk) - **License:** MIT - **Tamaño:** ~13 KB CJS / ~12 KB ESM, zero runtime deps (usa `fetch` nativo) Funciona en cualquier runtime con `fetch` nativo: Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge. **NO está diseñado para correr en el browser** — el constructor lanza un error si detecta `window`, para evitar exponer el API key en el bundle del cliente. ## Instalación ```bash npm install @reallyquickemails/sdk ``` ```bash pnpm add @reallyquickemails/sdk ``` ```bash yarn add @reallyquickemails/sdk ``` ```bash bun add @reallyquickemails/sdk ``` ## Quickstart ### Obtener API key Genera una key en `Configuración → Integraciones → API Keys`. Para empezar usa una **Test key** (`sk_test_*`) — envía emails reales sin contaminar tus métricas de producción. Ver [API Keys](/guides/api-keys). ```bash export RQE_API_KEY="sk_test_tu_test_key" ``` ### Inicializar el cliente ```ts import { RQE } from '@reallyquickemails/sdk'; const rqe = new RQE({ apiKey: process.env.RQE_API_KEY }); ``` Si omites `apiKey`, el SDK lee `process.env.RQE_API_KEY` automáticamente. Para apuntar a un backend distinto (staging, self-hosted) pasa `baseUrl` o define la variable de entorno `RQE_BASE_URL`: ```ts const rqe = new RQE({ apiKey: process.env.RQE_API_KEY, baseUrl: 'https://api-staging.ejemplo.com', }); ``` ### Mandar tu primer email ```ts const { data, error } = await rqe.emails.send({ sender: 'soporte@tudominio.com', // sender verificado recipient: 'usuario@example.com', subject: 'Bienvenido', html: '

Hola desde RQE

', }); if (error) { console.error('Send falló:', error.message); } else { console.log('Email encolado, id:', data.email_id); } ``` El sender debe estar verificado en tu proyecto. Ver [dominios](/api-reference/domains).
## Manejo de errores Cada método devuelve `{ data, error, headers }`. **Siempre revisa `error` antes de usar `data`** — `data` es `null` cuando hay error. ```ts const { data, error, headers } = await rqe.emails.send({ /* ... */ }); if (error) { switch (error.statusCode) { case 401: /* API key inválida */ break; case 429: /* Rate limit — espera y reintenta */ break; default: console.error(error); } return; } console.log(data.email_id); console.log('Idempotency replayed:', headers['idempotency-replayed']); ``` | `error.name` | Cuándo | |---|---| | `unauthorized` | API key inválida o ausente | | `validation_error` | Body inválido (campos requeridos faltantes, formato malo) | | `not_found` | Recurso no existe (lead, automation, event) | | `network_error` | Falla de red — no llegó respuesta del backend | | `parse_error` | Backend devolvió respuesta no-JSON | | `application_error` | Error genérico backend (5xx) | ## Módulos ### `emails` — transactional + broadcast ```ts // Single email await rqe.emails.send({ sender: 'soporte@brand.com', recipient: 'user@example.com', subject: 'Confirmación', html: '

Tu pedido fue recibido.

', attachments: [{ filename: 'factura.pdf', content: base64Pdf }], }); // Broadcast — un template a hasta 10,000 recipients await rqe.emails.sendBatch({ sender: 'noreply@brand.com', subject: 'Hola {{name}}', html: '

Hola {{name}}, tu plan {{plan}} se renueva pronto.

', recipients: [ { email: 'a@x.com', data: { name: 'Alice', plan: 'Pro' } }, { email: 'b@x.com', data: { name: 'Bob', plan: 'Free' } }, ], }); ``` ### `leads` — gestión de contactos ```ts // Upsert por email await rqe.leads.upsert({ email: 'user@example.com', data: { plan: 'pro', signup_date: '2026-04-30' }, segment_ids: ['seg-uuid-1'], }); // Bulk hasta 1000 await rqe.leads.upsertMany({ leads: [ { email: 'a@x.com', data: { plan: 'pro' } }, { email: 'b@x.com' }, ], }); // Tags por email await rqe.leads.addTags('user@example.com', ['vip', 'beta']); const { data } = await rqe.leads.getTags('user@example.com'); // Atributos custom (body raw, NO wrapped) await rqe.leads.setAttributes('user@example.com', { total_orders: 42, last_order_date: '2026-04-30', }); // Listar con paginación const page = await rqe.leads.list({ page: 1, per_page: 50, search: 'pro' }); // Get / update / delete por UUID const { data: { lead } } = await rqe.leads.get('lead-uuid'); await rqe.leads.update('lead-uuid', { data: { plan: 'enterprise' } }); await rqe.leads.delete('lead-uuid'); ``` ### `automations` — disparar flujos ```ts // Enroll idempotente — pasa trigger_reference_id para que múltiples calls // con el mismo external_id NO creen runs duplicados, sino que actualicen el run existente await rqe.automations.enroll('automation-uuid', { email: 'user@example.com', data: { first_name: 'Adriano', plan: 'pro' }, trigger_reference_id: 'order-12345', }); // Manual enroll (UI-style bulk) await rqe.automations.manualEnroll('automation-uuid', { emails: ['a@x.com', 'b@x.com'], }); // Cancel por recipient await rqe.automations.cancel('automation-uuid', { email: 'user@example.com', reason: 'payment_succeeded', }); // Cancel por external_id (cuando no tienes el email) await rqe.automations.cancelByExternalId('automation-uuid', { external_id: 'order-12345', }); ``` ### `events` — tracking custom ```ts // Single event await rqe.events.track({ email: 'user@example.com', event: 'offer_accepted', properties: { offer_id: 'xyz', amount_usd: 5000 }, }); // Bulk hasta 1000 await rqe.events.bulk([ { email: 'a@b.com', event: 'page_view', properties: { url: '/pricing' } }, { email: 'c@d.com', event: 'page_view', properties: { url: '/features' } }, ]); // Listar con filtros + paginación offset const { data } = await rqe.events.list({ email: 'user@example.com', event: 'offer_accepted', since: '2026-04-01T00:00:00Z', per_page: 50, }); ``` ### `senders` — listar / verificar remitentes (v0.1.1+) Permite descubrir programáticamente qué senders están verificados en el proyecto, verificar nuevos vía magic link y consultar el estado actual de un remitente. Este módulo requiere `projectId` en el constructor (o `RQE_PROJECT_ID` env). Encuéntralo en la URL del dashboard: `app.reallyquickemails.com/projects//...`. Los otros módulos (emails, leads, automations, events) no necesitan este parámetro. ```ts const rqe = new RQE({ apiKey: process.env.RQE_API_KEY, projectId: process.env.RQE_PROJECT_ID, }); // Listar todos los senders const { data } = await rqe.senders.list(); // data.senders = [{ email, sender_name, status, type, ... }, ...] // Filtrar solo verificados const { data: verified } = await rqe.senders.list({ status: 'verified' }); // Elegir sender en runtime según contexto const transactionalSender = verified.senders.find(s => s.email.startsWith('noreply@')); await rqe.emails.send({ sender: transactionalSender.email, recipient: 'user@example.com', subject: 'Confirmación', html: '

...

', }); // Verificar un sender nuevo (se envía un magic link al email) await rqe.senders.verifyEmail({ email: 'support@brand.com' }); // Check status de un sender específico const { data: st } = await rqe.senders.status('support@brand.com'); // st.status: 'Success' | 'Pending' | 'Failed' | 'NotStarted' ``` Casos de uso típicos: - **Multi-tenant** — listar senders del proyecto activo y elegir según el cliente - **Failover** — si un sender falla DKIM/DMARC, fallback automático a otro verified - **Onboarding wizard** — UI muestra senders disponibles para que el cliente elija - **Health check** — poll periódico de status para detectar senders fallados ## Idempotency Para retries seguros (timeouts de red, reintentos de Lambda, etc), pasa `idempotencyKey` en operaciones de escritura. Hoy soportado en `emails.send` y `emails.sendBatch`. ```ts await rqe.emails.send( { sender: 'soporte@brand.com', recipient: 'user@example.com', subject: 'Pedido #123', html: '

Confirmación

', }, { idempotencyKey: 'order-123-confirmation' }, ); ``` El backend cachea por 24h la respuesta para el par `(project_id, idempotencyKey)`. Una segunda llamada con la misma key devuelve la respuesta cacheada **sin re-procesar** y agrega el header `Idempotency-Replayed: true`. ```ts const { data, headers } = await rqe.emails.send( { /* ... */ }, { idempotencyKey: 'unique-id' }, ); if (headers['idempotency-replayed'] === 'true') { console.log('Replay — backend ya había procesado este request'); } ``` Más sobre el comportamiento del backend en [API Keys → Idempotency](/guides/api-keys). ## Compatibilidad de runtime | Runtime | Soportado | Notas | |---|---|---| | Node.js 18+ | ✅ | `fetch` nativo | | Node.js 16 | ❌ | Sin `fetch` nativo | | Bun | ✅ | | | Deno | ✅ | | | Cloudflare Workers | ✅ | | | Vercel Edge | ✅ | | | Browser | ❌ | El SDK throw si detecta `window` para proteger tu API key | ## Configuración | Opción | Tipo | Default | Descripción | |---|---|---|---| | `apiKey` | `string` | `process.env.RQE_API_KEY` | Tu API key del proyecto | | `baseUrl` | `string` | `process.env.RQE_BASE_URL` o `https://api.reallyquickemails.com` | Override para staging o self-hosted | | `projectId` | `string` | `process.env.RQE_PROJECT_ID` | UUID del proyecto. **Requerido solo por el módulo `senders`** — los demás módulos lo derivan del API key. | ## Soporte - **Bugs y feature requests:** [GitHub Issues](https://github.com/DropoutCapital/reallyquickemails-sdk/issues) - **Email:** [soporte@reallyquickemails.com](mailto:soporte@reallyquickemails.com) # Templates y Variables (/guides/templates) ReallyQuickEmails permite crear correos dinámicos con variables, condicionales, loops y helpers de formateo que se resuelven al momento del envío. Hay dos formas de enviar con variables, cada una con su propio motor de renderizado: | Endpoint | Campo de variables | Motor | |---|---|---| | [`POST /v1/send-template-email`](/api-reference/public-api#post-v1send-template-email) | `variables` | Sustitución simple: `{var}` o `{{var}}`, fallbacks con valor por defecto y loops `{{#each}}` | | [`POST /send-email`](/api-reference/send-email) (API avanzada) | `data` | Handlebars completo: condicionales `{{#if}}`, helpers y propiedades anidadas | --- ## Conceptos basicos ### Variables Inserta valores dinámicos en tu template con `{{nombreVariable}}`. **En el template:** ```html

Hola, {{nombre}}!

Tu cuenta en {{empresa}} ha sido creada exitosamente.

``` **En el request body (API pública v1):** ```json { "template_id": "550e8400-e29b-41d4-a716-446655440000", "variables": { "nombre": "Carlos", "empresa": "TechStore" } } ``` **Resultado renderizado:** ```html

Hola, Carlos!

Tu cuenta en TechStore ha sido creada exitosamente.

``` En `/v1/send-template-email` (campo `variables`), además: - `{nombreVariable}` con llave simple es equivalente a `{{nombreVariable}}`. - Puedes definir un fallback con `{variable || "valor por defecto"}` para cuando la variable no existe. - Los valores se escapan como HTML por defecto; usa `{!variable}` para insertar HTML sin escapar. - Las variables sin valor (y sin fallback) se renderizan como cadena vacía. En la API avanzada (`/send-email`, campo `data`) usa siempre la sintaxis de doble llave `{{variable}}`; la llave simple y los fallbacks `||` no están disponibles, y los valores se insertan sin escape HTML. --- ### Condicionales > Disponibles solo en la **API avanzada** (`POST /send-email`, campo `data`). `/v1/send-template-email` no soporta bloques `{{#if}}`. Usa `{{#if variable}}...{{/if}}` para mostrar contenido solo cuando una variable existe y es truthy. Combina con `{{else}}` para manejar el caso contrario. **En el template:** ```html {{#if premium}}

Gracias por ser miembro Premium. Tienes envío gratuito en todos tus pedidos.

{{else}}

Actualiza a Premium para obtener envío gratuito.

{{/if}} ``` **En el request body:** ```json { "data": { "premium": true } } ``` > **Nota:** Handlebars evalúa como falsy los valores `false`, `undefined`, `null`, `""`, `0` y arrays vacíos `[]`. --- ### Loops Usa `{{#each arreglo}}...{{/each}}` para iterar sobre una lista de elementos. Los loops están disponibles en ambas APIs, con diferencias de sintaxis dentro del bloque. **En la API avanzada** (`/send-email`, campo `data`), `{{this}}` referencia al elemento actual, puedes acceder a sus propiedades con `{{this.propiedad}}` y usar helpers: ```html {{#each productos}} {{/each}}
Producto Cantidad Precio
{{this.nombre}} {{this.cantidad}} {{formatCurrency this.precio}}
``` **En el request body:** ```json { "data": { "productos": [ { "nombre": "Camiseta Azul", "cantidad": 2, "precio": 15990 }, { "nombre": "Pantalon Negro", "cantidad": 1, "precio": 29990 }, { "nombre": "Zapatillas Blancas", "cantidad": 1, "precio": 45990 } ] } } ``` **Variables especiales dentro de loops (API avanzada):** | Variable | Descripción | | ---------------- | ------------------------------------------- | | `{{this}}` | El elemento actual de la iteración | | `{{@index}}` | Índice del elemento (comienza en 0) | | `{{@first}}` | `true` si es el primer elemento | | `{{@last}}` | `true` si es el último elemento | | `{{@key}}` | La clave actual (cuando se itera un objeto) | **En `/v1/send-template-email`** (campo `variables`), las propiedades del elemento actual se exponen directamente — usa `{{nombre}}` en lugar de `{{this.nombre}}` (la notación `this.` no está soportada). Las variables especiales disponibles son `@index`, `@first`, `@last`, `@odd` y `@even`, y no hay helpers de formateo: envía los valores ya formateados. --- ## Helpers integrados RQE incluye helpers de Handlebars adicionales para formateo común en correos transaccionales. > Los helpers están disponibles solo en la **API avanzada** (`POST /send-email`, campo `data`). En `/v1/send-template-email` envía los valores ya formateados. ### `formatCurrency` Formatea un número como moneda usando el formato `en-US`. Por defecto usa USD como moneda. Acepta un segundo parámetro opcional para especificar la moneda. ```html

Total: {{formatCurrency total}}

``` Con `"total": 61970` produce: `$61,970.00` Para usar otra moneda: ```html

Total: {{formatCurrency total "CLP"}}

``` ### `multiply` Multiplica dos valores numéricos. El resultado se redondea a 2 decimales. ```html

Subtotal: {{formatCurrency (multiply cantidad precio)}}

``` ### `formatDate` Formatea una fecha ISO 8601 a un formato legible en español (`es-ES`). Acepta un segundo parámetro opcional para el formato. **Formato por defecto (`short`):** ```html

Fecha: {{formatDate fechaEntrega}}

``` Con `"fechaEntrega": "2025-03-15T00:00:00Z"` produce: `15/3/2025` **Formato largo (`long`):** ```html

Fecha: {{formatDate fechaEntrega "long"}}

``` Con `"fechaEntrega": "2025-03-15T00:00:00Z"` produce: `sábado, 15 de marzo de 2025` ### `default` Proporciona un valor por defecto cuando la variable es falsy (`undefined`, `null`, `""`, `0` o `false`). ```html

Hola, {{default nombre "Cliente"}}!

``` Si `nombre` no está definido en `data`, se renderiza como `Hola, Cliente!`. ### `json` Serializa un objeto a formato JSON con indentación. Útil para depuración o para incluir datos estructurados en el correo. ```html
{{json datos}}
``` Con `"datos": {"clave": "valor"}` produce: ```json { "clave": "valor" } ``` También están disponibles los helpers de texto `capitalize`, `uppercase` y `lowercase` para transformar strings. --- ## Identificar un template Puedes referenciar un template de dos formas en la API pública (`/v1/send-template-email`): | Campo | Tipo | Descripción | |---|---|---| | `template_id` | string (UUID) | UUID único del template | | `template_internal_id` | number | ID interno auto-incrementado (por proyecto) | ```json { "template_id": "550e8400-e29b-41d4-a716-446655440000", "recipient_email": "usuario@ejemplo.com", "sender_email": "tienda@tudominio.com", "variables": { "nombre": "Ana", "numeroPedido": "5021" } } ``` - Si el template no existe, la API retorna un error `404`. - `template_internal_id` se resuelve dentro del proyecto asociado a la API key utilizada. En la API avanzada (`/send-email`), el campo `templateId` acepta el UUID del template o su ID interno numérico, siempre dentro del proyecto asociado a la API key. --- ## El objeto `variables` En `/v1/send-template-email`, el objeto `variables` es un JSON donde cada clave corresponde a una variable del template. Los valores se convierten a string al renderizar; las variables que el template usa pero que no existen en `variables` se renderizan como cadena vacía (o con su fallback `||` si está definido). ### Ejemplo completo **Request:** ```bash curl -X POST https://api.reallyquickemails.com/v1/send-template-email \ -H "Authorization: Bearer sk_proj_xxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "template_id": "550e8400-e29b-41d4-a716-446655440000", "recipient_email": "juan.perez@ejemplo.com", "sender_email": "pedidos@mitienda.com", "variables": { "nombre": "Juan", "numeroPedido": "3847", "fechaCompra": "20 de febrero de 2026", "productos": [ { "producto": "Audifonos Bluetooth", "cantidad": 1, "precio": "$34.990" }, { "producto": "Cable USB-C", "cantidad": 3, "precio": "$5.990" } ], "total": "$52.960" } }' ``` **Template correspondiente (sintaxis v1):** ```html

Gracias por tu compra, {{nombre}}!

Pedido #{{numeroPedido}} - {{fechaCompra}}

{{#each productos}} {{/each}}
{{producto}} x{{cantidad}} {{precio}}

Total: {{total}}

Atendido por: {vendedor || "nuestro equipo"}

``` Como este motor no incluye helpers de formateo, los precios y fechas se envían ya formateados como strings. Si necesitas condicionales, helpers o propiedades anidadas, usa la API avanzada (`POST /send-email`) con `templateId` + `data`. --- ## Acceso a propiedades anidadas En la API avanzada (`/send-email`, campo `data`), usa notación de punto para acceder a objetos anidados: ```html

Ciudad: {{direccion.ciudad}}

Región: {{direccion.region}}

``` ```json { "data": { "direccion": { "ciudad": "Santiago", "region": "Metropolitana" } } } ``` En `/v1/send-template-email` la notación de punto **no** está soportada: aplana las variables antes de enviarlas (por ejemplo, `direccion_ciudad` en lugar de `direccion.ciudad`). --- ## Buenas practicas - **Define valores por defecto para variables opcionales:** en la API avanzada usa el helper `default`; en v1 usa fallbacks `{variable || "valor"}`. Así evitas espacios vacíos en el correo. - **Valida tus variables antes de enviar.** Si un template espera variables que no existen en el objeto enviado, se renderizan como cadenas vacías sin producir error. - **Guarda el `template_id` (UUID) en tu configuración**, o usa `template_internal_id` si prefieres identificadores numéricos cortos por proyecto. - **Prueba tus templates** con datos de ejemplo antes de integrar. En la API avanzada puedes usar `dry_run: true` para renderizar el correo sin enviarlo — ver [Dry Run](/api-reference/send-email#dry-run).