Pular para o conteúdo principal

SDK oficial — stevo-sdk

O stevo-sdk é o SDK oficial (TypeScript/JavaScript) da Stevo. Um pacote só, com tudo que a plataforma oferece — bem separado por área:

ÁreaComo acessaO que faz
Gestão da contastevo.me, stevo.instances, stevo.links, stevo.ghl, stevo.billing, stevo.ghlAgencyinstâncias (criar, conectar, reconciliar, apagar), links de acesso, GHL, compras e GHL Agência
Webhooks de contastevo.webhooksum endpoint recebe os eventos de todas as instâncias, assinados com HMAC-SHA256, com histórico e reenvio
Envio de mensagemstevo.whatsapp(id) · stevo.messages · stevo.smv2(id) · stevo.oficial(id)camada unificada, proxy com idempotência e status, ou o servidor da instância direto (114 operações SM v2 / Cloud API da Meta)
Stevo IA (v2)stevo.aiagentes, funil, chaves de provedor, FAQ, tools, follow-up, RAG, memória
Disparo em massastevo.dispatchcampanhas de WhatsApp com rotação de instâncias, agendamento e variações
StevoVoicestevo.voicechamadas de voz com IA, gravações, ativação e widget

📦 npm: npm.im/stevo-sdk · versão atual 2.1.0 · Requer Node.js 18+ · ESM + CommonJS + tipos inclusos · zero dependências de runtime

Novidades da 2.x
  • Webhooks de conta assinados, com entrega confiável, deduplicação por event_id, histórico e reenvio (stevo.webhooks) — e max_in_flight para controlar a vazão (2.1.0).
  • Envio seguro: Idempotency-Key no envio e status da mensagem (queued → sent → delivered → read/failed).
  • Instâncias amarradas ao seu sistema: external_ref e metadata, reconciliação por listChanges e exclusão com delete.
  • Lifecycle completo pela API: QR, conexão, desconexão e saúde — nos dois motores, inclusive API Oficial.
  • Camada unificada stevo.whatsapp(id), retry configurável e observabilidade (X-Request-Id, onResponse).

Vindo da 1.x? Veja Atualizando da 1.x para a 2.x — a migração é pequena.

Prefere não escrever código? Use a IA + MCP

A mesma API tem um servidor MCP em https://openapi.stevo.chat/mcp. Plugue no n8n (nó MCP Client), Claude ou Cursor com a sua API key e a IA gerencia sua conta, envia mensagens, cria campanhas e agenda chamadas sem uma linha de código.

Sem Node.js?

Tudo que o SDK faz passa pela API de Gestão (REST). Para usar direto por HTTP, em qualquer linguagem, veja API de Gestão — guia de integração.

1. Crie sua API Key​

No painel da Stevo: menu do perfil → API Keys → Nova API Key. Marque os scopes (permissões) que essa key vai ter — de instances:read (só leitura) até envio, webhooks, disparo, voz, IA e billing. A key aparece uma única vez — guarde com segurança e nunca a exponha no navegador/front-end.

Uma key também pode ser restrita a algumas instâncias: fora da lista, a API responde 404 como se a instância não existisse. Prefira uma key restrita quando a integração fala só com um cliente.

2. Instale e conecte​

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 eu = await stevo.me.get(); // conta, scopes e restrição da key (allowed_instance_ids)

O construtor valida a key na hora: vazia ou sem o prefixo stevo_sk_ lança Error imediatamente.

Opções do construtor​

OpçãoDefaultObservação
baseUrlhttps://openapi.stevo.chat—
timeoutMs60000timeout por requisição
retryver Retryregras de repetição automática
maxRetries2legado — repetições extras (maxRetries: 2 = maxAttempts: 3)
onResponse—hook chamado a cada tentativa (métricas/logs)
credentialsCacheMs0cache das credenciais que smv2(id)/oficial(id) resolvem; invalidado sozinho em 401, restart, recreateTotal e delete

