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:
| Área | Como acessa | O que faz |
|---|---|---|
| Gestão da conta | stevo.me, stevo.instances, stevo.links, stevo.ghl, stevo.billing, stevo.ghlAgency | instâncias (criar, conectar, reconciliar, apagar), links de acesso, GHL, compras e GHL Agência |
| Webhooks de conta | stevo.webhooks | um endpoint recebe os eventos de todas as instâncias, assinados com HMAC-SHA256, com histórico e reenvio |
| Envio de mensagem | stevo.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.ai | agentes, funil, chaves de provedor, FAQ, tools, follow-up, RAG, memória |
| Disparo em massa | stevo.dispatch | campanhas de WhatsApp com rotação de instâncias, agendamento e variações |
| StevoVoice | stevo.voice | chamadas 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
- Webhooks de conta assinados, com entrega confiável, deduplicação por
event_id, histórico e reenvio (stevo.webhooks) — emax_in_flightpara controlar a vazão (2.1.0). - Envio seguro:
Idempotency-Keyno envio e status da mensagem (queued→sent→delivered→read/failed). - Instâncias amarradas ao seu sistema:
external_refemetadata, reconciliação porlistChangese exclusão comdelete. - 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.
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.
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ção | Default | Observação |
|---|---|---|
baseUrl | https://openapi.stevo.chat | — |
timeoutMs | 60000 | timeout por requisição |
retry | ver Retry | regras de repetição automática |
maxRetries | 2 | legado — repetições extras (maxRetries: 2 = maxAttempts: 3) |
onResponse | — | hook chamado a cada tentativa (métricas/logs) |
credentialsCacheMs | 0 | cache 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' });
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' });
metadataemupdatesubstitui o objeto (não faz merge) — leia o atual antes de mudar um campo só.external_refrepetido na conta →409 external_ref_conflict.external_refemetadatatambé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 }.iterateChangessegue o cursor sozinho.- Deduplique
deletedporid+deleted_at. Se o mesmoidreaparecer emdatacomupdated_atdepois dodeleted_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
2xxem 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_reasondiz o motivo (gone/too_many_failures). Reative comupdate(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).
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.
event_idUma 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:
| Caminho | Quando 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.
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ção | Comportamento 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 POST | não repete (evita duplicar compra, campanha ou envio) |
POST com Idempotency-Key | repete só se a API responder retryable: true; falha de conexão vira result_unknown |
resposta com retryable: false | nunca repete, em nenhum método |
| timeout do próprio SDK | nunca 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
}
}
}
| Campo | O que traz |
|---|---|
status / code / message | HTTP, código de negócio e mensagem (status: 0 = rede/timeout) |
retryable | se repetir faz sentido — vem da própria API quando ela informa |
requestId | id de correlação (X-Request-Id) — informe ao suporte |
attempt, latencyMs, retryAfter, rateLimit | tentativa 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:
POSTnão é mais repetido automaticamente em5xxnem em falha de conexão (só em429, ou comIdempotency-Keyeretryable: trueda API). Quem dependia do retry implícito embilling.purchase,dispatch.createCampaignou envios deve tratar oStevoError.- Timeout nunca é repetido, nem em
GET(ligue comretry: { retryTimeout: true }se quiser). - O SDK obedece
retryable: falseda API, e falha de conexão comidempotencyKeyviraresult_unknown(consultemessages.getByIdempotencyKey). messages.sende os envios dewhatsapp()validam a resposta:2xxsem corpo reconhecível viraStevoErrorinvalid_response.- Envios com
idempotencyKeydevolvemidempotencyKey(eco) no lugar deidempotent: true. - Tipos do SM v2 ficaram estritos e
JIDagora éstring(era objeto) — código com campo de tipo errado deixa de compilar. createWhatsAppecreateInstanceLifecycledeixaram de ser exportados — usestevo.whatsapp(id)estevo.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
constructEventsobre o corpo cru. tokenda 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 comidempotencyKey.
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+). Instancienew Stevo(process.env.STEVO_API_KEY)com a keystevo_sk_...da aba API Keys do painel Stevo. Gestão:me,instances(list/get/create/update comexternal_refemetadata, 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) evoice. Webhooks de conta:stevo.webhooks.create({ url, events, max_in_flight })devolve osecretuma vez; no handler, usestevo.webhooks.constructEvent({ rawBody, signature: X-Stevo-Signature, timestamp: X-Stevo-Timestamp, secret })sobre o corpo CRU e deduplique porevent_id;webhooks.deliverieslista e reenvia entregas. Envio:stevo.whatsapp(id)(sendText/sendMedia/markRead/sendReaction/getStatus nos dois motores),stevo.messages.send(id, params, { idempotencyKey })+messages.get/getByIdempotencyKeypara status,stevo.smv2(id)(servidor SM v2, 114 operações) estevo.oficial(id)(Cloud API Meta). O SDK resolveserver_url+tokensozinho. Erros sãoStevoErrorcom.status,.code,.retryablee.requestId; emresult_unknown/upstream_timeoutconsulte 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
- 📘 API de Gestão — guia de integração (HTTP puro, qualquer linguagem)
- 📖 Referência da API de Gestão (endpoints, erros e scopes)
- 📖 API StevoManager v2 e API Oficial
- 📦 Pacote no npm
- 🤖 Servidor MCP:
https://openapi.stevo.chat/mcp(mesma API key)