Pular para o conteúdo principal

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.

¿Usas Node.js/TypeScript?

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

ÁreaScopes
Instanciasinstances:read, instances:write
Mensajesmessages:send, messages:read
Webhooks de cuentawebhooks:read, webhooks:write
Stevo IAai:read, ai:write
StevoVoicevoice:read, voice:manage
Envíos masivosdispatch:read, dispatch:manage
Billingbilling:read, billing:purchase
GHL / Agencia / Linksghl: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 destructivas

Recreate 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: true cuando tiene sentido repetir la misma llamada (429, 5xx, falla de upstream); false en los demás 4xx — corrige la solicitud antes de repetir.
  • X-Request-Id: toda respuesta trae este header (y el mismo valor en error.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-Limit y X-RateLimit-Remaining. Al excederlo, 429 rate_limited con Retry-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​

RutaMotorQué hace
GET /v1/instances/{id}/connectionambosconnected, logged_in, nombre y número
GET /v1/instances/{id}/healthambossalud unificada (no falla si el servidor cae)
GET /v1/instances/{id}/qrSM v2QR actual (qr en data-URI) y pairing_code
POST /v1/instances/{id}/qr/refreshSM v2fuerza un nuevo QR
POST /v1/instances/{id}/disconnectSM v2desconecta; { "logout": true } + ?confirm=true descarta la sesión (irreversible)
DELETE /v1/instances/{id}?confirm=trueamboselimina 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), con caption/filename opcionales. Solo API Oficial: cloud_api con el payload Cloud API crudo (ej.: plantilla).
  • Respuesta 201 con engine, sent y message_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.

RespuestaSignificadoQué hacer
409 idempotency_conflictmisma clave, cuerpo diferenteusa otra clave
409 idempotency_in_progressla misma clave aún se está procesandoespera y repite
504 upstream_timeoutel servidor de envío no respondió — puede haber enviadoconsulta el estado antes de reenviar
502 upstream_unavailableno alcanzó el servidor de la instancia — no se envió nadapuedes 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.

CampoRegla
urlhttps:// y destino público; los redirects no se siguen
eventslista de tipos, o ["*"] para todos
max_in_flightentregas simultáneas a ese destino: 1 a 64, default 8. Súbelo para endpoints que absorben volumen (ej.: agencia con cientos de instancias)
activefalse 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 2xx en hasta 10 segundos (procesa en cola si es demorado). Sin 2xx, Stevo reentrega: hasta 8 intentos en unas 21h.
  • La entrega es "al menos una vez": una reentrega llega con el mismo event_id y un attempt mayor. Deduplica por event_id.
  • Responder 410 Gone desactiva 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));
}
Usa el cuerpo CRUDO

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​

RutaQué hace
GET /v1/webhooks/deliverieslista 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}/retryreenví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​

  1. Crea una API Key con instances:read, messages:send, messages:read, webhooks:read y webhooks:write.
  2. Crea las instancias con external_ref = id del cliente en tu sistema.
  3. Crea un webhook de cuenta y guarda el secret.
  4. En el endpoint: valida la firma, deduplica por event_id, responde 200 rápido y procesa en cola — usa instance.external_ref para saber de qué cliente es el evento.
  5. Periódicamente (ej.: cada hora), ejecuta la reconciliación con updated_since para capturar cualquier cambio perdido, incluidas las eliminaciones.
  6. Envía con Idempotency-Key; ante un 504 upstream_timeout, consulta el estado por la clave antes de reenviar.
Nunca accedas a la base de datos de Stevo

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​