3. Gestão da conta​

// Criar instância numa vaga livre do plano (não compra vaga nova)
const { instance } = await stevo.instances.create({ name: 'minha-instancia' });
const oficial = await stevo.instances.create({ engine: 'official' }); // devolve onboarding_url

await stevo.instances.restart(id); // reconectar (ler QR depois)
await stevo.instances.recreateTotal(id); // DESTRUTIVO: zera tudo
await stevo.instances.recreateTotalBatch([id1, id2, id3]); // em massa (até 50)

// Fusão: liga a instância API Oficial à SM v2 do MESMO número — STChat, transmissor e IA
// tratam as duas como um só contato (o "Fundir instâncias" do painel)
await stevo.instances.fuse(idOficial, idSmv2); // 400 phone_mismatch/engine_mismatch, 409 already_fused
const fusao = await stevo.instances.getFusion(idOficial); // { instance, fused, partner }
await stevo.instances.unfuse(idOficial); // limpa os dois lados

// Links, GHL e 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 o cartão salvo

// Sem cartão salvo (ou pra mandar o link pro cliente final): Stripe Checkout hospedado.
// Não cobra na hora — devolve checkout_url; após pagar, o provisionamento é automático.
const { checkout_url } = await stevo.billing.checkout({ plan: 'stevo3' });
Operações irreversíveis

recreateTotal, recreateTotalBatch, instances.delete, disconnect com logout: true e ai.conversations.clear não têm desfazer. Peça confirmação explícita em qualquer tela ou automação que os exponha.

3.1. external_ref e metadata — amarre a instância ao seu sistema​

Use external_ref (referência opaca, única por conta, até 128 caracteres) e metadata (pares texto→texto, até 16 chaves) para ligar a instância ao cliente, loja ou workspace do seu sistema — sem precisar guardar o UUID da Stevo:

const { instance } = await stevo.instances.create({
name: 'loja-centro',
external_ref: 'WSP-000123',
metadata: { workspace: 'WSP-000123', plano: 'pro' },
});

await stevo.instances.update(instance.id, { metadata: { plano: 'enterprise' } }); // SUBSTITUI o objeto inteiro
await stevo.instances.update(instance.id, { external_ref: null }); // limpa a referência

const minha = await stevo.instances.findByExternalRef('WSP-000123'); // Instance | null
const doTenant = await stevo.instances.list({ external_ref: 'WSP-000123' });
  • metadata em update substitui o objeto (não faz merge) — leia o atual antes de mudar um campo só.
  • external_ref repetido na conta → 409 external_ref_conflict.
  • external_ref e metadata também chegam em todo evento de webhook de conta (instance.external_ref), então seu handler sabe de qual cliente é o evento sem consultar nada.

3.2. Reconciliação — listChanges / iterateChanges​

Para manter o seu banco em sincronia sem reler tudo, peça só o que mudou desde a última vez — incluindo as instâncias apagadas:

const desde = await meuBanco.ultimaSincronizacao(); // Date

for await (const pagina of stevo.instances.iterateChanges(desde)) {
for (const instancia of pagina.data) await meuBanco.upsertInstancia(instancia);
for (const apagada of pagina.deleted) await meuBanco.removerInstancia(apagada.id); // { id, external_ref, metadata, deleted_at }
}

await meuBanco.salvarSincronizacao(new Date());
  • listChanges({ updated_since, cursor?, limit? }) devolve uma página: { data, deleted, next_cursor, has_more, deleted_has_more }. iterateChanges segue o cursor sozinho.
  • Deduplique deleted por id + deleted_at. Se o mesmo id reaparecer em data com updated_at depois do deleted_at, o slot foi reaproveitado numa nova instância.

3.3. Apagar uma instância — delete​

await stevo.instances.delete(id);                     // lança Error ANTES de chamar a API: falta { confirm: true }
await stevo.instances.delete(id, { confirm: true }); // { deleted: true, deleted_at }

