API de Gestión — guía de integración
La API de Gestión (https://openapi.stevo.chat) es la puerta de entrada oficial para integrar sistemas con Stevo: instancias, envío de mensajes, webhooks, Stevo IA, envíos masivos, StevoVoice, GHL y billing. Esta guía muestra cómo usarla — la lista completa de endpoints, con playground, está en la Referencia de la API de Gestión.
El SDK stevo-sdk hace todo esto por ti — retry seguro, validación de webhook y tipos. Esta guía es para quien integra en otro lenguaje (PHP, Python, Go, n8n, Make...) o quiere entender qué pasa por debajo.
1. Autenticación
Crea una API Key en el panel (menú del perfil → API Keys) y envíala en cada request:
curl https://openapi.stevo.chat/v1/me \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE'
GET /v1/me muestra la cuenta, los scopes y la restricción de la key — es el mejor endpoint para probar la configuración.
Scopes
Cada key solo hace lo que permiten sus scopes (403 insufficient_scope en caso contrario):
| Área | Scopes |
|---|---|
| Instancias | instances:read, instances:write |
| Mensajes | messages:send, messages:read |
| Webhooks de cuenta | webhooks:read, webhooks:write |
| Stevo IA | ai:read, ai:write |
| StevoVoice | voice:read, voice:manage |
| Envíos masivos | dispatch:read, dispatch:manage |
| Billing | billing:read, billing:purchase |
| GHL / Agencia / Links | ghl:manage, agency:read, agency:manage, links:generate |
Las keys antiguas con instances:manage y ai:manage siguen funcionando (son sinónimos de :write).
instances:write incluye operaciones destructivasRecreate total, logout y eliminación de instancia usan el mismo scope. No existe un scope separado para ellas — da instances:write solo a quien lo necesite.
Key restringida a instancias
Una key puede estar limitada a algunas instancias (allowed_instance_ids en /v1/me; null = todas). Fuera de la lista, la API responde 404 not_found, exactamente igual que para una instancia de otra cuenta. Las operaciones de cuenta (crear instancia, webhooks de cuenta) responden 403 key_restricted / 403 instance_restricted_key. Usa una key restringida por cliente siempre que la integración atienda a un solo cliente.
2. Errores, correlación y rate limit
Todo error tiene el mismo formato:
{ "error": { "code": "not_ready", "message": "instancia no conectada", "request_id": "req_3f9c...", "retryable": false } }
retryable:truecuando tiene sentido repetir la misma llamada (429,5xx, falla de upstream);falseen los demás4xx— corrige la solicitud antes de repetir.X-Request-Id: toda respuesta trae este header (y el mismo valor enerror.request_id). Puedes enviar el tuyo (hasta 128 caracteres[A-Za-z0-9._:-]) y se devuelve igual. Infórmalo a soporte al reportar un problema.- Rate limit por key: toda respuesta trae
X-RateLimit-LimityX-RateLimit-Remaining. Al excederlo,429 rate_limitedconRetry-After(segundos).
3. Instancias
3.1. Vincula la instancia a tu sistema: external_ref y metadata
# Crear en un cupo libre, ya con la referencia de tu cliente
curl https://openapi.stevo.chat/v1/instances \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE' \
--header 'Content-Type: application/json' \
--data '{ "name": "tienda-centro", "external_ref": "WSP-000123", "metadata": { "plan": "pro" } }'
# Encontrar por tu referencia
curl --get https://openapi.stevo.chat/v1/instances \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE' \
--data-urlencode 'external_ref=WSP-000123'
# Actualizar (metadata REEMPLAZA el objeto entero; external_ref: null lo limpia)
curl -X PATCH https://openapi.stevo.chat/v1/instances/UUID_INSTANCIA \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE' \
--header 'Content-Type: application/json' \
--data '{ "metadata": { "plan": "enterprise" } }'
external_ref: hasta 128 caracteres, único por cuenta (409 external_ref_conflict). metadata: hasta 16 pares texto→texto. Ambos viajan en cada evento de los webhooks de cuenta.
3.2. Reconciliación — solo lo que cambió
GET /v1/instances con updated_since se convierte en una consulta incremental, incluidas las instancias eliminadas:
curl --get https://openapi.stevo.chat/v1/instances \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE' \
--data-urlencode 'updated_since=2026-09-22T00:00:00Z' \
--data-urlencode 'limit=100'
{
"data": [ { "id": "...", "external_ref": "WSP-000123", "connected": true, "updated_at": "..." } ],
"deleted": [ { "id": "...", "external_ref": "WSP-000099", "metadata": null, "deleted_at": "..." } ],
"next_cursor": "eyJ1...",
"has_more": true,
"deleted_has_more": false
}
Sigue next_cursor (envíalo de vuelta en cursor) mientras has_more sea true. Si deleted_has_more viene true, repite la llamada con updated_since = último deleted_at recibido. Sin updated_since, la respuesta sigue siendo la de siempre ({ data, count }).
3.3. Conexión, QR y eliminación
| Ruta | Motor | Qué hace |
|---|---|---|
GET /v1/instances/{id}/connection | ambos | connected, logged_in, nombre y número |
GET /v1/instances/{id}/health | ambos | salud unificada (no falla si el servidor cae) |
GET /v1/instances/{id}/qr | SM v2 | QR actual (qr en data-URI) y pairing_code |
POST /v1/instances/{id}/qr/refresh | SM v2 | fuerza un nuevo QR |
POST /v1/instances/{id}/disconnect | SM v2 | desconecta; { "logout": true } + ?confirm=true descarta la sesión (irreversible) |
DELETE /v1/instances/{id}?confirm=true | ambos | elimina la instancia y libera el cupo (irreversible, idempotente) |
Sin ?confirm=true en las operaciones irreversibles, la API responde 400 confirmation_required y no ejecuta nada. La instancia eliminada aparece en deleted en la reconciliación y genera el evento instance.deleted.
4. Envío de mensajes
POST /v1/instances/{id}/messages (scope messages:send) envía un mensaje, en SM v2 o API Oficial, sin que tengas que lidiar con el servidor de la instancia:
curl https://openapi.stevo.chat/v1/instances/UUID_INSTANCIA/messages \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: pedido-9f21' \
--data '{ "to": "5511999999999", "text": "¡Hola!" }'
- Texto:
text. Medios:media_url+media_type(image,video,audio,document), concaption/filenameopcionales. Solo API Oficial:cloud_apicon el payload Cloud API crudo (ej.: plantilla). - Respuesta
201conengine,sentymessage_id. Instancia desconectada →409 not_ready. - Para volumen, prefiere los envíos masivos o el servidor de la instancia directo.
4.1. Idempotency-Key — nunca duplicar
Envía Idempotency-Key (1–200 caracteres, único por instancia, válido por 24h). Repetir la misma solicitud no reenvía: la API devuelve la respuesta original con el header Idempotent-Replayed: true.
| Respuesta | Significado | Qué hacer |
|---|---|---|
409 idempotency_conflict | misma clave, cuerpo diferente | usa otra clave |
409 idempotency_in_progress | la misma clave aún se está procesando | espera y repite |
504 upstream_timeout | el servidor de envío no respondió — puede haber enviado | consulta el estado antes de reenviar |
502 upstream_unavailable | no alcanzó el servidor de la instancia — no se envió nada | puedes repetir |
4.2. Estado del mensaje
# Por el id del mensaje
curl https://openapi.stevo.chat/v1/instances/UUID_INSTANCIA/messages/ID_DEL_MENSAJE \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE'
# Por la clave de idempotencia — la consulta correcta después de un timeout
curl --get https://openapi.stevo.chat/v1/instances/UUID_INSTANCIA/messages \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE' \
--data-urlencode 'idempotency_key=pedido-9f21'
Estados: queued → sent → delivered → read, o failed (scope messages:read). Las transiciones nunca retroceden y coinciden con los eventos message.* de los webhooks. Tras un upstream_timeout, el estado queda en queued — y evoluciona solo a sent/delivered si el mensaje realmente salió.
5. Webhooks de cuenta
Un webhook de cuenta recibe los eventos de todas las instancias en un sobre único, normalizado entre SM v2 y API Oficial, firmado y con entrega confiable. Es lo recomendado para integraciones nuevas (el webhook por instancia, /v1/instances/{id}/webhook, sigue existiendo, sin firma).
5.1. Crear
curl https://openapi.stevo.chat/v1/webhooks \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE' \
--header 'Content-Type: application/json' \
--data '{
"url": "https://mi-sistema.com/webhooks/stevo",
"events": ["message.received", "message.failed", "instance.connected", "instance.disconnected"],
"description": "Mi CRM",
"max_in_flight": 16
}'
La respuesta trae el secret (whsec_...) — solo en esta respuesta; después, solo secret_last4. Guárdalo en un gestor de secretos/variable de entorno.
| Campo | Regla |
|---|---|
url | https:// y destino público; los redirects no se siguen |
events | lista de tipos, o ["*"] para todos |
max_in_flight | entregas simultáneas a ese destino: 1 a 64, default 8. Súbelo para endpoints que absorben volumen (ej.: agencia con cientos de instancias) |
active | false lo pausa; la API también lo desactiva sola tras un 410 Gone o 5 entregas agotadas seguidas (disabled_reason: gone / too_many_failures) |
Gestiónalo con GET/PATCH/DELETE /v1/webhooks/{id}. Límite de 10 webhooks por cuenta. Requiere una key sin restricción de instancias.
Eventos: instance.created, instance.updated, instance.connecting, instance.connected, instance.disconnected, instance.auth_failed, instance.qr.updated, instance.deleted, message.received, message.sent, message.delivered, message.read, message.failed, message.edited, message.deleted, message.reaction.
5.2. Qué llega a tu endpoint
POST /webhooks/stevo HTTP/1.1
Content-Type: application/json
X-Stevo-Event-Id: evt_01J...
X-Stevo-Timestamp: 1790190000
X-Stevo-Attempt: 1
X-Stevo-Signature: sha256=5b1c...
{
"event_id": "evt_01J...",
"type": "message.received",
"attempt": 1,
"created_at": "2026-09-22T14:03:11.000Z",
"account_id": "UUID_DE_LA_CUENTA",
"instance": { "id": "UUID_INSTANCIA", "external_ref": "WSP-000123", "metadata": { "plan": "pro" }, "engine": "smv2" },
"data": { "message_id": "3EB0...", "chat": "5511999999999", "from": "5511999999999", "from_me": false, "is_group": false, "type": "text", "text": "¡Hola!" }
}
- Responde
2xxen hasta 10 segundos (procesa en cola si es demorado). Sin2xx, Stevo reentrega: hasta 8 intentos en unas 21h. - La entrega es "al menos una vez": una reentrega llega con el mismo
event_idy unattemptmayor. Deduplica porevent_id. - Responder
410 Gonedesactiva el webhook.
5.3. Validar la firma
X-Stevo-Signature = sha256= + HMAC-SHA256(secret, timestamp + "." + cuerpoCrudo) en hexadecimal, donde secret es el valor completo whsec_... y timestamp es el X-Stevo-Timestamp. Rechaza también timestamps con más de 5 minutos de diferencia (protección contra replay).
import hmac, hashlib, time
def firma_valida(cuerpo_crudo: bytes, firma: str, timestamp: str, secreto: str) -> bool:
if abs(time.time() - int(timestamp)) > 300:
return False
esperado = hmac.new(secreto.encode(), f"{timestamp}.".encode() + cuerpo_crudo, hashlib.sha256).hexdigest()
return hmac.compare_digest(firma.removeprefix("sha256="), esperado)
function firma_valida(string $cuerpoCrudo, string $firma, string $timestamp, string $secreto): bool {
if (abs(time() - (int) $timestamp) > 300) return false;
$esperado = hash_hmac('sha256', $timestamp . '.' . $cuerpoCrudo, $secreto);
return hash_equals($esperado, preg_replace('/^sha256=/', '', $firma));
}
Calcula el HMAC sobre los bytes exactos recibidos, antes de cualquier parseo de JSON. Volver a serializar el JSON cambia el cuerpo y la firma no coincide.
5.4. Rotación del secreto
POST /v1/webhooks/{id}/rotate-secret genera un secreto nuevo (devuelto una sola vez). Durante la ventana overlap_seconds (0–86400, default 86400 = 24h), cada entrega lleva también X-Stevo-Signature-Previous, firmada con el secreto anterior — acepta cualquiera de las dos mientras cambias la configuración. overlap_seconds: 0 es la rotación de emergencia (el anterior deja de valer al instante).
5.5. Historial y reenvío de entregas
| Ruta | Qué hace |
|---|---|
GET /v1/webhooks/deliveries | lista entregas; filtros webhook_id, instance_id, status (pending, delivered, failed, exhausted), event_type, since, cursor, limit |
GET /v1/webhooks/deliveries/{id} | detalle con el log de cada intento |
POST /v1/webhooks/deliveries/{id}/retry | reenvía ahora, con el mismo event_id (hasta 5 reenvíos manuales) |
¿Tu endpoint estuvo caído? Lista las entregas exhausted desde el incidente y reenvíalas — la deduplicación por event_id garantiza que nada se procese dos veces.
6. Receta: sincronizar un sistema externo con Stevo
- Crea una API Key con
instances:read,messages:send,messages:read,webhooks:readywebhooks:write. - Crea las instancias con
external_ref= id del cliente en tu sistema. - Crea un webhook de cuenta y guarda el
secret. - En el endpoint: valida la firma, deduplica por
event_id, responde200rápido y procesa en cola — usainstance.external_refpara saber de qué cliente es el evento. - Periódicamente (ej.: cada hora), ejecuta la reconciliación con
updated_sincepara capturar cualquier cambio perdido, incluidas las eliminaciones. - Envía con
Idempotency-Key; ante un504 upstream_timeout, consulta el estado por la clave antes de reenviar.
Las integraciones externas usan solo la API (o el SDK). Supabase/Postgres, service_role y las tablas internas no forman parte del contrato y pueden cambiar sin aviso.
7. MCP — la misma API para agentes de IA
El mismo backend expone un servidor MCP en https://openapi.stevo.chat/mcp (Streamable HTTP), con las mismas operaciones como tools y autenticado con la misma API key. Conéctalo a n8n (nodo MCP Client), Claude o Cursor.
Referencias
- 📖 Referencia de la API de Gestión — todos los endpoints, schemas y playground
- 📦 SDK
stevo-sdk(Node.js) - 📄 Especificación OpenAPI:
https://openapi.stevo.chat/openapi.yaml