Confirma · Documentación de la API Ir al panel

Contrato de API — Plataforma ↔ Confirma

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.

Onboarding OAuth 2.0 con Rocket (fase 9)

Los tenants conectan su cuenta seller de Rocket en self-service (mismo flujo que LucidBot). Desde Ajustes → Rocket → Conectar:

  1. Redirección a 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).
  2. Rocket redirige al callback con un code de un solo uso (TTL 5 min).
  3. Confirma canjea el code en POST {rocket}/api/oauth/confirma/token{access_token, token_public, token_private, integration_id, seller_ref}.
  4. 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.

Autenticación (ambos sentidos)

HMAC-SHA256 con timestamp y protección anti-replay (tolerancia ±300s).

Plataforma → Confirma

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

Confirma → Plataforma

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"}

Eventos plataforma → suite

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:

Eventos suite → plataforma

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"}
}

API OTP (§2.2)

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}}

API de conversiones (fase 23)

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).

API de confirmación de pago (fase 21)

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.

API de ESCRITURA y servidor MCP (fase 25)

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"

API de lectura (fase 13)

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)

Webhook de Meta (Cloud API)

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.

Integración de referencia en Rocket (§4.1 del diseño)