Libera o slot, limpa external_ref/metadata e registra a exclusão (ela aparece em listChanges e dispara o evento instance.deleted). É idempotente: apagar um slot já vazio devolve { deleted: false }.

4. Conectar uma instância: criar → QR → conectado​

getConnection e health funcionam nos dois motores; QR e disconnect são do SM v2 (a API Oficial conecta pelo onboarding_url e responde 400 unsupported_engine a essas duas):

const { instance } = await stevo.instances.create({ name: 'atendimento', external_ref: 'WSP-000123' });

// Configure o webhook ANTES de escanear, para não perder o instance.connected
await stevo.webhooks.create({
url: 'https://meu-sistema.com/webhooks/stevo',
events: ['instance.connected', 'instance.disconnected', 'instance.qr.updated', 'message.received'],
});

// QR — pode levar alguns segundos até o servidor gerar
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,... — renderize num <img>, ou mostre qr.pairing_code

// Confirme pela conexão (ou pelo evento instance.connected no seu webhook)
const conexao = await stevo.instances.getConnection(instance.id); // { connected, logged_in, name?, phone_number? }

await stevo.instances.refreshQrCode(instance.id); // QR expirou? gera outro (restaura o webhook salvo)
await stevo.instances.health(instance.id); // não lança se o servidor cair: server_reachable: false
await stevo.instances.disconnect(instance.id); // desconecta mantendo a sessão
await stevo.instances.disconnect(instance.id, { logout: true, confirm: true }); // IRREVERSÍVEL: descarta a sessão

Webhook da instância × webhook de conta​

  • stevo.instances.setWebhook(id, ...) é o webhook de uma instância — o mesmo de Configurações → Webhook no painel, no formato nativo do motor e sem assinatura. Continua funcionando:
await stevo.instances.setWebhook(id, { url: 'https://meu-sistema.com/webhook/stevo', events: ['MESSAGE', 'CONNECTION'] }); // SM v2
await stevo.instances.setWebhook(idOficial, { url: 'https://meu-sistema.com/webhook/oficial' }); // API Oficial (todos os eventos)
const webhook = await stevo.instances.getWebhook(id); // { engine, url, events }
await stevo.instances.deleteWebhook(id);
  • stevo.webhooks é o webhook de conta — recomendado para integrações novas: um endpoint recebe os eventos de todas as instâncias num envelope único e normalizado, assinado, com reentrega e histórico. Veja a próxima seção.

5. Webhooks de conta​

5.1. Criar e gerenciar​

const criado = await stevo.webhooks.create({
url: 'https://meu-sistema.com/webhooks/stevo', // https:// obrigatório, destino público
events: ['message.received', 'message.failed', 'instance.connected', 'instance.disconnected'], // ou ['*']
description: 'Meu CRM',
max_in_flight: 16, // entregas simultâneas: 1 a 64, default 8
});
console.log(criado.secret); // whsec_... — guarde AGORA: a API não devolve o valor completo de novo

