SDK oficial — stevo-sdk
stevo-sdk es el SDK oficial (TypeScript/JavaScript) de Stevo. Un solo paquete con todo lo que ofrece la plataforma, bien separado por área:
| Área | Se accede con | Qué hace |
|---|---|---|
| Gestión de cuenta | stevo.me, stevo.instances, stevo.links, stevo.ghl, stevo.billing, stevo.ghlAgency | instancias (crear, conectar, reconciliar, eliminar), links de acceso, GHL, compras y GHL Agencia |
| Webhooks de cuenta | stevo.webhooks | un endpoint recibe los eventos de todas las instancias, firmados con HMAC-SHA256, con historial y reenvío |
| Envío de mensajes | stevo.whatsapp(id) · stevo.messages · stevo.smv2(id) · stevo.oficial(id) | capa unificada, proxy con idempotencia y estado, o el servidor de la instancia directo (114 operaciones SM v2 / Cloud API de Meta) |
| Stevo IA (v2) | stevo.ai | agentes, embudo, claves de proveedor, FAQ, tools, follow-up, RAG, memoria |
| Envío masivo | stevo.dispatch | campañas de WhatsApp con rotación de instancias, programación y variaciones |
| StevoVoice | stevo.voice | llamadas de voz con IA, grabaciones, activación y widget |
📦 npm: npm.im/stevo-sdk · versión actual 2.1.0 · Node.js 18+ · ESM + CommonJS + tipos incluidos · cero dependencias de runtime
- Webhooks de cuenta firmados, con entrega confiable, deduplicación por
event_id, historial y reenvío (stevo.webhooks) — ymax_in_flightpara controlar el caudal (2.1.0). - Envío seguro:
Idempotency-Keyen el envío y estado del mensaje (queued→sent→delivered→read/failed). - Instancias vinculadas a tu sistema:
external_refymetadata, reconciliación conlistChangesy eliminación condelete. - Ciclo de vida completo por la API: QR, conexión, desconexión y salud.
- Capa unificada
stevo.whatsapp(id), retry configurable y observabilidad (X-Request-Id,onResponse).
¿Vienes de la 1.x? Mira Actualizando de la 1.x a la 2.x — la migración es pequeña.
La misma API tiene un servidor MCP en https://openapi.stevo.chat/mcp. Conéctalo a n8n (nodo MCP Client), Claude o Cursor con tu API key y la IA gestiona la cuenta, envía mensajes, crea campañas y programa llamadas sin una línea de código.
Todo lo que hace el SDK pasa por la API de Gestión (REST). Para usarla directo por HTTP, en cualquier lenguaje, mira API de Gestión — guía de integración.
1. Crea tu API Key
En el panel de Stevo: menú del perfil → API Keys → Nueva API Key. Marca los scopes (permisos) que tendrá esa key — desde instances:read (solo lectura) hasta envío, webhooks, envío masivo, voz, IA y billing. La key aparece una sola vez — guárdala de forma segura y nunca la expongas en el navegador/front-end.
Una key también puede estar restringida a algunas instancias: fuera de la lista, la API responde 404 como si la instancia no existiera. Prefiere una key restringida cuando la integración habla con un solo cliente.
2. Instala y conecta
npm install stevo-sdk
import { Stevo } from 'stevo-sdk';
const stevo = new Stevo(process.env.STEVO_API_KEY!); // stevo_sk_...
const instancias = await stevo.instances.list();
console.log(`${instancias.filter((i) => i.connected).length} conectadas`);
const yo = await stevo.me.get(); // cuenta, scopes y restricción de la key (allowed_instance_ids)
El constructor valida la key al instante: vacía o sin el prefijo stevo_sk_ lanza Error inmediatamente.
Opciones del constructor
| Opción | Default | Observación |
|---|---|---|
baseUrl | https://openapi.stevo.chat | — |
timeoutMs | 60000 | timeout por solicitud |
retry | ver Retry | reglas de repetición automática |
maxRetries | 2 | legado — repeticiones extra (maxRetries: 2 = maxAttempts: 3) |
onResponse | — | hook llamado en cada intento (métricas/logs) |
credentialsCacheMs | 0 | caché de las credenciales que resuelven smv2(id)/oficial(id); se invalida sola en 401, restart, recreateTotal y delete |
3. Gestión de cuenta
// Crear instancia en un cupo libre del plan (no compra cupo nuevo)
const { instance } = await stevo.instances.create({ name: 'mi-instancia' });
const oficial = await stevo.instances.create({ engine: 'official' }); // devuelve onboarding_url
await stevo.instances.restart(id); // reconectar (leer el QR después)
await stevo.instances.recreateTotal(id); // DESTRUCTIVO: lo borra todo
await stevo.instances.recreateTotalBatch([id1, id2, id3]); // en lote (hasta 50)
// Fusión: une la instancia API Oficial con la SM v2 del MISMO número — STChat, transmisor e IA
// tratan ambas como un solo contacto (el "Fusionar instancias" del panel)
await stevo.instances.fuse(idOficial, idSmv2); // 400 phone_mismatch/engine_mismatch, 409 already_fused
const fusion = await stevo.instances.getFusion(idOficial); // { instance, fused, partner }
await stevo.instances.unfuse(idOficial); // limpia ambos lados
// Links, GHL y compras
const link = await stevo.links.whiteLabel(id, { permanent: true });
await stevo.ghl.connect(id, { mode: 'oauth' });
const catalogo = await stevo.billing.plans();
if (catalogo.has_saved_card) await stevo.billing.purchase({ plan: 'stevo3' }); // cobra la tarjeta guardada
// Sin tarjeta guardada (o para enviar el link al cliente final): Stripe Checkout alojado.
// No cobra al instante — devuelve checkout_url; tras el pago, el aprovisionamiento es automático.
const { checkout_url } = await stevo.billing.checkout({ plan: 'stevo3' });
recreateTotal, recreateTotalBatch, instances.delete, disconnect con logout: true y ai.conversations.clear no se pueden deshacer. Pide confirmación explícita en cualquier pantalla o automatización que las exponga.
3.1. external_ref y metadata — vincula la instancia a tu sistema
Usa external_ref (referencia opaca, única por cuenta, hasta 128 caracteres) y metadata (pares texto→texto, hasta 16 claves) para vincular la instancia al cliente, tienda o workspace de tu sistema — sin guardar el UUID de Stevo:
const { instance } = await stevo.instances.create({
name: 'tienda-centro',
external_ref: 'WSP-000123',
metadata: { workspace: 'WSP-000123', plan: 'pro' },
});
await stevo.instances.update(instance.id, { metadata: { plan: 'enterprise' } }); // REEMPLAZA el objeto entero
await stevo.instances.update(instance.id, { external_ref: null }); // limpia la referencia
const mia = await stevo.instances.findByExternalRef('WSP-000123'); // Instance | null
const delTenant = await stevo.instances.list({ external_ref: 'WSP-000123' });
metadataenupdatereemplaza el objeto (no hace merge) — lee el actual antes de cambiar un solo campo.external_refrepetido en la cuenta →409 external_ref_conflict.external_refymetadatatambién llegan en cada evento de webhook de cuenta (instance.external_ref), así tu handler sabe de qué cliente es el evento sin consultar nada.
3.2. Reconciliación — listChanges / iterateChanges
Para mantener tu base de datos sincronizada sin releer todo, pide solo lo que cambió desde la última vez — incluidas las instancias eliminadas:
const desde = await miBase.ultimaSincronizacion(); // Date
for await (const pagina of stevo.instances.iterateChanges(desde)) {
for (const instancia of pagina.data) await miBase.upsertInstancia(instancia);
for (const eliminada of pagina.deleted) await miBase.eliminarInstancia(eliminada.id); // { id, external_ref, metadata, deleted_at }
}
await miBase.guardarSincronizacion(new Date());
listChanges({ updated_since, cursor?, limit? })devuelve una página:{ data, deleted, next_cursor, has_more, deleted_has_more }.iterateChangessigue el cursor solo.- Deduplica
deletedporid+deleted_at. Si el mismoidreaparece endataconupdated_atposterior aldeleted_at, el cupo fue reutilizado por una nueva instancia.
3.3. Eliminar una instancia — delete
await stevo.instances.delete(id); // lanza Error ANTES de llamar a la API: falta { confirm: true }
await stevo.instances.delete(id, { confirm: true }); // { deleted: true, deleted_at }
Libera el cupo, limpia external_ref/metadata y registra la eliminación (aparece en listChanges y dispara el evento instance.deleted). Es idempotente: eliminar un cupo ya vacío devuelve { deleted: false }.
4. Conectar una instancia: crear → QR → conectada
getConnection y health funcionan en ambos motores; el QR y disconnect son de SM v2 (la API Oficial se conecta por el onboarding_url y responde 400 unsupported_engine a esas dos):
const { instance } = await stevo.instances.create({ name: 'atencion', external_ref: 'WSP-000123' });
// Configura el webhook ANTES de escanear, para no perder el instance.connected
await stevo.webhooks.create({
url: 'https://mi-sistema.com/webhooks/stevo',
events: ['instance.connected', 'instance.disconnected', 'instance.qr.updated', 'message.received'],
});
// QR — el servidor puede tardar unos segundos en generarlo
let qr = await stevo.instances.getQrCode(instance.id);
while (!qr.qr) {
await new Promise((r) => setTimeout(r, 2_000));
qr = await stevo.instances.getQrCode(instance.id);
}
console.log(qr.qr); // data:image/png;base64,... — renderízalo en un <img>, o muestra qr.pairing_code
// Confirma por la conexión (o por el evento instance.connected en tu webhook)
const conexion = await stevo.instances.getConnection(instance.id); // { connected, logged_in, name?, phone_number? }
await stevo.instances.refreshQrCode(instance.id); // ¿expiró el QR? genera otro (restaura el webhook guardado)
await stevo.instances.health(instance.id); // no lanza si el servidor cae: server_reachable: false
await stevo.instances.disconnect(instance.id); // desconecta manteniendo la sesión
await stevo.instances.disconnect(instance.id, { logout: true, confirm: true }); // IRREVERSIBLE: descarta la sesión
Webhook de la instancia × webhook de cuenta
stevo.instances.setWebhook(id, ...)es el webhook de una instancia — el mismo de Configuración → Webhook en el panel, en el formato nativo del motor y sin firma. Sigue funcionando:
await stevo.instances.setWebhook(id, { url: 'https://mi-sistema.com/webhook/stevo', events: ['MESSAGE', 'CONNECTION'] }); // SM v2
await stevo.instances.setWebhook(idOficial, { url: 'https://mi-sistema.com/webhook/oficial' }); // API Oficial (todos los eventos)
const webhook = await stevo.instances.getWebhook(id); // { engine, url, events }
await stevo.instances.deleteWebhook(id);
stevo.webhookses el webhook de cuenta — recomendado para integraciones nuevas: un endpoint recibe los eventos de todas las instancias en un sobre único y normalizado, firmado, con reentrega e historial. Mira la siguiente sección.
5. Webhooks de cuenta
5.1. Crear y gestionar
const creado = await stevo.webhooks.create({
url: 'https://mi-sistema.com/webhooks/stevo', // https:// obligatorio, destino público
events: ['message.received', 'message.failed', 'instance.connected', 'instance.disconnected'], // o ['*']
description: 'Mi CRM',
max_in_flight: 16, // entregas simultáneas: 1 a 64, default 8
});
console.log(creado.secret); // whsec_... — guárdalo AHORA: la API no vuelve a devolver el valor completo
await stevo.webhooks.list(); // solo secret_last4
await stevo.webhooks.update(creado.id, { active: false }); // PATCH parcial (incluido max_in_flight)
await stevo.webhooks.rotateSecret(creado.id, { overlap_seconds: 86_400 }); // el anterior vale 24h más; 0 = emergencia
await stevo.webhooks.delete(creado.id);
- Entrega confiable: hasta 8 intentos en unas 21h. Responde
2xxen hasta 10 s; los redirects no se siguen. - Caudal (
max_in_flight): cuántas entregas quedan en vuelo al mismo tiempo hacia ese destino (default 8, de 1 a 64). Súbelo para endpoints que absorben volumen — por ejemplo, una agencia con cientos de instancias. - Desactivación automática: responder
410 Gone, o 5 entregas agotadas seguidas, desactiva el webhook —disabled_reasonindica el motivo (gone/too_many_failures). Reactívalo conupdate(id, { active: true }). - Hasta 10 webhooks por cuenta (
409 webhook_limit_reached). Una key restringida a instancias no gestiona webhooks de cuenta (403 instance_restricted_key).
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. El sobre
{
"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_DE_LA_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!" }
}
El data está normalizado entre SM v2 y API Oficial: los mensajes usan message_id, chat, from, from_me, type, text, media...; las confirmaciones de lectura/entrega traen message_ids[]; instance.qr.updated trae qr_code_image; los demás instance.* traen reason/reason_code. Todo tipado en el paquete (StevoWebhookEnvelope, StevoMessageEventData, StevoReceiptEventData, StevoQrEventData, StevoConnectionEventData).
5.3. Validar la firma
Cada POST lleva X-Stevo-Signature: sha256=<hex>, X-Stevo-Timestamp, X-Stevo-Event-Id y X-Stevo-Attempt. constructEvent verifica la firma (con tolerancia de 5 min contra replay) y ya devuelve el evento tipado:
import express from 'express';
import { Stevo, StevoError } from 'stevo-sdk';
const stevo = new Stevo(process.env.STEVO_API_KEY!);
const app = express();
app.post(
'/webhooks/stevo',
express.raw({ type: 'application/json' }), // cuerpo CRUDO — NO uses express.json() aquí
async (req, res) => {
let event;
try {
event = stevo.webhooks.constructEvent({
rawBody: req.body,
signature: req.header('x-stevo-signature'),
timestamp: req.header('x-stevo-timestamp'),
secret: process.env.STEVO_WEBHOOK_SECRET!, // o [actual, anterior] durante la rotación
});
} catch (e) {
return res.status(e instanceof StevoError ? e.status : 401).end();
}
if (await yaProcesado(event.event_id)) return res.status(200).end(); // entrega "al menos una vez"
res.status(200).end(); // responde rápido; procesa en cola
await encolar(event);
},
);
Con Fetch API (Next.js Route Handler, Cloudflare Workers, Bun, Deno) es igual: const rawBody = await request.text() y stevo.webhooks.constructEvent({ rawBody, signature: request.headers.get('x-stevo-signature'), ... }). También existen verify (devuelve boolean), assert (lanza), parse (solo el formato) y sign (para pruebas).
La firma es HMAC-SHA256(secret, timestamp + "." + cuerpoCrudo). Si el framework hace JSON.parse y vuelves a serializar, el HMAC no coincide. Usa siempre un raw body parser.
event_idUna reentrega llega con el mismo event_id y un attempt mayor. Guarda los event_id procesados (ej.: UNIQUE(event_id) en tu base de datos) e ignora los repetidos.
5.4. Historial y reenvío — webhooks.deliveries
const fallas = await stevo.webhooks.deliveries.list({ status: 'failed', webhook_id: creado.id, limit: 50 });
for await (const pagina of stevo.webhooks.deliveries.iterate({ status: 'exhausted', since: '2026-09-20T00:00:00Z' })) {
for (const entrega of pagina.data) await stevo.webhooks.deliveries.retry(entrega.id); // mismo event_id, attempt++
}
const detalle = await stevo.webhooks.deliveries.get(deliveryId); // con attempt_log de cada intento
Estados: pending, delivered, failed, exhausted. Filtros: webhook_id, instance_id, status, event_type, since. Hasta 5 reenvíos manuales por entrega (409 manual_retry_limit_reached).
6. Enviando mensajes
Hay cuatro caminos — elige según lo que necesites:
| Camino | Cuándo usarlo |
|---|---|
stevo.whatsapp(id) | una sola capa para ambos motores: sendText, sendMedia, markRead, sendReaction, getStatus |
stevo.messages.send(id, params) | proxy de la API: un mensaje por llamada, con Idempotency-Key y estado consultable |
stevo.smv2(id) | el servidor SM v2 directo — 114 operaciones (chats, grupos, etiquetas, newsletter, voz…) |
stevo.oficial(id) | el gateway Cloud API de Meta — plantillas HSM, perfil, medios recibidos |
Capa unificada — stevo.whatsapp(id)
const wa = await stevo.whatsapp(instanceId);
console.log(wa.engine); // 'smv2' | 'official' | 'proxy'
const r = await wa.sendText({ to: '5511999999999', text: '¡Hola!', idempotencyKey: 'pedido-9f21' });
console.log(r.messageId);
await wa.sendMedia({ to: '5511999999999', url: 'https://...', type: 'image', caption: '¡Aquí está!' });
await wa.markRead({ messageId: r.messageId!, chat: '5511999999999' });
await wa.sendReaction({ messageId: r.messageId!, chat: '5511999999999', emoji: '👍', fromMe: true });
const st = await wa.getStatus(); // { engine, connected, loggedIn? }
Proxy de la API — stevo.messages
const r = await stevo.messages.send(
instanceId,
{ to: '5511999999999', text: '¡Hola!' },
{ idempotencyKey: 'pedido-9f21' }, // 1–200 caracteres, elegidos por ti
);
// { engine, sent, message_id?, status?, replayed, idempotencyKey }
await stevo.messages.send(instanceId, { to: '5511999999999', media_url: 'https://...', media_type: 'image', caption: '¡Aquí está!' });
// Solo API Oficial: payload Cloud API crudo (ej.: plantilla)
await stevo.messages.send(idOficial, {
to: '5511999999999',
cloud_api: { type: 'template', template: { name: 'hello_world', language: { code: 'es' } } },
});
// Estado: queued → sent → delivered → read (o failed)
const estado = await stevo.messages.get(instanceId, r.message_id!);
const porClave = await stevo.messages.getByIdempotencyKey(instanceId, 'pedido-9f21');
El proxy cuenta en el rate limit de tu key. Para volumen, usa stevo.dispatch, stevo.smv2() o stevo.oficial().
Servidor directo — stevo.smv2(id) y stevo.oficial(id)
Cada instancia corre en su propio servidor, con token propio — el SDK lo resuelve solo a partir del instanceId:
// SM v2 (WhatsApp no oficial) — 114 operaciones del servidor
const sm = await stevo.smv2(instanceId);
const envio = await sm.sendText({ body: { number: '5511999999999', text: '¡Hola!' } });
console.log(envio.data.Info.ID); // id del mensaje en SM v2
const grupos = await sm.getGroupList();
// API Oficial de Meta — formato Cloud API + plantillas HSM
const meta = await stevo.oficial(instanceIdOficial);
await meta.sendMessage({ to: '5511999999999', type: 'text', text: { body: '¡Hola!' } });
const plantillas = await meta.listTemplates();
// Medios RECIBIDOS (el webhook trae solo el id) — sin token de Meta
const medio = await meta.downloadMedia('1436196501752591'); // { data, mimeType, fileSize, sha256, fileName }
const stream = await meta.downloadMediaStream('1436196501752591'); // Response crudo, para archivos grandes
El cliente SM v2 tiene 114 operaciones generadas del swagger oficial, todas con autocompletado y tipos. Referencia: API StevoManager v2 · API Oficial.
Para instancias API Oficial, el token que devuelve la API es la clave del gateway de Stevo (sk_of_...), válida solo en el gateway — el access token de Meta nunca se expone. Trátalo como secreto.
6.1. Envío sin duplicar: Idempotency-Key
Repetir la misma clave con el mismo cuerpo (durante 24h, por instancia) no reenvía: la API devuelve la respuesta original con replayed: true. Cuerpo diferente → 409 idempotency_conflict; clave aún en procesamiento → 409 idempotency_in_progress.
Funciona en messages.send, whatsapp(id).sendText/sendMedia y smv2(id).sendText/sendMedia ({ idempotencyKey, body }). En la API Oficial con token directo la clave se ignora.
6.2. Timeout no significa "no se envió"
Si la conexión cae o el servidor de envío no responde (504 upstream_timeout), el mensaje puede haber salido. Con idempotencyKey, el SDK no repite solo — lanza StevoError con code: 'result_unknown'. Consulta antes de reenviar:
import { StevoError, type SendMessageParams } from 'stevo-sdk';
async function enviarConSeguridad(instanceId: string, params: SendMessageParams, idempotencyKey: string) {
try {
return await stevo.messages.send(instanceId, params, { idempotencyKey });
} catch (e) {
const incierto = e instanceof StevoError && (e.code === 'result_unknown' || e.code === 'upstream_timeout');
if (!incierto) throw e;
try {
const estado = await stevo.messages.getByIdempotencyKey(instanceId, idempotencyKey);
if (estado.status !== 'failed') return estado; // ya salió (o está saliendo) — NO reenvíes
} catch (consulta) {
if (!(consulta instanceof StevoError) || consulta.code !== 'not_found') throw consulta;
}
return stevo.messages.send(instanceId, params, { idempotencyKey }); // misma clave: seguro
}
}
502 upstream_unavailable es distinto: el proxy no alcanzó el servidor de la instancia, así que no se envió nada y es seguro repetir (retryable: true).
7. Envío masivo
Pasas los instance_ids que van a enviar — las credenciales de los servidores se resuelven internamente (nunca envías token):
const camp = await stevo.dispatch.createCampaign({
instance_ids: [instanceId],
name: 'Promo Julio',
messages: ['¡Hola! Tenemos novedades para ti 🎉'],
recipients: [{ phone: '5511999999999', name: 'María' }],
config: { minDelay: 5, maxDelay: 15 }, // segundos entre envíos
start: true,
});
await stevo.dispatch.getCampaign(camp.campaign_id); // estado/progreso
await stevo.dispatch.pause(camp.campaign_id);
await stevo.dispatch.resume(camp.campaign_id);
Con más de una instancia en instance_ids, la rotación es automática. messages acepta variaciones con medios y botones.
8. StevoVoice — llamadas de voz con IA
Requiere StevoVoice contratado en la instancia:
// Una llamada con agente de IA (ElevenLabs)
await stevo.voice.scheduleCall(instanceId, {
to_number: '5511999999999',
agent_id: 'agent_xyz',
scheduled_at: '2026-07-20T14:00:00Z', // opcional (default: ahora)
});
// En lote, con intervalo entre llamadas
await stevo.voice.batchCalls(instanceId, {
numbers: ['5511999999999', '5511888888888'],
agent_id: 'agent_xyz',
interval_seconds: 60,
});
const programadas = await stevo.voice.listScheduled(instanceId);
await stevo.voice.cancelCall(callId);
// Grabaciones de las llamadas nativas — requiere scope voice:read
const page = await stevo.voice.listRecordings(instanceId, {
limit: 50,
direction: 'outbound',
has_audio: true,
});
const grabacion = await stevo.voice.getRecording(instanceId, page.recordings[0].id);
const respuesta = await stevo.voice.downloadRecording(instanceId, grabacion.id);
const mp3 = await respuesta.arrayBuffer();
// Atajo post-llamada: acepta UUID, nombre visible o instance_name
const ultima = await stevo.voice.getLatestRecording('well-pessoal-teste', {
call_id: 'id-de-la-llamada', // opcional, pero recomendado en llamadas simultáneas
});
Receta lista: descargar la última grabación por la API REST
No necesitas averiguar el UUID de la instancia ni el ID de la grabación antes de
empezar. Crea una API Key con el scope voice:read e indica el nombre de la instancia:
curl --get 'https://openapi.stevo.chat/v1/voice/recordings/latest' \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE' \
--data-urlencode 'instance=nombre-de-la-instancia'
La respuesta ya trae la instancia resuelta, la última grabación y la URL autenticada de descarga:
{
"data": {
"instance": {
"id": "UUID_INSTANCIA",
"name": "Mi instancia",
"instance_name": "nombre-de-la-instancia"
},
"recording": {
"id": "UUID_GRABACION",
"duration_seconds": 42,
"format": "mp3",
"download_available": true,
"download_url": "/v1/instances/UUID_INSTANCIA/voice/recordings/UUID_GRABACION/download"
}
}
}
Copia el download_url y haz el segundo GET con la misma clave. El -L es
obligatorio porque la API valida la propiedad y responde con un redirect 307 al
MP3:
curl -L \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE' \
'https://openapi.stevo.chat/v1/instances/UUID_INSTANCIA/voice/recordings/UUID_GRABACION/download' \
--output llamada.mp3
Para listar el historial, usa el data.instance.id devuelto en el primer paso:
curl --get 'https://openapi.stevo.chat/v1/instances/UUID_INSTANCIA/voice/recordings' \
--header 'Authorization: Bearer stevo_sk_TU_CLAVE' \
--data-urlencode 'limit=20' \
--data-urlencode 'has_audio=true'
En automatizaciones, pasa también call_id al endpoint latest cuando varias
llamadas puedan terminar juntas. La grabación aparece solo después de finalizar y
completar la subida; reintenta brevemente si el primer intento responde 404.
Si el nombre corresponde a más de una instancia, la API devuelve
409 ambiguous_instance; en ese caso, usa el UUID.
Activar, configurar e incrustar el webphone (widget)
// Estado de StevoVoice en la instancia SM v2: ¿motor disponible? ¿activado? cuota/canales
const st = await stevo.voice.status(instanceId); // { voice_available, enabled, unlimited, remaining, channels, active_calls }
// Activar SIN cobrar (reglas del panel): una cuenta común activa la prueba de 50 llamadas;
// una cuenta con paquete StevoVoice usa un cupo del paquete. Para canales/ilimitado:
await stevo.voice.enable(instanceId);
await stevo.billing.purchase({ product: 'stevovoice', instance_id: instanceId, tier: 5 }); // tarjeta guardada
const { checkout_url } = await stevo.billing.checkout({ product: 'stevovoice', instance_id: instanceId, tier: 5 }); // link de pago
// Configuración de la pestaña StevoVoice
await stevo.voice.updateSettings(instanceId, {
summary_enabled: true, summary_emails: ['gerente@empresa.com'], // resumen de la llamada por e-mail
coach_enabled: true, // coach IA durante la llamada
receive_enabled: true, // recibir llamadas en el webphone
});
// Widget: webphone incrustable en el sitio del cliente
const w = await stevo.voice.createWidget(instanceId, { name: 'Sitio', allowed_origins: ['https://misitio.com'] });
console.log(w.embed_snippet); // <script src="https://call.shurima.cloud/stevophone/widget.js" data-widget="wgt_..."></script>
await stevo.voice.updateWidget(w.id, { video_enabled: true });
await stevo.voice.deleteWidget(w.id);
9. Stevo IA (v2)
Configura agentes multiproveedor, embudo de etapas, base de conocimiento y más:
// Config base + clave del proveedor (write-only: nunca se devuelve)
await stevo.ai.updateSettings(instanceId, { enabled: true, timezone: 'America/Mexico_City' });
await stevo.ai.setProviderKeys(instanceId, { openai: 'sk-...' });
// Agente + embudo
const agente = await stevo.ai.agents.create(instanceId, {
name: 'Asistente',
provider: 'openai',
model: 'gpt-4o-mini',
system_prompt: 'Eres un asistente cordial.',
is_primary: true,
});
await stevo.ai.stages.create(agente.id, { name: 'Bienvenida', objective: 'Obtener el nombre' });
// FAQ, follow-up y conocimiento (RAG)
await stevo.ai.faq.create(instanceId, { question: '¿Cuál es el horario?', answer: 'De 9 a 18 h.' });
await stevo.ai.followup.set(instanceId, { enabled: true, max_followups: 3 });
10. Retry
El SDK repite solo cuando es seguro:
| Situación | Comportamiento por defecto |
|---|---|
429 (rate limit) | repite siempre, respetando Retry-After |
5xx / falla de conexión en GET, PATCH, DELETE... | repite |
5xx / falla de conexión en POST | no repite (evita duplicar compra, campa ña o envío) |
POST con Idempotency-Key | repite solo si la API responde retryable: true; una falla de conexión se vuelve result_unknown |
respuesta con retryable: false | nunca repite, en ningún método |
| timeout del propio SDK | nunca repite |
const stevo = new Stevo(process.env.STEVO_API_KEY!, {
retry: {
maxAttempts: 3, // TOTAL de intentos, incluido el primero
retry429: true,
retry5xx: true,
retryNetwork: true,
retryTimeout: false,
retryNonIdempotent: false, // true permite repetir POST sin clave (¡puede duplicar!)
},
});
¿Ya tienes una cola con backoff en tu worker? Desactiva el del SDK (retry: { enabled: false }) y usa StevoError.retryable para decidir — así solo una capa repite.
11. Errores y observabilidad
Toda falla se convierte en un StevoError:
import { StevoError } from 'stevo-sdk';
try {
await stevo.billing.purchase({ plan: 'stevo3' });
} catch (e) {
if (e instanceof StevoError) {
console.error(e.status, e.code, e.requestId, e.retryable);
if (e.code === 'no_saved_card') {
// cuenta sin tarjeta guardada — usa billing.checkout u orienta a registrarla en el panel
}
}
}
| Campo | Qué trae |
|---|---|
status / code / message | HTTP, código de negocio y mensaje (status: 0 = red/timeout) |
retryable | si tiene sentido repetir — viene de la propia API cuando ella lo informa |
requestId | id de correlación (X-Request-Id) — infórmalo a soporte |
attempt, latencyMs, retryAfter, rateLimit | intento en que falló, duración, Retry-After y límites de la key |
toJSON() | serializa todo de forma segura para log |
Códigos comunes: not_ready (instancia no conectada), insufficient_scope, key_restricted, external_ref_conflict, idempotency_conflict, idempotency_in_progress, upstream_timeout, upstream_unavailable, result_unknown, invalid_response, webhook_signature_invalid, webhook_timestamp_expired, no_saved_card, no_available_slot.
Para métricas, pasa onResponse — se llama en cada intento y nunca altera el resultado:
const stevo = new Stevo(process.env.STEVO_API_KEY!, {
onResponse: (info) => {
// { method, path, status, ok, attempt, latencyMs, requestId, rateLimit?, willRetry, errorCode? }
metrics.histogram('stevo_request_ms', info.latencyMs, { path: info.path, status: String(info.status) });
},
});
12. Actualizando de la 1.x a la 2.x
La 2.0.0 cambió algunos comportamientos para nunca duplicar un envío o una compra:
- Los
POSTya no se repiten automáticamente en5xxni en falla de conexión (solo en429, o conIdempotency-Keyyretryable: truede la API). Quien dependía del retry implícito enbilling.purchase,dispatch.createCampaigno envíos debe tratar elStevoError. - El timeout nunca se repite, ni siquiera en
GET(actívalo conretry: { retryTimeout: true }si quieres). - El SDK obedece
retryable: falsede la API, y una falla de conexión conidempotencyKeyse vuelveresult_unknown(consultamessages.getByIdempotencyKey). messages.sendy los envíos dewhatsapp()validan la respuesta: un2xxsin cuerpo reconocible se vuelveStevoErrorinvalid_response.- Los envíos con
idempotencyKeydevuelvenidempotencyKey(eco) en lugar deidempotent: true. - Los tipos de SM v2 son estrictos y
JIDahora esstring(era objeto) — el código con un campo de tipo incorrecto deja de compilar. createWhatsAppycreateInstanceLifecycleya no se exportan — usastevo.whatsapp(id)ystevo.instances.
En la práctica: npm install stevo-sdk@latest, corre tsc y revisa los catch de tus POST.
13. Seguridad
- Nunca uses la API key en el frontend. Mantenla en el backend y en una variable de entorno.
- Scopes mínimos y, si es posible, key restringida a las instancias de la integración.
- Valida todo webhook de cuenta con
constructEventsobre el cuerpo crudo. - El
tokende la instancia y las URLs de WebSocket (?apikey=) son secretos — no los registres en logs ni los envíes al front-end. - Nunca accedas a la base de datos de Stevo. Las integraciones externas usan solo la API y el SDK: instancias por
external_ref, sincronización por webhooks de cuenta +listChanges, y envío conidempotencyKey.
14. Entregándoselo a tu IA para implementar
Si quien va a escribir la integración es una IA (Claude, Cursor, Copilot, ChatGPT...), pega este bloque en su contexto:
Usa el paquete npm
stevo-sdk(2.x, TypeScript, tipos incluidos, Node 18+). Instancianew Stevo(process.env.STEVO_API_KEY)con la keystevo_sk_...de la pestaña API Keys del panel de Stevo. Gestión:me,instances(list/get/create/update conexternal_refymetadata, findByExternalRef, listChanges/iterateChanges para reconciliar incluidas las eliminadas, delete con{ confirm: true }, getQrCode/refreshQrCode/getConnection/disconnect/health, restart, recreateTotal, setWebhook, fuse),links,ghl,billing(purchase/checkout),ghlAgency,ai,dispatch(campañas con instance_ids) yvoice. Webhooks de cuenta:stevo.webhooks.create({ url, events, max_in_flight })devuelve elsecretuna sola vez; en el handler, usastevo.webhooks.constructEvent({ rawBody, signature: X-Stevo-Signature, timestamp: X-Stevo-Timestamp, secret })sobre el cuerpo CRUDO y deduplica porevent_id;webhooks.deliverieslista y reenvía entregas. Envío:stevo.whatsapp(id)(sendText/sendMedia/markRead/sendReaction/getStatus en ambos motores),stevo.messages.send(id, params, { idempotencyKey })+messages.get/getByIdempotencyKeypara el estado,stevo.smv2(id)(servidor SM v2, 114 operaciones) ystevo.oficial(id)(Cloud API de Meta). El SDK resuelveserver_url+tokensolo. Los errores sonStevoErrorcon.status,.code,.retryabley.requestId; anteresult_unknown/upstream_timeoutconsulta el estado por la clave antes de reenviar. Retry por defecto: 429 siempre; 5xx/red solo fuera de POST; timeout nunca. Referencia completa: https://tutorial.stevo.chat/public-api-reference
Referencias
- 📘 API de Gestión — guía de integración (HTTP puro, cualquier lenguaje)
- 📖 Referencia de la API de Gestión (endpoints, errores y scopes)
- 📖 API StevoManager v2 y API Oficial
- 📦 Paquete en npm
- 🤖 Servidor MCP:
https://openapi.stevo.chat/mcp(misma API key)