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
GET /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"
}
GET /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  }
    ]
}
GET /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.
POST /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"
}
GET /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.

GET /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
}
POST /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": "...",
            ...
        },
        ...
    }
}
POST /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
}
POST /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
}
GET /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, ... } ]
    }
}
GET /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.

POST /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

GET /webhooks

Lista los webhooks del usuario autenticado, paginados de 25 en 25.

POST /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"
}
GET /webhooks/:id

Detalle de un webhook. La respuesta nunca incluye el secret.

PATCH /webhooks/:id

Actualiza name, url, events o active. Re-activar un webhook desactivado resetea el contador de fallos.

POST /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.

POST /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.

GET /webhooks/:id/deliveries

Historial paginado de entregas con status code, intento, duración y error. Se conservan 7 días.

DELETE /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.