await stevo.webhooks.list(); // só secret_last4
await stevo.webhooks.update(criado.id, { active: false }); // PATCH parcial (inclusive max_in_flight)
await stevo.webhooks.rotateSecret(criado.id, { overlap_seconds: 86_400 }); // o antigo vale por mais 24h; 0 = emergência
await stevo.webhooks.delete(criado.id);
  • Entrega confiável: até 8 tentativas em cerca de 21h. Responda 2xx em até 10 s; redirects não são seguidos.
  • Vazão (max_in_flight): quantas entregas ficam em voo ao mesmo tempo para esse destino (default 8, de 1 a 64). Suba para endpoints que absorvem volume — por exemplo, uma agência com centenas de instâncias.
  • Desativação automática: responder 410 Gone, ou 5 entregas esgotadas seguidas, desativa o webhook — disabled_reason diz o motivo (gone / too_many_failures). Reative com update(id, { active: true }).
  • Até 10 webhooks por conta (409 webhook_limit_reached). Key restrita a instâncias não gerencia webhook de conta (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. O envelope​

{
"event_id": "evt_01J...",
"type": "message.received",
"attempt": 1,
"created_at": "2026-09-22T14:03:11.000Z",
"account_id": "UUID_DA_CONTA",
"instance": { "id": "UUID_DA_INSTANCIA", "external_ref": "WSP-000123", "metadata": { "plano": "pro" }, "engine": "smv2" },
"data": { "message_id": "3EB0...", "chat": "5511999999999", "from": "5511999999999", "from_me": false, "is_group": false, "type": "text", "text": "Olá!" }
}

O data é normalizado entre SM v2 e API Oficial: mensagens usam message_id, chat, from, from_me, type, text, media...; confirmações de leitura/entrega trazem message_ids[]; instance.qr.updated traz qr_code_image; os demais instance.* trazem reason/reason_code. Tudo tipado no pacote (StevoWebhookEnvelope, StevoMessageEventData, StevoReceiptEventData, StevoQrEventData, StevoConnectionEventData).

5.3. Validar a assinatura​

Cada POST leva X-Stevo-Signature: sha256=<hex>, X-Stevo-Timestamp, X-Stevo-Event-Id e X-Stevo-Attempt. constructEvent confere a assinatura (com tolerância de 5 min contra replay) e já devolve o 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' }), // corpo CRU — NÃO use express.json() aqui
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!, // ou [atual, anterior] durante a rotação
});
} catch (e) {
return res.status(e instanceof StevoError ? e.status : 401).end();
}

if (await jaProcessado(event.event_id)) return res.status(200).end(); // entrega "pelo menos uma vez"
res.status(200).end(); // responda rápido; processe em fila
await enfileirar(event);
},
);

Em Fetch API (Next.js Route Handler, Cloudflare Workers, Bun, Deno) é igual: const rawBody = await request.text() e stevo.webhooks.constructEvent({ rawBody, signature: request.headers.get('x-stevo-signature'), ... }). Também existem verify (devolve boolean), assert (lança), parse (só o formato) e sign (para testes).

O corpo tem que ser CRU

A assinatura é HMAC-SHA256(secret, timestamp + "." + corpoCru). Se o framework fizer JSON.parse e você reserializar, o HMAC não confere. Use sempre um raw body parser.

Deduplique por event_id

Uma reentrega chega com o mesmo event_id e attempt maior. Guarde os event_id processados (ex.: UNIQUE(event_id) no seu banco) e ignore repetidos.

5.4. Histórico e reenvio — webhooks.deliveries​

const falhas = await stevo.webhooks.deliveries.list({ status: 'failed', webhook_id: criado.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); // mesmo event_id, attempt++
}

const detalhe = await stevo.webhooks.deliveries.get(deliveryId); // com attempt_log de cada tentativa

Status: pending, delivered, failed, exhausted. Filtros: webhook_id, instance_id, status, event_type, since. Até 5 reenvios manuais por entrega (409 manual_retry_limit_reached).

6. Enviando mensagens​

Há quatro caminhos — escolha pelo que você precisa:

CaminhoQuando usar
stevo.whatsapp(id)uma camada só para os dois motores: sendText, sendMedia, markRead, sendReaction, getStatus
stevo.messages.send(id, params)proxy da API: uma mensagem por chamada, com Idempotency-Key e status consultável
stevo.smv2(id)o servidor SM v2 direto — 114 operações (chats, grupos, etiquetas, newsletter, voz…)
stevo.oficial(id)o gateway Cloud API da Meta — templates HSM, perfil, mídia recebida

Camada 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: 'Olá!', idempotencyKey: 'pedido-9f21' });
console.log(r.messageId);

await wa.sendMedia({ to: '5511999999999', url: 'https://...', type: 'image', caption: 'Segue!' });
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 da API — stevo.messages​

