npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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.md e docs/sdk-stevo.md para external_ref/metadata, reconciliação, Idempotency-Key e 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-sdk

Node 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 o OficialClient com o token que a API de gestão devolve em GET /v1/instances/{id}. Se ela responder token: null (já observado numa instância oficial conectada), stevo.oficial(id) lança 409 not_ready. Nesse caso use o proxy stevo.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 (StevoError result_unknown); o backend responde 504 upstream_timeout quando o servidor de envio não respondeu. Nos dois casos, consulte messages.getByIdempotencyKey antes de reenviar. Fluxo completo em docs/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. Resposta 201; 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), markRead e sendReaction não são suportados (409 unsupported_operation). No motor oficial, caption não vai em áudio e filename só em documento. messageId só é 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, refreshQrCode e health tentam primeiro as rotas de lifecycle da API de gestão (getConnection e health nos dois motores; QR e disconnect só 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 suporta disconnect/QR (400 unsupported_engine), e desconectar apaga as assinaturas de webhook do nó: depois de reconectar, reaplique com setWebhook — o refreshQrCode já restaura o webhook salvo por padrão (restoreWebhook: false pra não restaurar). Tutorial completo em docs/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: false no envelope — ex.: 504 upstream_timeout com Idempotency-Key), o SDK não repete e preserva retryable: false. Com Idempotency-Key, falha de conexão/timeout vira result_unknown: consulte messages.getByIdempotencyKey em 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 todas

Para 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+). Instancie new Stevo(apiKey) com a key stevo_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/update aceitam external_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 ou cloud_api cru na API Oficial); messages.get/messages.getByIdempotencyKey consultam o status (queued|sent|delivered|read|failed); para volume use smv2()/oficial();
  • whatsapp(instanceId) — camada unificada (sendText/sendMedia/markRead/sendReaction/getStatus) com engine smv2|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, com max_in_flight) e deliveries (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) e await stevo.oficial(instanceId) (Cloud API: sendMessage/listTemplates/downloadMedia/downloadMediaStream...) — o SDK resolve server_url + token da instância sozinho. Erros são StevoError com .status, .code e .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 em retry; um POST só é 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.clear e a automação ghlAgency com auto_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(...) devolve boolean (nunca lança); assert lança StevoError (webhook_signature_invalid, webhook_timestamp_expired, webhook_malformed, webhook_secret_missing); sign gera 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 com event_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 em docs/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 swagger

O 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