Pular para o conteúdo principal

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:

ÁreaSe accede conQué hace
Gestión de cuentastevo.me, stevo.instances, stevo.links, stevo.ghl, stevo.billing, stevo.ghlAgencyinstancias (crear, conectar, reconciliar, eliminar), links de acceso, GHL, compras y GHL Agencia
Webhooks de cuentastevo.webhooksun endpoint recibe los eventos de todas las instancias, firmados con HMAC-SHA256, con historial y reenvío
Envío de mensajesstevo.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.aiagentes, embudo, claves de proveedor, FAQ, tools, follow-up, RAG, memoria
Envío masivostevo.dispatchcampañas de WhatsApp con rotación de instancias, programación y variaciones
StevoVoicestevo.voicellamadas 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

Novedades de la 2.x
  • Webhooks de cuenta firmados, con entrega confiable, deduplicación por event_id, historial y reenvío (stevo.webhooks) — y max_in_flight para controlar el caudal (2.1.0).
  • Envío seguro: Idempotency-Key en el envío y estado del mensaje (queued → sent → delivered → read/failed).
  • Instancias vinculadas a tu sistema: external_ref y metadata, reconciliación con listChanges y eliminación con delete.
  • 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.

¿Sin código? Usa IA + MCP

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.

¿Sin Node.js?

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ónDefaultObservación
baseUrlhttps://openapi.stevo.chat—
timeoutMs60000timeout por solicitud
retryver Retryreglas de repetición automática
maxRetries2legado — repeticiones extra (maxRetries: 2 = maxAttempts: 3)
onResponse—hook llamado en cada intento (métricas/logs)
credentialsCacheMs0caché 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' });
Operaciones irreversibles

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' });
  • metadata en update reemplaza el objeto (no hace merge) — lee el actual antes de cambiar un solo campo.
  • external_ref repetido en la cuenta → 409 external_ref_conflict.
  • external_ref y metadata tambié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 }. iterateChanges sigue el cursor solo.
  • Deduplica deleted por id + deleted_at. Si el mismo id reaparece en data con updated_at posterior al deleted_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.webhooks es 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 2xx en 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_reason indica el motivo (gone / too_many_failures). Reactívalo con update(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).

El cuerpo tiene que ser CRUDO

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.

Deduplica por event_id

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

CaminoCuá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.

Token de la 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ónComportamiento 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 POSTno repite (evita duplicar compra, campaña o envío)
POST con Idempotency-Keyrepite solo si la API responde retryable: true; una falla de conexión se vuelve result_unknown
respuesta con retryable: falsenunca repite, en ningún método
timeout del propio SDKnunca 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
}
}
}
CampoQué trae
status / code / messageHTTP, código de negocio y mensaje (status: 0 = red/timeout)
retryablesi tiene sentido repetir — viene de la propia API cuando ella lo informa
requestIdid de correlación (X-Request-Id) — infórmalo a soporte
attempt, latencyMs, retryAfter, rateLimitintento 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 POST ya no se repiten automáticamente en 5xx ni en falla de conexión (solo en 429, o con Idempotency-Key y retryable: true de la API). Quien dependía del retry implícito en billing.purchase, dispatch.createCampaign o envíos debe tratar el StevoError.
  • El timeout nunca se repite, ni siquiera en GET (actívalo con retry: { retryTimeout: true } si quieres).
  • El SDK obedece retryable: false de la API, y una falla de conexión con idempotencyKey se vuelve result_unknown (consulta messages.getByIdempotencyKey).
  • messages.send y los envíos de whatsapp() validan la respuesta: un 2xx sin cuerpo reconocible se vuelve StevoError invalid_response.
  • Los envíos con idempotencyKey devuelven idempotencyKey (eco) en lugar de idempotent: true.
  • Los tipos de SM v2 son estrictos y JID ahora es string (era objeto) — el código con un campo de tipo incorrecto deja de compilar.
  • createWhatsApp y createInstanceLifecycle ya no se exportan — usa stevo.whatsapp(id) y stevo.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 constructEvent sobre el cuerpo crudo.
  • El token de 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 con idempotencyKey.

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+). Instancia new Stevo(process.env.STEVO_API_KEY) con la key stevo_sk_... de la pestaña API Keys del panel de Stevo. Gestión: me, instances (list/get/create/update con external_ref y metadata, 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) y voice. Webhooks de cuenta: stevo.webhooks.create({ url, events, max_in_flight }) devuelve el secret una sola vez; en el handler, usa stevo.webhooks.constructEvent({ rawBody, signature: X-Stevo-Signature, timestamp: X-Stevo-Timestamp, secret }) sobre el cuerpo CRUDO y deduplica por event_id; webhooks.deliveries lista 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/getByIdempotencyKey para el estado, stevo.smv2(id) (servidor SM v2, 114 operaciones) y stevo.oficial(id) (Cloud API de Meta). El SDK resuelve server_url + token solo. Los errores son StevoError con .status, .code, .retryable y .requestId; ante result_unknown/upstream_timeout consulta 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​