const r = await stevo.messages.send(
instanceId,
{ to: '5511999999999', text: 'Olá!' },
{ idempotencyKey: 'pedido-9f21' }, // 1–200 caracteres, escolhidos por você
);
// { engine, sent, message_id?, status?, replayed, idempotencyKey }

await stevo.messages.send(instanceId, { to: '5511999999999', media_url: 'https://...', media_type: 'image', caption: 'Segue!' });

// Só API Oficial: payload Cloud API cru (ex.: template)
await stevo.messages.send(idOficial, {
to: '5511999999999',
cloud_api: { type: 'template', template: { name: 'hello_world', language: { code: 'pt_BR' } } },
});

// Status: queued → sent → delivered → read (ou failed)
const status = await stevo.messages.get(instanceId, r.message_id!);
const porChave = await stevo.messages.getByIdempotencyKey(instanceId, 'pedido-9f21');

O proxy conta no rate limit da sua key. Para volume, use stevo.dispatch, stevo.smv2() ou stevo.oficial().

Servidor direto — stevo.smv2(id) e stevo.oficial(id)​

Cada instância roda no seu próprio servidor, com token próprio — o SDK resolve isso sozinho a partir do instanceId:

// SM v2 (WhatsApp não-oficial) — 114 operações do servidor
const sm = await stevo.smv2(instanceId);
const envio = await sm.sendText({ body: { number: '5511999999999', text: 'Olá!' } });
console.log(envio.data.Info.ID); // id da mensagem no SM v2
const grupos = await sm.getGroupList();

// API Oficial Meta — formato Cloud API + templates HSM
const meta = await stevo.oficial(instanceIdOficial);
await meta.sendMessage({ to: '5511999999999', type: 'text', text: { body: 'Olá!' } });
const templates = await meta.listTemplates();
// Mídia RECEBIDA (o webhook traz só o id) — sem token da Meta
const midia = await meta.downloadMedia('1436196501752591'); // { data, mimeType, fileSize, sha256, fileName }
const stream = await meta.downloadMediaStream('1436196501752591'); // Response cru, para arquivos grandes

O client SM v2 tem 114 operações geradas do swagger oficial, todas com autocomplete e tipos. Referência: API StevoManager v2 · API Oficial.

Token da API Oficial

Para instâncias API Oficial, o token que a API devolve é a chave do gateway da Stevo (sk_of_...), válida só no gateway — o access token da Meta nunca é exposto. Trate como segredo.

6.1. Envio sem duplicar: Idempotency-Key​

Repetir a mesma chave com o mesmo corpo (por 24h, por instância) não reenvia: a API devolve a resposta original com replayed: true. Corpo diferente → 409 idempotency_conflict; chave ainda em processamento → 409 idempotency_in_progress.

Funciona em messages.send, whatsapp(id).sendText/sendMedia e smv2(id).sendText/sendMedia ({ idempotencyKey, body }). Na API Oficial com token direto a chave é ignorada.

6.2. Timeout não significa "não enviou"​

Se a conexão cair ou o servidor de envio não responder (504 upstream_timeout), a mensagem pode ter saído. Com idempotencyKey, o SDK não repete sozinho — lança StevoError com code: 'result_unknown'. Consulte antes de reenviar:

import { StevoError, type SendMessageParams } from 'stevo-sdk';

async function enviarComSeguranca(instanceId: string, params: SendMessageParams, idempotencyKey: string) {
try {
return await stevo.messages.send(instanceId, params, { idempotencyKey });
} catch (e) {
const incerto = e instanceof StevoError && (e.code === 'result_unknown' || e.code === 'upstream_timeout');
if (!incerto) throw e;

try {
const status = await stevo.messages.getByIdempotencyKey(instanceId, idempotencyKey);
if (status.status !== 'failed') return status; // já saiu (ou está saindo) — NÃO reenvie
} catch (consulta) {
if (!(consulta instanceof StevoError) || consulta.code !== 'not_found') throw consulta;
}
return stevo.messages.send(instanceId, params, { idempotencyKey }); // mesma chave: seguro
}
}

