stevo-sdk
v2.1.1
Published
SDK oficial COMPLETO da Stevo: gestão da conta (openapi.stevo.chat) + servidor SM v2 (114 operações) + API Oficial Meta + Stevo IA — tudo em um pacote.
Maintainers
Readme
stevo-sdk
SDK oficial COMPLETO (TypeScript/JavaScript) da Stevo — tudo em um pacote, bem separado:
| Área | Namespace | O que faz |
|---|---|---|
| Gestão da conta | stevo.instances, stevo.links, stevo.ghl, stevo.billing, stevo.ghlAgency, stevo.ai, stevo.messages | API de gestão (openapi.stevo.chat): instâncias, links, GHL, compras, GHL Agência, Stevo IA e envio avulso de mensagem (proxy) |
| Servidor SM v2 | await stevo.smv2(instanceId) | fala DIRETO com o servidor da instância: enviar mensagens, chats, grupos, etiquetas, newsletter, chamadas e gravações (114 operações) |
| API Oficial Meta | await stevo.oficial(instanceId) | gateway Cloud API: mensagens, templates HSM, perfil, mídia (upload e download de mídia recebida) |
| Envio unificado | await stevo.whatsapp(instanceId) | escolhe o motor sozinho (SM v2, API Oficial ou proxy): sendText, sendMedia, markRead, sendReaction, getStatus |
| Webhooks | stevo.webhooks | valida a assinatura HMAC dos webhooks recebidos (verify/assert/sign/constructEvent); CRUD de webhooks de conta e deliveries (histórico/reenvio), com max_in_flight por destino |
🔌 Integração vindo de acesso direto ao banco da STEVO (ex.: STChat)? Veja
docs/migration-guide.mdedocs/sdk-stevo.mdparaexternal_ref/metadata, reconciliação,Idempotency-Keye webhooks de conta.
Cada instância tem seu próprio servidor + token — mas você não precisa se preocupar: o SDK resolve isso sozinho pela API de gestão.
⚠️ Este pacote substitui o
stevo-gestao(renomeado). 📚 Referência da API de gestão: https://tutorial.stevo.chat/public-api-reference 🤖 Prefere plugar uma IA sem escrever código? Use o servidor MCP:https://openapi.stevo.chat/mcp(mesma API key).
Instalação
npm install stevo-sdkNode 18+ (usa fetch nativo). Funciona em ESM e CommonJS, com tipos inclusos.
Começando
Crie uma API key na aba API Keys do painel da Stevo (a key é da conta, com scopes que você escolhe).
import { Stevo } from 'stevo-sdk';
const stevo = new Stevo('stevo_sk_...');
// Quem sou eu (scopes, rate limit)
const eu = await stevo.me.get();
// Todas as instâncias da conta
const instancias = await stevo.instances.list();
// server_url + token de cada instância = fale direto com o servidor dela
// (envio de mensagem acontece no servidor da instância, não nesta API)
const conectadas = instancias.filter((i) => i.connected);Enviando mensagens (servidor da instância)
// SM v2 — o SDK busca server_url + token da instância e conecta
const wa = await stevo.smv2(instanceId);
await wa.sendText({ body: { number: '5511999999999', text: 'Olá!' } });
await wa.sendMedia({ body: { number: '5511999999999', url: 'https://...', type: 'image', caption: 'Segue!' } });
const grupos = await wa.getGroupList();
const status = await wa.getInstanceStatus();
const saude = await wa.getInstanceHealth(); // GET sem argumentos também funciona
// API Oficial Meta — mensagens no 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();
// Prefere passar as credenciais na mão? Também dá:
const wa2 = await stevo.smv2({ serverUrl: 'https://sm-x.stevo.chat', token: 'apikey-da-instancia' });[!note]
stevo.oficial(id)depende do token da instância O SDK monta oOficialClientcom otokenque a API de gestão devolve emGET /v1/instances/{id}. Se ela respondertoken: null(já observado numa instância oficial conectada),stevo.oficial(id)lança409 not_ready. Nesse caso use o proxystevo.messages.send(...)(funciona sem credencial) ou passe a credencial na mão:stevo.oficial({ token }).
Envio avulso pelo proxy stevo.messages
Se você só precisa mandar uma mensagem (e não quer montar o client do motor), a API de gestão tem um proxy: stevo.messages.send(instanceId, params) (POST /v1/instances/{id}/messages, scope messages:send). O servidor resolve server_url/token e envia pela instância — funciona tanto para SM v2 quanto para API Oficial.
const r = await stevo.messages.send(instanceId, { to: '5511999999999', text: 'Olá!' });
console.log(r.engine, r.sent);
// Mídia (media_type: 'image' | 'video' | 'audio' | 'document')
await stevo.messages.send(instanceId, {
to: '5511999999999',
media_url: 'https://...',
media_type: 'image',
caption: 'Segue!',
});
// Só na API Oficial: dá pra mandar um payload Cloud API cru
await stevo.messages.send(instanceIdOficial, {
to: '5511999999999',
cloud_api: { type: 'template', template: { name: 'hello_world', language: { code: 'pt_BR' } } },
});
// Idempotency-Key: repetir a MESMA chave com o MESMO corpo não duplica o
// envio — devolve a resposta original (`replayed: true`).
const r2 = await stevo.messages.send(instanceId, { to: '5511999999999', text: 'Olá!' }, { idempotencyKey: 'pedido-9f21-1' });
// Status da mensagem: queued → sent → delivered → read, ou failed
const status = await stevo.messages.get(instanceId, r2.message_id!);
const porChave = await stevo.messages.getByIdempotencyKey(instanceId, 'pedido-9f21-1');[!note] Timeout não significa "não enviou" A chave vale por 24h, por instância. Em falha de conexão/timeout com
idempotencyKey, o SDK não repete sozinho (StevoErrorresult_unknown); o backend responde504 upstream_timeoutquando o servidor de envio não respondeu. Nos dois casos, consultemessages.getByIdempotencyKeyantes de reenviar. Fluxo completo emdocs/sdk-stevo.md.
[!note] Proxy × client direto O proxy é para integrações leves: uma mensagem por chamada e conta no rate limit da sua API key. Para volume, disparo em massa ou recursos avançados (chats, grupos, templates, mídia recebida), use
stevo.smv2(instanceId)/stevo.oficial(instanceId)— o client fala direto com o motor da instância. Resposta201; se a instância não estiver conectada,409 not_ready.
WhatsApp unificado — stevo.whatsapp(id)
Se você não quer escolher entre SM v2 e API Oficial, stevo.whatsapp(instanceId) resolve o motor sozinho e normaliza as operações básicas. A propriedade engine diz qual caminho está em uso: 'smv2', 'official' ou 'proxy' (API Oficial sem token, que cai no proxy da gestão).
const wa = await stevo.whatsapp(instanceId);
console.log(wa.engine); // 'smv2' | 'official' | 'proxy'
const r = await wa.sendText({ to: '5511999999999', text: 'Olá!' }); // { engine, messageId?, raw }
await wa.sendMedia({ to: '5511999999999', url: 'https://...', type: 'image', caption: 'Segue!' });
await wa.markRead({ messageId, chat: '5511999999999' });
await wa.sendReaction({ messageId, chat: '5511999999999', emoji: '👍', fromMe: true });
const st = await wa.getStatus(); // { engine, connected, loggedIn?, raw }[!note] Limites por motor No modo
proxy(API Oficial sem token),markReadesendReactionnão são suportados (409 unsupported_operation). No motor oficial,captionnão vai em áudio efilenamesó em documento.messageIdsó é preenchido no motor oficial — a resposta do SM v2 ainda não foi observada.
Disparo em massa e voz (via API de gestão)
// Campanha de disparo — passe instance_ids (credenciais resolvidas internamente)
const camp = await stevo.dispatch.createCampaign({
instance_ids: [instanceId],
name: 'Promo Julho',
messages: ['Olá {{name}}! Temos novidades 🎉'],
recipients: [{ phone: '5511999999999', name: 'Maria' }],
start: true,
});
await stevo.dispatch.getCampaign(camp.campaign_id);
await stevo.dispatch.pause(camp.campaign_id);
// StevoVoice — chamada de voz com IA (requer StevoVoice assinado na instância)
await stevo.voice.scheduleCall(instanceId, { to_number: '5511999999999', agent_id: 'agent_xyz' });
await stevo.voice.batchCalls(instanceId, { numbers: ['5511...', '5511...'], agent_id: 'agent_xyz', interval_seconds: 60 });
// Gravações das ligações nativas da instância (scope voice:read)
const page = await stevo.voice.listRecordings(instanceId, {
limit: 50,
direction: 'outbound',
has_audio: true,
});
const recording = await stevo.voice.getRecording(instanceId, page.recordings[0].id);
const response = await stevo.voice.downloadRecording(instanceId, recording.id);
const mp3 = await response.arrayBuffer();
// Atalho para automação pós-ligação — aceita UUID, nome ou instance_name
const latest = await stevo.voice.getLatestRecording('well-pessoal-teste', {
call_id: 'id-da-chamada', // opcional, mas evita ambiguidade
});O client SM v2 tem 114 operações geradas do swagger oficial (12 grupos: Send Message, Chat, Group, Label, Newsletter, User, Instance, Community, Call, Voice, Message, Chatwoot) — todas com JSDoc. Referência: https://tutorial.stevo.chat/api-reference
Upload multipart, WebSocket e tipos (SM v2)
Uploads são multipart/form-data: monte um FormData (o fetch define o Content-Type/boundary sozinho):
// Campanha de fake call (POST /voice/broadcast): audio (obrigatório), numbers, delay_seconds...
const form = new FormData();
form.set('audio', new Blob([await fs.promises.readFile('audio.ogg')]));
form.set('numbers', '5511999999999,5511888888888');
await wa.voiceBroadcast({ form });
// Injetar áudio numa chamada ativa (POST /voice/call/{callId}/audio)
const audio = new FormData();
audio.set('audio', new Blob([bytes]));
await wa.voiceCallAudio({ params: { callId }, form: audio });Endpoints WebSocket não passam por fetch: o método devolve a URL ws(s):// já autenticada com ?apikey=, pronta pra abrir com new WebSocket(url):
const url = wa.getVoiceCallWs({ params: { callId } }); // wss://.../voice/call/<callId>/ws?apikey=<token>
const ws = new WebSocket(url);Os payloads do swagger viram tipos Smv2* exportados pelo pacote (ex.: Smv2MediaStruct, Smv2CallInfo):
import type { Smv2MediaStruct } from 'stevo-sdk';
const body: Smv2MediaStruct = { number: '5511999999999', url: 'https://...', type: 'image', caption: 'Olá' };
await wa.sendMedia({ body });Mídia recebida na API Oficial (download em stream)
downloadMedia carrega o arquivo inteiro na memória; pra vídeos/documentos grandes use downloadMediaStream, que devolve o Response cru pra streaming:
const res = await meta.downloadMediaStream(webhook.video.id);
await pipeline(Readable.fromWeb(res.body!), fs.createWriteStream('video.mp4'));Instâncias
// Criar (provisiona um SLOT LIVRE da conta — não compra slot novo)
const nova = await stevo.instances.create({ name: 'minha-instancia' });
// engine 'official' (API Oficial Meta) devolve onboarding_url pra abrir no navegador:
const oficial = await stevo.instances.create({ engine: 'official' });
// external_ref/metadata: amarre a instância ao seu tenant, sem guardar o UUID
const comRef = await stevo.instances.create({ name: 'loja', external_ref: 'WSP-000123' });
await stevo.instances.update(comRef.instance.id, { metadata: { plano: 'pro' } }); // metadata SUBSTITUI o objeto inteiro
const minha = await stevo.instances.findByExternalRef('WSP-000123'); // Instance | null
// Reconciliação: o que mudou desde a última sincronização, incl. apagadas
for await (const pagina of stevo.instances.iterateChanges('2026-09-01T00:00:00Z')) {
console.log(pagina.data.length, 'mudaram,', pagina.deleted.length, 'apagadas');
}
// Apagar (DESTRUTIVO: exige confirm: true, senão o SDK recusa antes do fetch)
await stevo.instances.delete(comRef.instance.id, { confirm: true });
// Reiniciar (recriar/reconectar — leia o QR depois)
await stevo.instances.restart(id);
// Recriar Total (DESTRUTIVO: zera tudo, vira slot vazio)
await stevo.instances.recreateTotal(id);
// Em massa (até 50; falha em uma não interrompe as demais)
const lote = await stevo.instances.recreateTotalBatch([id1, id2, id3]);
console.log(`${lote.succeeded} ok, ${lote.failed} falharam`);
// Configurações (whitelist de campos)
await stevo.instances.updateSettings(id, { group_view: true });
// Webhook da instância (o mesmo de Configurações → Webhook no painel)
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; `events` é ignorado)
const wh = await stevo.instances.getWebhook(id); // { engine, url, events }
await stevo.instances.deleteWebhook(id);
// Fusão: liga a instância API Oficial à SM v2 do MESMO número (STChat/transmissor/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
// Lifecycle (hoje composto sobre o servidor SM v2)
await stevo.instances.disconnect(id, { logout: true });
const conn = await stevo.instances.getConnection(id); // { engine, connected, loggedIn?, name?, raw }
const qr = await stevo.instances.getQrCode(id); // { raw }
const novo = await stevo.instances.refreshQrCode(id); // { raw, webhookRestored }
const saude = await stevo.instances.health(id); // { engine, connected, raw }[!warning] Lifecycle: rotas da API de gestão, com fallback para SM v2
disconnect,getConnection,getQrCode,refreshQrCodeehealthtentam primeiro as rotas de lifecycle da API de gestão (getConnectionehealthnos dois motores; QR edisconnectsó no SM v2). Contra um backend sem essas rotas, uma sonda única (GET /v1/instances/{id}) distingue "rota não existe" de "instância não existe", e o SDK cai sozinho no caminho antigo, composto sobre o servidor SM v2 (src/lifecycle.ts) — decisão que fica em memória para a vida deste client. Nesse caminho antigo, a API Oficial não suportadisconnect/QR (400 unsupported_engine), e desconectar apaga as assinaturas de webhook do nó: depois de reconectar, reaplique comsetWebhook— orefreshQrCodejá restaura o webhook salvo por padrão (restoreWebhook: falsepra não restaurar). Tutorial completo emdocs/sdk-stevo.md.
Links, GHL e Billing
// Links de acesso
const wl = await stevo.links.whiteLabel(id, { permanent: true });
const direto = await stevo.links.directAccess(id);
// GHL da instância
const status = await stevo.ghl.status(id);
await stevo.ghl.connect(id, { mode: 'oauth' }); // devolve oauth_url
await stevo.ghl.disconnect(id); // idempotente
// Billing — compra com o CARTÃO SALVO (off-session, cobrança real!)
const catalogo = await stevo.billing.plans();
if (catalogo.has_saved_card) {
await stevo.billing.purchase({ plan: 'stevo3' });
// StevoVoice (voz com IA, por instância):
await stevo.billing.purchase({ product: 'stevovoice', instance_id: id, tier: 5 });
}
// Sem cartão salvo (ou pra mandar o link pro cliente final): checkout hospedado da Stripe.
// Não cobra na hora — devolve a URL; após pagar, o provisionamento é automático.
const { checkout_url } = await stevo.billing.checkout({ plan: 'stevo3' });
const voz = await stevo.billing.checkout({ product: 'stevovoice', instance_id: id, tier: 5, success_url: 'https://meusite.com/obrigado' });StevoVoice na instância (ativar, configurar, widget)
// Status: motor disponível no servidor? ativado? cota/canais/ligações ativas
const st = await stevo.voice.status(id); // { voice_available, enabled, unlimited, remaining, channels, ... }
// 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, compre acima.
await stevo.voice.enable(id);
// Configurações da aba StevoVoice
await stevo.voice.updateSettings(id, {
summary_enabled: true, summary_emails: ['[email protected]'],
coach_enabled: true, receive_enabled: true,
});
// Widget: webphone embutível no site do cliente
const w = await stevo.voice.createWidget(id, { 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);GHL Agência (monitor de assinatura SaaS)
Monitore a assinatura SaaS (no GHL) de cada location conectado às suas instâncias, com ações automáticas:
// Cadastrar sua agência GHL (token privado a nível de agência)
await stevo.ghlAgency.createAgency({
name: 'Minha Agência',
company_id: 'X5KhNv...',
agency_token: 'pit-...',
});
// Webhook + verificação diária + desconectar GHL sozinho após 7 dias pausado
await stevo.ghlAgency.updateSettings({
webhook_url: 'https://meu-sistema.com/hook',
cron_interval_hours: 24,
auto_disconnect_ghl: true,
auto_action_after_days: 7,
});
// Verificar agora
const { results } = await stevo.ghlAgency.runCheck();
const pausadas = results.filter((r) => r.is_problem);Erros
Toda falha vira StevoError com status (HTTP) e code (negócio):
import { StevoError } from 'stevo-sdk';
try {
await stevo.billing.purchase({ plan: 'stevo3' });
} catch (e) {
if (e instanceof StevoError) {
if (e.code === 'no_saved_card') {
// conta sem cartão — mandar cadastrar no painel
}
console.error(e.status, e.code, e.message);
}
}Códigos comuns: unauthorized (401), insufficient_scope (403), not_found (404 — recurso de outra conta também), rate_limited (429, com retry automático), no_available_slot (409), no_saved_card / card_declined (402), partner_only / special_access_required (403), already_subscribed (409). Instâncias, webhooks de conta e envio idempotente: external_ref_conflict / key_restricted (instâncias), webhook_limit_reached / instance_restricted_key (webhooks de conta), idempotency_conflict / idempotency_in_progress (envio) e not_supported (501, backend sem reconciliação).
Além de status/code/message, o StevoError traz retryable, requestId, attempt, retryAfter, rateLimit, latencyMs, instanceId, method/path e toJSON() — tudo seguro para log (sem credenciais):
if (e instanceof StevoError) {
logger.warn(e.toJSON()); // { status, code, retryable, requestId, attempt, ... }
if (e.retryable) { /* sua fila decide se repete */ }
}retryable diz se repetir faz sentido e leva o método em conta: 429 sempre; 0 (rede/timeout) e 5xx só em métodos idempotentes (GET/DELETE/PUT/PATCH); um POST só é repetível com retry.retryNonIdempotent ligado ou com o header Idempotency-Key e retryable: true confirmado pela API (ver messages.send). Outros 4xx nunca. O SDK envia X-Request-Id em toda chamada e usa o x-request-id da resposta quando existir.
O SDK tenta de novo automaticamente em 429 (respeitando Retry-After), sempre; e em 5xx ou falha de conexão somente em métodos não-POST — POST (compra, campanha, envio) nunca é reenviado, pra não duplicar efeito. Timeout próprio nunca é reenviado, nem em GET (se o servidor está pendurado, repetir só multiplicaria a espera). Falha de conexão ou timeout vira StevoError com code: 'network_error' e status: 0 (mensagem timeout após <N>ms no timeout); falha ao ler o body de uma resposta OK também. Essa política vale para os três clients (gestão, smv2 e oficial) e é ajustável em retry — ver Opções.
Se o servidor classificar o erro como não repetível (
retryable: falseno envelope — ex.:504 upstream_timeoutcomIdempotency-Key), o SDK não repete e preservaretryable: false. ComIdempotency-Key, falha de conexão/timeout viraresult_unknown: consultemessages.getByIdempotencyKeyem vez de reenviar.
Opções
const stevo = new Stevo('stevo_sk_...', {
baseUrl: 'https://openapi.stevo.chat', // default
timeoutMs: 60_000, // por request
maxRetries: 2, // legado: 2 repetições = 3 tentativas
retry: { // regras finas de retry
maxAttempts: 3, // TOTAL de tentativas (inclui a 1ª)
retryTimeout: false, // timeout nunca repete (default)
},
onResponse: (info) => metrics.observe(info), // observa cada tentativa
credentialsCacheMs: 0, // 0 = sem cache (default)
});retry (RetryOptions): enabled (default true), maxAttempts (total, default 3), retry429, retry5xx e retryNetwork (default true), retryTimeout e retryNonIdempotent (default false). O maxRetries legado equivale a maxAttempts = maxRetries + 1. Pra desligar todo o retry (deixar sua fila no controle): retry: { enabled: false }.
onResponse recebe um ResponseInfo a cada tentativa (method, path sem query string, status, ok, attempt, maxAttempts, latencyMs, requestId, retryAfter?, rateLimit?, willRetry, errorCode?). É passivo: nunca altera o resultado da chamada.
credentialsCacheMs guarda por quanto tempo as credenciais resolvidas por smv2(id)/oficial(id) (server_url + token) ficam em cache — útil ao criar muitos clients da mesma instância. Chamadas concorrentes à mesma instância compartilham um único GET; instância ainda sem credenciais (sem token, ou SM v2 sem server_url) não é cacheada, pra o 409 not_ready não ficar preso até o TTL. O cache é invalidado sozinho em 401 do servidor da instância e em restart/recreateTotal/recreateTotalBatch feitos por este client. Pra limpar na mão:
stevo.clearCredentialsCache(instanceId); // de uma instância
stevo.clearCredentialsCache(); // de todasPara IAs (Claude, Cursor, Copilot...)
Cole isto no contexto da sua IA e ela implementa a integração sozinha:
Use o pacote npm
stevo-sdk(TypeScript, tipos inclusos, Node 18+). Instancienew Stevo(apiKey)com a keystevo_sk_...da aba API Keys do painel Stevo. Recursos:
me.get— identidade da key (scopes, rate limit);instances— list/get/create/update/listChanges/iterateChanges/findByExternalRef/delete/restart/recreateTotal/recreateTotalBatch/officialOnboarding/getSettings/updateSettings/getWebhook/setWebhook/deleteWebhook/getFusion/fuse/unfuse/disconnect/getConnection/getQrCode/refreshQrCode/health;create/updateaceitamexternal_ref/metadata;links.whiteLabel/directAccess;ghl.status/connect/disconnect;billing.plans/purchase/checkout(purchase cobra no cartão salvo; checkout gera link Stripe);ghlAgency.overview/createAgency/updateAgency/deleteAgency/updateSettings/runCheck;ai— overview/updateSettings/setProviderKeys + sub-namespaces agents/stages/ghlConfig/faq/tools/followup/documents/conversations;messages.send(instanceId, params, { idempotencyKey? })— proxy da API de gestão para enviar UMA mensagem pela instância (texto, mídia oucloud_apicru na API Oficial);messages.get/messages.getByIdempotencyKeyconsultam o status (queued|sent|delivered|read|failed); para volume usesmv2()/oficial();whatsapp(instanceId)— camada unificada (sendText/sendMedia/markRead/sendReaction/getStatus) comenginesmv2|official|proxy;webhooks— verify/assert/sign/parse/constructEvent da assinatura HMAC dos webhooks recebidos; CRUD de webhooks de conta (create/list/get/update/delete/rotateSecret, commax_in_flight) edeliveries(list/get/retry/iterate);dispatch— createCampaign/listCampaigns/getCampaign/start/pause/resume/cancel;voice— status/enable/disable/getSettings/updateSettings/listWidgets/createWidget/getWidget/updateWidget/deleteWidget/scheduleCall/batchCalls/listScheduled/cancelCall/executeCall/queue/listRecordings/getLatestRecording/getRecording/downloadRecording;await stevo.smv2(instanceId)(servidor SM v2: sendText/sendMedia/getGroupList... 114 ops) eawait stevo.oficial(instanceId)(Cloud API: sendMessage/listTemplates/downloadMedia/downloadMediaStream...) — o SDK resolve server_url + token da instância sozinho. Erros sãoStevoErrorcom.status,.codee.retryable(erro de rede:.code === 'network_error',.status === 0). Retry padrão: 429 sempre; 5xx/falha de conexão só em métodos não-POST; timeout nunca — configurável emretry; umPOSTsó é repetível em 429. Referência completa: https://tutorial.stevo.chat/public-api-reference
Segurança
Instance.tokené credencial forte: autentica direto no servidor da instância (SM v2) ou no gateway da API Oficial. Não logue, não versiona e não envie ao front-end — use só no backend.- As URLs de WebSocket devolvidas por
wa.getVoiceCallWs(...)e afins carregam a apikey em?apikey=; trate-as como segredo (não registre em log nem compartilhe). - Operações destrutivas (sem desfazer):
instances.recreateTotal,instances.recreateTotalBatch,stevo.ai.conversations.cleare a automaçãoghlAgencycomauto_recreate_total: true.
Webhooks — validar assinatura
Todo webhook assinado traz X-Stevo-Signature: sha256=HMAC-SHA256(secret, timestamp + "." + corpoCru). Valide sempre sobre o corpo cru (antes de JSON.parse):
import express from 'express';
import { Stevo, StevoError } from 'stevo-sdk';
const stevo = new Stevo('stevo_sk_...');
const app = express();
app.post('/webhooks/stevo', express.raw({ type: 'application/json' }), (req, res) => {
try {
stevo.webhooks.assert({
rawBody: req.body, // Buffer cru — NÃO use express.json() aqui
signature: req.header('x-stevo-signature'),
timestamp: req.header('x-stevo-timestamp'),
secret: process.env.STEVO_WEBHOOK_SECRET!,
});
} catch (e) {
return res.status(e instanceof StevoError ? e.status : 401).end();
}
const event = JSON.parse(req.body.toString('utf8'));
// deduplique por event_id (entrega "pelo menos uma vez") e processe
res.status(200).end();
});stevo.webhooks.verify(...)devolveboolean(nunca lança);assertlançaStevoError(webhook_signature_invalid,webhook_timestamp_expired,webhook_malformed,webhook_secret_missing);signgera assinatura para fixtures de teste.- Tolerância de replay de 5 minutos; para rotação de segredo, passe uma lista em
secret(a assinatura vale se bater com qualquer um). - HMAC-SHA256 em TypeScript puro (sem
node:crypto), então roda também em Edge/Workers.
Webhooks de conta (CRUD e deliveries)
Além da validação, o SDK também gerencia webhooks de conta (todas as instâncias, envelope assinado) e o histórico de entregas:
const criado = await stevo.webhooks.create({ url: 'https://meu-sistema.com/hooks', events: ['message.received', 'instance.connected'], max_in_flight: 16 }); // 1–64 entregas simultâneas (default 8)
console.log(criado.secret); // whsec_... — só vem na criação/rotateSecret
await stevo.webhooks.deliveries.retry(deliveryId); // reenvia uma entrega failed/exhausted (mesmo event_id, attempt++)
for await (const p of stevo.webhooks.deliveries.iterate({ status: 'failed' })) { /* ... */ }[!note] Webhook de conta × webhook da instância O webhook de conta chega assinado (headers
X-Stevo-*, envelope comevent_id/attempt), com até 8 tentativas em ~21h. O webhook da instância (stevo.instances.setWebhook) continua existindo, no formato nativo do motor e sem assinatura. Guia completo emdocs/sdk-stevo.md.
Desenvolvimento
npm test # testes com fetch mockado (node:test) contra o build (128 testes)
npm run smoke # checagem real, SOMENTE LEITURA, contra a API da Stevo
npm run check:api # confere as rotas do SDK contra a spec pública openapi.yaml
npm run gen # regenera o client SM v2 a partir do swaggerO smoke precisa das variáveis STEVO_API_KEY (obrigatória) e STEVO_INSTANCE_ID (opcional; se informada, também testa o client da instância — qual usar é decidido por is_official_api). Ele só faz me.get, instances.list, billing.plans e o status da instância: não cria, não cobra e não envia nada.
O check:api baixa https://openapi.stevo.chat/openapi.yaml (ou usa um arquivo local passado como argumento) e compara as rotas com src/client.ts, saindo com código 1 se alguma rota da spec estiver ausente no SDK. Hoje o resultado é zero ausências nos dois sentidos.
No CI (.github/workflows/ci.yml) a suíte roda em matriz de Node 18/20/22/24 (todas bloqueiam), junto com typecheck, build, smoke do dist e npm audit --omit=dev como gate; a publicação no npm só acontece em push/dispatch na main, num job isolado (environment: npm, proveniência quando o repositório é público).
Licença
MIT © Stevo
