Implementación del contrato §3.5 del design doc. Rocket es el primer consumidor, no el dueño: cualquier plataforma puede integrarse con este mismo contrato.
Los tenants conectan su cuenta seller de Rocket en self-service (mismo flujo que
LucidBot). Desde Ajustes → Rocket → Conectar:
GET {rocket}/api/oauth/confirma/authorize?client_id&redirect_uri&response_type=code&state
— el seller inicia sesión en Rocket (sus credenciales no pasan por Confirma).code de un solo uso (TTL 5 min).POST {rocket}/api/oauth/confirma/token
→ {access_token, token_public, token_private, integration_id, seller_ref}.POST {rocket}/api/integration/confirma/register-webhook (Bearer) con
{notification_url, events_api_key, events_api_secret, actions_secret}:
Rocket guarda a dónde emitir los eventos de ese seller, con qué firmarlos y
con qué verificar nuestras acciones — y habilita al seller en la suite.Tras esto la integración queda operativa: los eventos del seller llegan firmados
al workspace y las acciones vuelven a {rocket}/api/suite/webhook verificadas
por integración. El modo tenant-zero (credenciales SUITE_WHATSAPP_* en el .env
de Rocket) sigue funcionando como fallback.
HMAC-SHA256 con timestamp y protección anti-replay (tolerancia ±300s).
POST {suite}/api/v1/events
X-Api-Key: api_key del workspace (cf_...)
X-Suite-Timestamp: unix epoch
X-Suite-Signature: hex( HMAC-SHA256( "{timestamp}.{raw_body}", api_secret ) )
Content-Type: application/json
Ejemplo de firma en PHP (lado plataforma):
$body = json_encode($payload);
$ts = time();
$signature = hash_hmac('sha256', $ts.'.'.$body, $apiSecret);
Los eventos-acción llegan a la platform_webhook_url del workspace con:
X-Confirma-Timestamp: unix epoch
X-Confirma-Signature: hex( HMAC-SHA256( "{timestamp}.{raw_body}", platform_webhook_secret ) )
Retries automáticos si la plataforma no responde 2xx: 1m / 5m / 30m / 2h / 12h
(después queda exhausted y visible en el panel). Un 200 con ok: 0 es un rechazo
de negocio: NO se reintenta y el motivo (message) se traslada al cliente final.
Respuesta esperada de la plataforma:
{"ok": 1, "applied": true}
// o
{"ok": 0, "code": 409, "message": "Pedido ya preparado, no editable"}
order.created · order.pending_confirmation · order.confirmed · order.updated ·
order.fulfilled · order.on_delivery · order.agency_pickup · order.incidence ·
incidence.managed · order.delivered · order.returned · order.rejected ·
cart.abandoned · cart.converted
{
"event_id": "rocket-osr-9912345",
"event": "order.pending_confirmation",
"occurred_at": "2026-07-16T14:03:22-05:00",
"seller_ref": "user-4821",
"order": {
"external_id": "784512",
"external_ref": "#SH-1042",
"status": "pending_confirmation",
"customer": {"name": "María P.", "phone": "+593991234567"},
"shipping": {"address": "...", "city": "Quito", "province": "Pichincha", "country": "EC", "reference": "..."},
"totals": {"total": 42.50, "currency": "USD", "cod": true},
"lines": [{"name": "Smartwatch X", "sku": "SW-01", "qty": 1, "price": 42.50}],
"tracking": {"code": "SRV123", "url": "https://...", "courier": "Servientrega"},
"incidence": {"type": "ausente", "message": "...", "manageable": true},
"consent": {"source": "checkout", "at": "2026-07-16T13:59:01-05:00"}
}
}
Notas:
event_id es la clave de idempotencia: reenviar el mismo id devuelve 200 sin reprocesar.seller_ref crea el perfil de seller automáticamente si no existe (reglas por defecto §2.1).phone se normaliza a E.164 (acepta formatos locales EC/CL/CO/PE si llega shipping.country).order.confirmed (confirmado por otra vía) detiene la secuencia de confirmación en curso.consent es la evidencia de opt-in del checkout (§6.3) — se persiste por si Meta audita.order.incidence arranca el árbol conversacional del tipo de novedad
(incidence.type: ausente · general · direccion_incorrecta · no_acepta ·
desconocido · entrega_aplazada · pendiente_recoger) — secuencias con
trigger_filter por tipo.incidence.managed cierra el loop §2.5: detiene los recordatorios de novedad
pendientes y le confirma al cliente que su entrega quedó coordinada (texto si la
ventana 24h sigue abierta; plantilla novedad_gestionada si no). Si viaja
incidence.resolution_summary, ese resumen se le traslada literal al cliente.Acción (la plataforma debe ejecutar y responder el resultado real):
| Evento | data | Semántica en la plataforma |
|---|---|---|
order.confirm |
{mode: "auto"|"flag"} |
auto: ejecutar la confirmación (en Rocket Order::do_confirm). flag: solo marcar «cliente confirmó» para revisión |
order.cancel |
{reason} |
Cancelar/rechazar el pedido. Motivos: ya_no_lo_quiere, pedido_por_error, precio, no_response, sin_motivo |
order.update |
{fields} |
Actualizar datos del pedido (v1) |
order.update_address |
{address, city, province, reference} |
(v1, novedades) |
order.reschedule |
{date, slot} |
Reprogramar entrega. date en YYYY-MM-DD, slot manana|tarde. Lo emiten los botones 1-tap de novedades y el flujo "otra fecha" con eco-confirmación |
cart.recover |
{address?} |
Convertir el carrito en pedido. address es la dirección eco-confirmada por el cliente (en Rocket: el carrito pasa a PENDIENTE DE CONFIRMACIÓN y entra al módulo 2.1) |
order.add_line |
{sku, qty, price, name} |
Upsell §2.7 aceptado tras confirmar: añadir la línea al pedido ANTES del despacho y sumar price × qty al total COD (en Rocket: solo estados editables y pago contraentrega) |
order.reorder |
{address?} |
Winback §2.6: clonar el pedido ENTREGADO (cliente + líneas a costes actuales) como pedido nuevo en pendiente de confirmación. Idempotente por referencia RW-{original} |
order.create |
{customer{...}, products[{sku,quantity,price}], total, shipping_method_id?, city_id?, external_ref?} |
Crear un pedido desde la conversación (checkout/bot de ventas). Mismo flujo de validación que la creación LucidBot; nace en pendiente de confirmación. Idempotente por external_ref |
order.apply_discount |
{pct, reason} |
Fase 21: el cliente aceptó el descuento de rescate que iba a perderse por precio. pct lo fija el admin del tenant (tope 20%), nunca la IA. Aplicar el descuento al total COD antes del despacho |
payment.confirmed |
{amount, currency, provider, external_ref, discount_pct} |
Fase 21: el pedido se pagó por adelantado (COD→prepago). Marcar el pedido como PAGADO y despachar sin recaudo en destino — si se despacha con cobro, el cliente pagaría dos veces |
Información (la plataforma solo registra):
conversation.no_response · conversation.escalated {reason} · feedback.received {nps: 1-5, comment} (encuesta NPS post-entrega §2.6) · otp.verified (v2) ·
lead.captured {name, phone, email?, message?, page_url?, campaign?, source} (fase 24: alguien dejó su WhatsApp en el widget web de la tienda; la conversación ya siguió por WhatsApp desde la suite) ·
sweep.<slug> {sweep} (fase 28: un barrido configurado por el tenant con acción webhook casó con este pedido; <slug> es el nombre de evento que el tenant eligió al crear el barrido — ^[a-z0-9_]{1,40}$ — y sweep el nombre del barrido. Mismo sobre HMAC y retries que el resto)
Envelope de todos los salientes:
{
"event_id": "confirma-uuid",
"event": "order.confirm",
"occurred_at": "...",
"seller_ref": "user-4821",
"order": {"external_id": "784512", "external_ref": "#SH-1042"},
"data": {"mode": "auto"}
}
Misma autenticación HMAC. Rate limits: 3/min por teléfono en send, 10/min en verify, 60/min por tenant. TTL 10 minutos, máximo 3 intentos por código.
POST /api/v1/otp/send {"phone": "0991234567", "country": "EC", "brand_name": "MiTienda"}
→ {"ok":1, "content": {"phone_normalized": "+593991234567", "ttl_minutes": 10}}
POST /api/v1/otp/verify {"phone": "0991234567", "country": "EC", "code": "123456"}
→ {"ok":1, "content": {"verified": true, "phone_normalized": "+593991234567"}}
→ 422 {"ok":0, "content": {"verified": false, "attempts_left": 2}}
Misma autenticación HMAC. Devuelve a la pauta (Meta CAPI / TikTok Events) los hechos
de negocio que ocurren fuera del navegador. Idempotente por (tipo, pedido): el
mismo POST repetido no duplica la conversión. El teléfono viaja siempre hasheado
(SHA-256) y cada evento lleva su event_id, así que si el pixel del navegador envió
lo mismo, la plataforma lo deduplica.
GET /api/v1/conversion-types → tipos del tenant y su estado
POST /api/v1/conversions {"key": "pedido_entregado", "external_order_id": "784512"}
→ {"ok":1, "content": {"recorded": true, "id": 12, "status": "pending"}}
→ {"ok":1, "content": {"recorded": false, "reason": "tipo inexistente, inactivo o sin destinos"}}
GET /api/v1/conversions?status=sent&key=pedido_confirmado
Tipos de fábrica: pedido_confirmado · pedido_entregado · pedido_prepagado
(nacen apagados hasta que el tenant conecta un pixel en el panel).
Misma autenticación HMAC. La llama la plataforma madre o un puente desde el PSP (webhook de Stripe/MercadoPago/Wompi → este endpoint). Idempotente: el reintento del proveedor no vuelve a confirmar ni re-emite el webhook.
POST /api/v1/payment-webhook {"link_id": 42, "status": "paid", "external_ref": "pi_3Nx..."}
→ {"ok":1, "applied":1} // applied:0 = ya estaba pagado (reintento)
Al aplicarse: el pedido queda confirmado, se paran los recordatorios, el espejo
deja de ser COD y sale payment.confirmed hacia la plataforma.
Autenticación distinta: token con scopes, no la firma HMAC (esa es la vía de la plataforma madre; esta es la del equipo técnico del tenant y de su agente).
Authorization: Bearer cft_xxxxxxxx
Los tokens se crean en el panel (Desarrolladores) y se muestran una sola vez.
Scopes: orders:read · orders:write · messages:write · metrics:read · conversions:write.
POST /api/v1/orders {"external_id","phone","customer_name?","total?","currency?","city?","country?","campaign?","lines?"}
→ crea el pedido y arranca su confirmación. Idempotente por external_id.
POST /api/v1/orders/{externalId}/pause {"reason?"} → para secuencias + apaga el bot
POST /api/v1/orders/{externalId}/resume → reanuda
POST /api/v1/orders/{externalId}/first-message {"template?"} → fuerza el primer contacto
Un reintento no es un error, y se distingue del efecto real. Las tres acciones son idempotentes y lo declaran en la respuesta, para que un cliente que reintenta tras un timeout de red sepa qué pasó de verdad:
| Acción | Campo | Significado |
|---|---|---|
pause |
already_paused: true |
ya estaba pausado: no se paró ninguna secuencia ni se apagó ningún bot |
resume |
already_running: true |
ya estaba activo: nada que liberar ni re-arrancar |
resume |
held_by_human: N |
conversaciones que siguen en manos del equipo — la API no se las quita |
first-message |
already_sent: true + message_id |
el dedup bloqueó el reenvío; devuelve el mensaje original y sent: false |
La pausa es del pedido, no solo de la conversación. Pausar un pedido que aún no tiene conversación (el cliente todavía no ha escrito) deja la marca en el pedido, y la conversación que nazca después hereda la pausa. Sin esto, la pausa era un no-op silencioso y el bot contestaba al primer mensaje entrante.
get_order devuelve el estado explícito en tres partes independientes:
"automation": {
"bot_paused": true, "paused_by": "api",
"paused_at": "2026-07-30T02:00:00+00:00", "sequence_running": false
}
paused_by es api o human: la API nunca reanuda el bot encima de un
agente humano que tomó el control desde el inbox.
Servidor MCP en POST /api/mcp (JSON-RPC 2.0: initialize, tools/list,
tools/call, ping). Las tools visibles dependen del scope del token:
get_order · get_metrics · create_order · pause_automation ·
resume_automation · force_first_message · record_conversion.
claude mcp add confirma --transport http https://TU-DOMINIO/api/mcp \
--header "Authorization: Bearer cft_xxxxxxxx"
Misma autenticación HMAC (la firma cubre {timestamp}. con body vacío en GET):
GET /api/v1/orders?status=&result=&per_page= → pedidos gestionados (paginado)
GET /api/v1/orders/{external_id} → detalle + conversaciones
GET /api/v1/metrics?days=30 → rollups del período (listos para BI)
GET /api/webhooks/whatsapp → verificación hub.challenge (verify token por conexión)
POST /api/webhooks/whatsapp → mensajes + estados de entrega + quality rating (200 inmediato, proceso en cola)
Campos a suscribir en la app de Meta: messages, phone_number_quality_update.
LucidBot.php (SuiteWhatsapp) + job NotifyOrderSuite
enganchado en OrderStatusRecord::create() con el mapa estado→evento del Apéndice A,
más el hook Eloquent Order::created para order.created./api/suite/orders/{confirm,cancel,update} espejo del set de
LucidBot (routes/api.php:209-230 de Rocket), ejecutando Order::do_confirm() /
do_reject() y devolviendo {ok, message} con el motivo real si falla.goverify_users).