502 upstream_unavailable é diferente: o proxy não alcançou o servidor da instância, então nada foi enviado e é seguro repetir (retryable: true).

7. Disparo em massa​

Você passa os instance_ids que vão disparar — as credenciais dos servidores são resolvidas internamente (você nunca envia token):

const camp = await stevo.dispatch.createCampaign({
instance_ids: [instanceId],
name: 'Promo Julho',
messages: ['Olá! Temos novidades para você 🎉'],
recipients: [{ phone: '5511999999999', name: 'Maria' }],
config: { minDelay: 5, maxDelay: 15 }, // segundos entre envios
start: true,
});

await stevo.dispatch.getCampaign(camp.campaign_id); // status/progresso
await stevo.dispatch.pause(camp.campaign_id);
await stevo.dispatch.resume(camp.campaign_id);

Com mais de uma instância em instance_ids, a rotação é automática. messages aceita variações com mídia e botões.

8. StevoVoice — chamadas de voz com IA​

Requer o StevoVoice assinado na instância:

// Uma chamada com 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: agora)
});

// Em lote, com intervalo entre chamadas
await stevo.voice.batchCalls(instanceId, {
numbers: ['5511999999999', '5511888888888'],
agent_id: 'agent_xyz',
interval_seconds: 60,
});

const agendadas = await stevo.voice.listScheduled(instanceId);
await stevo.voice.cancelCall(callId);

// Gravações das ligações nativas — requer scope voice:read
const page = await stevo.voice.listRecordings(instanceId, {
limit: 50,
direction: 'outbound',
has_audio: true,
});

const gravacao = await stevo.voice.getRecording(instanceId, page.recordings[0].id);
const resposta = await stevo.voice.downloadRecording(instanceId, gravacao.id);
const mp3 = await resposta.arrayBuffer();

// Atalho pós-ligação: aceita UUID, nome visível ou instance_name
const ultima = await stevo.voice.getLatestRecording('well-pessoal-teste', {
call_id: 'id-da-chamada', // opcional, mas recomendado em chamadas simultâneas
});

Receita pronta: baixar a última gravação pela API REST​

Você não precisa descobrir o UUID da instância nem o ID da gravação antes de começar. Crie uma API Key com o scope voice:read e informe o nome da instância:

curl --get 'https://openapi.stevo.chat/v1/voice/recordings/latest' \
--header 'Authorization: Bearer stevo_sk_SUA_CHAVE' \
--data-urlencode 'instance=nome-da-instancia'

A resposta já traz a instância resolvida, a última gravação e a URL autenticada de download:

{
"data": {
"instance": {
"id": "UUID_DA_INSTANCIA",
"name": "Minha instância",
"instance_name": "nome-da-instancia"
},
"recording": {
"id": "UUID_DA_GRAVACAO",
"duration_seconds": 42,
"format": "mp3",
"download_available": true,
"download_url": "/v1/instances/UUID_DA_INSTANCIA/voice/recordings/UUID_DA_GRAVACAO/download"
}
}
}

Copie o download_url e faça o segundo GET com a mesma chave. O -L é obrigatório porque a API valida a posse e responde com um redirect 307 para o MP3:

curl -L \
--header 'Authorization: Bearer stevo_sk_SUA_CHAVE' \
'https://openapi.stevo.chat/v1/instances/UUID_DA_INSTANCIA/voice/recordings/UUID_DA_GRAVACAO/download' \
--output ligacao.mp3

Para listar o histórico, use o data.instance.id devolvido no primeiro passo:

curl --get 'https://openapi.stevo.chat/v1/instances/UUID_DA_INSTANCIA/voice/recordings' \
--header 'Authorization: Bearer stevo_sk_SUA_CHAVE' \
--data-urlencode 'limit=20' \
--data-urlencode 'has_audio=true'

