Documentación
Roomie API
La API de Roomie expone el pipeline completo de 4 agentes — analista, estratega, creativo, auditor — más el envío de emails, tracking, estadísticas y secuencias de follow-up autónomas. Todo JSON, todo sobre HTTPS, todo autenticado con un bearer token.
Casos de uso:
- Integrar Roomie con tu CRM hotelero y disparar campañas desde ahí.
- Programar envíos recurrentes vía cron o Zapier/Make.
- Leer métricas en vivo desde tu dashboard de BI.
- Construir un bot de Slack que cree campañas desde lenguaje natural.
Base URL
https://roomie.leiro.dev/api/v1
Versión actual
v1
Autenticación
Cada request protegida lleva una cabecera Authorization: Bearer {token}. Genera el token desde el dashboard en Ajustes → API. Solo se muestra una vez al crearlo — guárdalo en un vault.
Roomie almacena solo el hash SHA-256 del token, nunca el token en claro. Puedes revocarlo en cualquier momento desde el mismo panel y cualquier integración que lo esté usando dejará de funcionar inmediatamente.
curl https://roomie.leiro.dev/api/v1/providers \ -H "Authorization: Bearer sk-roomie-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Accept: application/json"
Ejemplo completo
Crea una campaña, espera a que el pipeline complete, lánzala, y pide las estadísticas. Todo con curl.
TOKEN="sk-roomie-xxxxxxxxxxxx..."
BASE="https://roomie.leiro.dev/api/v1"
# 1. Crear la campaña
CAMPAIGN_ID=$(curl -s -X POST $BASE/campaigns \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Junio cultural Granada",
"objective": "Subir la ocupación del Aurea Catedral en Granada durante junio",
"aggressiveness": 2,
"persuasion_patterns": 2,
"provider": "anthropic",
"api_key": "sk-ant-api03-..."
}' | jq -r .id)
# 2. Poll hasta que termine el pipeline (~60s)
while [ "$(curl -s $BASE/campaigns/$CAMPAIGN_ID/status \
-H "Authorization: Bearer $TOKEN" | jq -r .status)" != "completed" ]; do
sleep 3
done
# 3. Lanzar el envío a los 50 recipients mejor rankeados
curl -X POST $BASE/campaigns/$CAMPAIGN_ID/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"recipient_mode": "50", "enable_followups": true}'
# 4. Esperar un poco y pedir el dashboard
sleep 30
curl $BASE/campaigns/$CAMPAIGN_ID/stats \
-H "Authorization: Bearer $TOKEN" | jq
/health
Sin auth
Health check. Útil para load balancers y para verificar que el token se lee bien antes de hacer llamadas reales.
{
"ok": true,
"version": "v1",
"service": "roomie-api"
}
/providers
Lista los LLM providers que Roomie sabe usar. Úsalo para validar tu selector de provider antes de hacer un POST /campaigns.
{
"providers": [
{ "id": "anthropic", "label": "Anthropic Claude", "requires_custom_fields": false },
{ "id": "google", "label": "Google Gemini", "requires_custom_fields": false },
{ "id": "openai", "label": "OpenAI", "requires_custom_fields": false },
{ "id": "deepseek", "label": "DeepSeek", "requires_custom_fields": false },
{ "id": "custom", "label": "Custom", "requires_custom_fields": true }
]
}
/campaigns
Lista paginada de campañas del usuario dueño del token, ordenadas por fecha de creación descendente.
Query params
per_page |
Entero, default 25. Cuántas campañas por página. |
page |
Entero, default 1. Número de página. |
/campaigns
Crea una campaña y encola el pipeline de 4 agentes. Devuelve 202 Accepted con un poll_url para consultar el estado.
Body
objective |
String, 10–1000 chars. Requerido. |
name |
String opcional, máx 120 chars. |
aggressiveness |
Entero 0–5. 0 = informativa, 5 = agresiva. |
persuasion_patterns |
Entero 0–5. 0 = neutral, 5 = dark patterns. |
provider |
Uno de anthropic, google, openai, deepseek, custom. |
api_key |
Tu clave del provider. Se cifra con la APP_KEY. |
api_base_url |
Requerido si provider=custom. Endpoint compatible con chat completions de OpenAI. |
api_model |
Requerido si provider=custom. Nombre del modelo. |
// Response 202
{
"id": 42,
"status": "pending",
"poll_url": "https://roomie.leiro.dev/api/v1/campaigns/42/status"
}
/campaigns/:id
Detalle completo de la campaña — todos los outputs de los agentes, la configuración de envío y follow-ups, y los metadatos. El api_key nunca se devuelve.
/campaigns/:id/status
Respuesta ligera para hacer polling mientras el pipeline corre. Incluye has_analysis, has_strategy, etc. y previews cortos a medida que cada agente completa.
{
"id": 42,
"status": "processing",
"quality_score": null,
"has_analysis": true,
"has_strategy": true,
"has_creative": false,
"has_audit": false,
"analysis_preview": { "segments_count": 4, "focus_segment": "Parejas internacionales" },
"strategy_preview": { "hotel_name": "Aurea Catedral", "channel": "email", "segment": "Parejas internacionales" },
"creative_preview": null,
"audit_preview": null
}
/campaigns/:id/refine-creative
Aplica un ajuste en lenguaje natural al creative actual. La IA lee el objetivo, la estrategia y el creative vigente y devuelve un creative actualizado que solo toca lo que le pediste. Requiere que la campaña todavía tenga su clave API retenida — si ha sido borrada, devuelve key_not_available.
Body
refinement_prompt |
String 5–500 chars. Ejemplos: "asunto más corto", "añade un pull-quote sobre el atardecer", "suaviza el tono del último párrafo". |
// Response 200 — el Campaign completo con el creative actualizado
{
"data": {
"id": 42,
"creative": {
"subject_line": "Granada, ahora.",
"headline": "Los atardeceres más lentos del verano",
"body_html": "...",
...
},
...
}
}
/campaigns/:id/send
Selecciona recipients con el heurístico interno y dispatcha el envío. Opcionalmente activa la secuencia de follow-ups autónomos.
Body
recipient_mode |
Uno de 50, 100, 200, custom, all. |
recipient_count_custom |
Entero. Requerido si recipient_mode=custom. |
enable_followups |
Bool. Si true, activa la secuencia de seguimiento. |
followup_max_attempts |
Entero 2–5. Default 3. |
followup_cooldown_hours |
Entero 1–168. Default 48. |
followup_api_key |
Solo si la clave original ha sido borrada (retención expirada). Normalmente no hace falta. |
// Response 200
{
"dispatched": 50,
"total_queued": 50
}
/campaigns/:id/stop-followups
Detiene la secuencia de follow-ups y borra inmediatamente la clave API retenida del servidor. Es idempotente: llamarlo dos veces devuelve el mismo resultado.
// Response 200
{
"stopped": true,
"key_wiped": true
}
/campaigns/:id/stats
Dashboard completo en JSON: funnel, time series, breakdown por país, breakdown por segmento (edad + género), performance por intento de follow-up, y métricas agregadas.
{
"data": {
"campaign_id": 42,
"summary": { "sent": 50, "opened": 18, "clicked": 5, "converted": 5, "open_rate": 36, ... },
"funnel": [
{ "label": "Disparados", "count": 50, "pct_total": 100, "pct_prev": 100 },
{ "label": "Entregados", "count": 48, "pct_total": 96, "pct_prev": 96 },
{ "label": "Abiertos", "count": 18, "pct_total": 36, "pct_prev": 37.5 },
{ "label": "Clickados", "count": 5, "pct_total": 10, "pct_prev": 27.8 },
{ "label": "Convertidos", "count": 5, "pct_total": 10, "pct_prev": 100 }
],
"time_series": { "buckets": [...], "has_enough_data": true },
"country_breakdown": [ { "country": "ES", "sent": 25, "opened": 10, "clicked": 3, "pct": 100 }, ... ],
"segment_breakdown": { "age_range": [...], "gender": [...] },
"followup_performance": [ { "attempt": 1, "sent": 50, "opened": 18, "clicked": 5, ... } ]
}
}
/campaigns/:id/recipients
Lista paginada de recipients con su estado actual, contadores de opens/clicks, número de intentos enviados y todos los timestamps del lifecycle (open, click, convert, bounce, unsubscribe).
El tracking_token de cada recipient es un secreto interno y nunca se incluye en la respuesta.
/campaigns/:id/recipients/:recipient/toggle-conversion
Marca manualmente a un recipient como convertido (o deshace la marca). Útil cuando tu sistema de reservas real confirma una venta y quieres que Roomie pare los follow-ups para esa persona.
Tiempo real
Webhooks
En lugar de hacer polling a /campaigns/:id/status y /stats, suscribe un endpoint HTTPS a los eventos que te interesen y Roomie te los empujará con firma HMAC. Ideal para bots de Slack, sincronización con un CRM, o dashboards de BI que reflejen cada conversión en tiempo real.
Crea y gestiona webhooks desde Ajustes → API o desde los endpoints /webhooks del propio API. El secret de firma se muestra una sola vez al crear el webhook — guárdalo en un vault.
Protocolo
POST application/json
Firma
HMAC-SHA256
Catálogo de eventos
Al crear un webhook puedes suscribirte a un subset concreto o usar ["*"] para recibirlos todos.
Campaign lifecycle
campaign.created |
Se creó una campaña. El pipeline aún no ha arrancado. |
campaign.analysis_completed |
El Analista terminó. Payload incluye analysis. |
campaign.strategy_completed |
El Estratega terminó. Payload incluye strategy. |
campaign.creative_completed |
El Creativo terminó. Payload incluye creative con el body_html. |
campaign.audit_completed |
El Auditor terminó. Payload incluye audit y quality_score. |
campaign.completed |
El pipeline completó sin errores. Payload incluye el objeto campaign completo. |
campaign.failed |
El pipeline falló. Payload incluye error. |
campaign.creative_refined |
Se re-generó el creative con un prompt de refinamiento. Payload incluye creative e instructions. |
campaign.send_started |
Se disparó el envío inicial. Payload incluye dispatched y total_queued. |
campaign.followup_started |
Arrancó un batch de follow-up. Payload incluye attempt y recipient_count. |
Recipient lifecycle
recipient.sent |
Un email se entregó al MTA. Incluye el número de intento. |
recipient.bounced |
El transporte rechazó el envío. Payload incluye error. |
recipient.opened |
Se disparó la pixel de apertura por primera vez. Las aperturas sucesivas no vuelven a dispararse para evitar ruido. |
recipient.clicked |
El destinatario hizo click en el CTA por primera vez. |
recipient.converted |
Se marca al destinatario como convertido (normalmente en el primer click, o manualmente vía toggle-conversion). |
recipient.unsubscribed |
El destinatario hizo click en "Darse de baja" y su email entra en la lista global de opt-out. |
Payload
Todos los eventos comparten el mismo envelope. data cambia según el tipo.
{
"id": "8f9a4d0e-3c5b-4f12-9f6b-1b2a4d8c0e77",
"type": "campaign.completed",
"created": 1744080000,
"data": {
"campaign": {
"id": 42,
"name": "Junio cultural Granada",
"status": "completed",
"quality_score": 87,
"creative": { "subject_line": "...", "body_html": "..." },
...
}
}
}
Cada POST incluye además cabeceras:
X-Roomie-Event |
Tipo del evento (ej. campaign.completed). |
X-Roomie-Delivery |
UUID único por evento. Úsalo para deduplicar reintentos en tu sistema. |
X-Roomie-Signature |
Firma HMAC en formato t={timestamp},v1={hex}. |
User-Agent |
Roomie-Webhooks/1.0 |
Verificar la firma
La cabecera X-Roomie-Signature contiene un timestamp y una firma HMAC-SHA256 calculada sobre {timestamp}.{raw_body} con tu secret. Recomponla en tu endpoint, compárala en tiempo constante, y rechaza cualquier delivery con un t que se desvíe más de 5 minutos del reloj actual.
PHP
$secret = getenv('ROOMIE_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_ROOMIE_SIGNATURE'] ?? '';
parse_str(strtr($header, ',', '&'), $parts);
$timestamp = (int) ($parts['t'] ?? 0);
$signature = (string) ($parts['v1'] ?? '');
if (abs(time() - $timestamp) > 300) {
http_response_code(400);
exit('stale');
}
$expected = hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit('bad signature');
}
http_response_code(200);
Node.js
import crypto from 'node:crypto';
export function verifyRoomieSignature(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
const t = parseInt(parts.t, 10);
if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(parts.v1, 'hex'),
);
}
Reintentos y auto-desactivación
Un evento se intenta entregar hasta 5 veces con backoff exponencial: 10s, 30s, 2m, 5m, 15m. Cualquier respuesta 2xx se considera éxito. Cualquier 4xx/5xx o timeout reintenta hasta agotar los intentos.
El webhook lleva un contador de eventos consecutivos fallidos (no intentos — eventos). Si llega a 10, Roomie desactiva el webhook automáticamente para dejar de quemar capacidad. Verás active: false y podrás re-activarlo desde Ajustes → API o con PATCH /webhooks/:id. Una respuesta 2xx exitosa resetea el contador.
Las entregas se archivan durante 7 días en GET /webhooks/:id/deliveries para debugging. Después se purgan.
Endpoints de gestión
/webhooks
Lista los webhooks del usuario autenticado, paginados de 25 en 25.
/webhooks
Crea un webhook. La respuesta 201 devuelve el secret en plano — es la única vez que se muestra. Guárdalo en un vault antes de hacer cualquier otra request.
Body
name |
string (requerido, max 120) — etiqueta visible en el UI. |
url |
URL HTTPS (requerida). HTTP plano no se acepta. |
events |
Array de event types. Usa ["*"] para todos. |
curl -X POST https://roomie.leiro.dev/api/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Slack de growth",
"url": "https://hooks.mi-empresa.com/roomie",
"events": ["campaign.completed", "recipient.converted"]
}'
# 201
{
"id": 7,
"name": "Slack de growth",
"url": "https://hooks.mi-empresa.com/roomie",
"events": ["campaign.completed", "recipient.converted"],
"active": true,
"secret": "whsec_A1B2C3...",
"created_at": "2026-04-14T10:20:00+00:00"
}
/webhooks/:id
Detalle de un webhook. La respuesta nunca incluye el secret.
/webhooks/:id
Actualiza name, url, events o active. Re-activar un webhook desactivado resetea el contador de fallos.
/webhooks/:id/rotate-secret
Genera un secret nuevo y devuelve el valor en plano. El anterior deja de verificar inmediatamente — no hay ventana de superposición, así que prepara el nuevo en tu consumidor antes de rotarlo.
/webhooks/:id/test
Dispara un evento sintético webhook.test contra la URL configurada. Útil para verificar conectividad y firma sin tener que esperar a un evento real.
/webhooks/:id/deliveries
Historial paginado de entregas con status code, intento, duración y error. Se conservan 7 días.
/webhooks/:id
Borra el webhook y todas sus entregas archivadas.
Errores
Todas las respuestas de error usan este shape:
{
"error": "short_code",
"message": "Human-readable explanation."
}
| Status | Error code | Cuándo |
|---|---|---|
| 401 | unauthenticated | Sin header, header vacío o token no encontrado. |
| 403 | forbidden | El recurso pertenece a otro usuario. |
| 404 | — | Recurso inexistente. |
| 422 | validation_failed | Body inválido. Incluye errors field-by-field. |
| 422 | campaign_not_ready | Intentaste hacer send antes de que el pipeline completase. |
| 422 | no_recipients | El selector no encontró clientes que encajaran con la estrategia. |
| 422 | no_key_for_followups | Activaste follow-ups pero la clave original se había borrado y no enviaste followup_api_key. |
| 429 | — | Rate limit excedido. Respeta el header Retry-After. |
Rate limits
Los endpoints de lectura tienen un límite de 60 requests por minuto. Los endpoints de escritura (crear campaña, enviar, stop-followups, toggle-conversion) tienen un límite más bajo de 20 por minuto porque cada uno dispara trabajo asíncrono costoso.
Cuando superes un límite recibirás 429 con una cabecera Retry-After indicando cuántos segundos esperar.