Em automações, passe também call_id no endpoint latest quando várias ligações puderem terminar juntas. A gravação aparece somente após encerrar e concluir o upload; repita brevemente se a primeira tentativa responder 404. Se o nome corresponder a mais de uma instância, a API retorna 409 ambiguous_instance; nesse caso, use o UUID.

Ativar, configurar e embutir o webphone (widget)​

// Status do StevoVoice na instância SM v2: motor disponível? ativado? cota/canais
const st = await stevo.voice.status(instanceId); // { voice_available, enabled, unlimited, remaining, channels, active_calls }

// Ativar SEM cobrar (regras do painel): conta comum ativa o trial de 50 ligações;
// conta com pacote StevoVoice usa uma vaga do pacote. Pra canais/ilimitado:
await stevo.voice.enable(instanceId);
await stevo.billing.purchase({ product: 'stevovoice', instance_id: instanceId, tier: 5 }); // cartão salvo
const { checkout_url } = await stevo.billing.checkout({ product: 'stevovoice', instance_id: instanceId, tier: 5 }); // link de pagamento

// Configurações da aba StevoVoice
await stevo.voice.updateSettings(instanceId, {
summary_enabled: true, summary_emails: ['gestor@empresa.com'], // resumo da ligação por e-mail
coach_enabled: true, // coach IA durante a ligação
receive_enabled: true, // receber ligações no webphone
});

// Widget: webphone embutível no site do cliente
const w = await stevo.voice.createWidget(instanceId, { name: 'Site', allowed_origins: ['https://meusite.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)​

Configure agentes multi-provedor, funil de etapas, base de conhecimento e mais:

// Config base + chave do provedor (write-only: nunca é retornada)
await stevo.ai.updateSettings(instanceId, { enabled: true, timezone: 'America/Sao_Paulo' });
await stevo.ai.setProviderKeys(instanceId, { openai: 'sk-...' });

// Agente + funil
const agente = await stevo.ai.agents.create(instanceId, {
name: 'Atendente',
provider: 'openai',
model: 'gpt-4o-mini',
system_prompt: 'Você é um atendente cordial.',
is_primary: true,
});
await stevo.ai.stages.create(agente.id, { name: 'Boas-vindas', objective: 'Coletar o nome' });

// FAQ, follow-up e conhecimento (RAG)
await stevo.ai.faq.create(instanceId, { question: 'Qual o horário?', answer: 'Das 9h às 18h.' });
await stevo.ai.followup.set(instanceId, { enabled: true, max_followups: 3 });

10. Retry​

O SDK repete sozinho só quando é seguro:

SituaçãoComportamento padrão
429 (rate limit)repete sempre, respeitando Retry-After
5xx / falha de conexão em GET, PATCH, DELETE...repete
5xx / falha de conexão em POSTnão repete (evita duplicar compra, campanha ou envio)
POST com Idempotency-Keyrepete só se a API responder retryable: true; falha de conexão vira result_unknown
resposta com retryable: falsenunca repete, em nenhum método
timeout do próprio SDKnunca repete
const stevo = new Stevo(process.env.STEVO_API_KEY!, {
retry: {
maxAttempts: 3, // TOTAL de tentativas, incluindo a primeira
retry429: true,
retry5xx: true,
retryNetwork: true,
retryTimeout: false,
retryNonIdempotent: false, // true permite repetir POST sem chave (pode duplicar!)
},
});

Já tem fila com backoff no seu worker? Desligue o do SDK (retry: { enabled: false }) e use StevoError.retryable para decidir — assim só uma camada repete.

11. Erros e observabilidade​

Toda falha vira um 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') {
// conta sem cartão salvo — use billing.checkout ou oriente a cadastrar no painel
}
}
}
CampoO que traz
status / code / messageHTTP, código de negócio e mensagem (status: 0 = rede/timeout)
retryablese repetir faz sentido — vem da própria API quando ela informa
requestIdid de correlação (X-Request-Id) — informe ao suporte
attempt, latencyMs, retryAfter, rateLimittentativa em que falhou, duração, Retry-After e limites da key
toJSON()serializa tudo com segurança para log

Códigos comuns: not_ready (instância não 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, passe onResponse — é chamado a cada tentativa e nunca altera o 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. Atualizando da 1.x para a 2.x​

A 2.0.0 mudou alguns comportamentos para nunca duplicar um envio ou uma compra:

  • POST não é mais repetido automaticamente em 5xx nem em falha de conexão (só em 429, ou com Idempotency-Key e retryable: true da API). Quem dependia do retry implícito em billing.purchase, dispatch.createCampaign ou envios deve tratar o StevoError.
  • Timeout nunca é repetido, nem em GET (ligue com retry: { retryTimeout: true } se quiser).
  • O SDK obedece retryable: false da API, e falha de conexão com idempotencyKey vira result_unknown (consulte messages.getByIdempotencyKey).
  • messages.send e os envios de whatsapp() validam a resposta: 2xx sem corpo reconhecível vira StevoError invalid_response.
  • Envios com idempotencyKey devolvem idempotencyKey (eco) no lugar de idempotent: true.
  • Tipos do SM v2 ficaram estritos e JID agora é string (era objeto) — código com campo de tipo errado deixa de compilar.
  • createWhatsApp e createInstanceLifecycle deixaram de ser exportados — use stevo.whatsapp(id) e stevo.instances.

Na prática: npm install stevo-sdk@latest, rode o tsc e revise os catch dos seus POST.

13. Segurança​

  • Nunca use a API key no frontend. Mantenha no backend e em variável de ambiente.
  • Scopes mínimos e, se possível, key restrita às instâncias da integração.
  • Valide todo webhook de conta com constructEvent sobre o corpo cru.
  • token da instância e URLs de WebSocket (?apikey=) são segredo — não logue, não mande pro front-end.
  • Nunca acesse o banco da Stevo. Integrações externas usam só a API e o SDK: instâncias por external_ref, sincronização por webhooks de conta + listChanges, e envio com idempotencyKey.

14. Entregando pra sua IA implementar​

Se quem vai escrever a integração é uma IA (Claude, Cursor, Copilot, ChatGPT...), cole este bloco no contexto dela:

Use o pacote npm stevo-sdk (2.x, TypeScript, tipos inclusos, Node 18+). Instancie new Stevo(process.env.STEVO_API_KEY) com a key stevo_sk_... da aba API Keys do painel Stevo. Gestão: me, instances (list/get/create/update com external_ref e metadata, findByExternalRef, listChanges/iterateChanges para reconciliar com apagadas, delete com { confirm: true }, getQrCode/refreshQrCode/getConnection/disconnect/health, restart, recreateTotal, setWebhook, fuse), links, ghl, billing (purchase/checkout), ghlAgency, ai, dispatch (campanhas com instance_ids) e voice. Webhooks de conta: stevo.webhooks.create({ url, events, max_in_flight }) devolve o secret uma vez; no handler, use stevo.webhooks.constructEvent({ rawBody, signature: X-Stevo-Signature, timestamp: X-Stevo-Timestamp, secret }) sobre o corpo CRU e deduplique por event_id; webhooks.deliveries lista e reenvia entregas. Envio: stevo.whatsapp(id) (sendText/sendMedia/markRead/sendReaction/getStatus nos dois motores), stevo.messages.send(id, params, { idempotencyKey }) + messages.get/getByIdempotencyKey para status, stevo.smv2(id) (servidor SM v2, 114 operações) e stevo.oficial(id) (Cloud API Meta). O SDK resolve server_url + token sozinho. Erros são StevoError com .status, .code, .retryable e .requestId; em result_unknown/upstream_timeout consulte o status pela chave antes de reenviar. Retry padrão: 429 sempre; 5xx/rede só fora de POST; timeout nunca. Referência completa: https://tutorial.stevo.chat/public-api-reference

Referências​