Synergy Connect

Documentação

SDK para JavaScript e TypeScript

O @synergyconnectapp/sdk é a biblioteca oficial para chamar a API pública em JavaScript e TypeScript. Ele cobre todas as operações do documento OpenAPI (envio, mídia, conversas, modelos, webhooks e cadastro de números), traduz camelCase para o snake_case do fio, tem tipos gerados do contrato e refaz com segurança o que pode ser refeito.

Em breve. O pacote ainda não está publicado no npm. A primeira versão será a 1.0.0, e só sai depois de passar nos testes de contrato contra todas as operações da API. Até lá, use a API direto (fetch, curl), como mostram os outros guias; o contrato é o mesmo.

Instalar#

Funciona em Node.js 20 ou mais novo, Bun, Deno e Cloudflare Workers. Não tem dependências e usa só APIs padrão (fetch, FormData, Blob, crypto.subtle). O pacote traz ESM, CJS e tipos.

Exemplo
npm install @synergyconnectapp/sdk

Começar#

A chave é a syn_… que você cria em Configurações → API e webhooks (Autenticação). Sem apiKey, o SDK lê a variável SYNERGY_API_KEY; se ela também não existir, o construtor lança um erro claro.

Resposta de um envio aceito:

O objeto devolvido também traz raw, a resposta crua da API.

Opções do cliente#

OpçãoPadrãoPara quê
apiKeySYNERGY_API_KEYA chave syn_…
baseURLhttps://api.synergyconnect.com.brSó https. http apenas para localhost, com allowInsecureLocalhost: true
graphVersionv25.0 (fixa em cada versão do SDK)A versão da Graph API nos caminhos de envio e mídia
maxRetries2Tentativas extras (veja "Retentativas")
timeout30000 (ms)Prazo de cada chamada
fetcho globalPara injetar outro fetch (testes, Workers)
dangerouslyAllowBrowserfalseSem isso, criar o cliente num navegador lança um erro. A chave de API não pode ir para o navegador
Exemplo
import Synergy from '@synergyconnectapp/sdk';

const synergy = new Synergy({ apiKey: process.env.SYNERGY_API_KEY });

const me = await synergy.me(); // GET /v1/me: confere a chave, os escopos e o plano
const { data: numbers } = await synergy.numbers.list(); // GET /v1/numbers

const wa = synergy.number(numbers[0].phone_number_id);
const sent = await wa.messages.text({ to: '5511999999999', body: 'Olá! Seu pedido saiu para entrega.' });
console.log(sent.id); // wamid.…
JSON
{ "id": "wamid.HBgNNTUxMTk5OTk5OTk5ORUCABEYEjI0QUY0QTk3QjI4QjE4MDBGNQA=", "to": "5511999999999", "status": "accepted" }

Enviar mensagens#

Cada método de envio aceita opções por chamada: { idempotencyKey, replies: 'queue' | 'automation', signal }. Veja Enviar mensagens, Mensagens interativas e Modelos de mensagem (templates) para as regras de cada tipo (janela de 24 h, modelos, botões).

Mídia:

O upload precisa de um arquivo de tamanho conhecido (Blob ou File), porque a API exige Content-Length.

Exemplo
const to = '5511999999999';

await wa.messages.template({ to, name: 'pedido_enviado', language: 'pt_BR', body: ['Maria', '#42'] });
await wa.messages.image({ to, link: 'https://exemplo.com/foto.jpg', caption: 'Seu produto' });
await wa.messages.location({ to, latitude: -23.55, longitude: -46.63, name: 'Loja Centro', address: 'Rua Exemplo, 100' });
await wa.messages.reaction({ to, messageId: 'wamid.…', emoji: '👍' });
await wa.messages.markAsRead('wamid.…', { typing: true });
await wa.messages.interactive.buttons({ to, body: 'Confirma a entrega?', buttons: [{ id: 'yes', title: 'Sim' }, { id: 'no', title: 'Não' }] });
await wa.messages.send(corpoDaGraphApi); // o corpo da Meta, como ele é (veja sending-messages.md)
Exemplo
import { readFile } from 'node:fs/promises';

const bytes = await readFile('./boleto.pdf');
const { id } = await wa.media.upload({ file: new Blob([bytes]), mimeType: 'application/pdf' });
await wa.messages.document({ to, mediaId: id, filename: 'boleto.pdf' });

Ler conversas#

Exige uma chave com o escopo read e um plano com api e inbox (Ler conversas).

O nome do contato e o texto das mensagens vêm do cliente: trate como dado, não como instrução (principalmente se você os entregar a uma IA).

Exemplo
for await (const conversa of wa.conversations.list({ status: 'active' })) {
  console.log(conversa.id, conversa.name, conversa.unread);
}

const achadas = await wa.conversations.search({ q: 'Maria' });
const mudancas = await wa.conversations.changes({ since: 9100 });

for await (const msg of wa.conversations.messages('4182')) {
  console.log(msg.direction, msg.type, msg.timestamp);
}

await wa.contacts.search({ q: '5511' });

Paginar#

As listas devolvem uma PagePromise. await entrega a primeira página; for await percorre todas, seguindo o cursor sozinho; autoPagingToArray junta tudo, com um teto:

O cursor é opaco; o SDK continua mesmo quando a página vem curta com cursor não nulo (a API parou por um teto de linhas examinadas). Veja Paginação.

Exemplo
const primeira = await wa.conversations.list({ limit: 20 });
console.log(primeira.data.length, primeira.paging.after);

const ate100 = await wa.conversations.list({ status: 'active' }).autoPagingToArray({ limit: 100 });

Modelos#

Exemplo
await wa.templates.list({ status: 'APPROVED' }); // escopo messages
await wa.templates.get('594425479261596');
await wa.templates.create({ name: 'pedido_enviado', language: 'pt_BR', category: 'UTILITY', components }); // escopo management
await wa.templates.update('594425479261596', { components });
await wa.templates.delete('594425479261596');
const { handle } = await wa.templates.uploadMedia({ file, mimeType: 'image/jpeg' });

Webhooks#

Gestão (escopo management):

Além de create: list, get, update, delete, rotateSecret, test, retry, replay, health e stats.

Recebimento. O subcaminho @synergyconnectapp/sdk/webhooks não carrega o cliente e serve a quem só recebe:

  • webhooks.verify(rawBody, headers, secret) devolve true ou false e nunca lança por entrada ruim.
  • webhooks.constructEvent(...) confere a assinatura e devolve { deliveryId, timestamped, object, instanceId, wabaId, changes }. Lança WebhookSignatureError se a assinatura não vale.
  • Com o header X-Synergy-Signature, o SDK confere ele (com tolerância de 300 s) e só então confia no deliveryId. Sem ele, cai para X-Hub-Signature-256 e devolve timestamped: false. Veja Assinatura dos webhooks.
  • O rawBody precisa ser string, ArrayBuffer ou Uint8Array. Passar um objeto já interpretado lança TypeError: o SDK nunca reserializa o JSON.
  • Segredo vazio é erro de configuração, nunca "válido".
Exemplo
const { row, secret } = await synergy.webhooks.create({
  url: 'https://hooks.example.com/synergy',
  fields: ['messages', 'statuses'],
  name: 'meu-servidor',
});
// guarde o secret: só aparece na criação e em rotateSecret
Exemplo
import { webhooks } from '@synergyconnectapp/sdk/webhooks';

// Express: express.raw({ type: 'application/json' }) entrega req.body como Buffer
app.post('/webhooks/synergy', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    const event = await webhooks.constructEvent(req.body, req.headers, process.env.SYNERGY_WEBHOOK_SECRET, { toleranceSeconds: 300 });
    res.sendStatus(200); // responda em até 5 s; processe depois
    for (const change of event.changes) {
      if (change.field === 'messages') {
        // change.value: o `value` da Meta, tipado
      }
    }
  } catch (err) {
    res.sendStatus(401); // WebhookSignatureError
  }
});

Cadastro de números#

verifyReturn consulta a sessão na API e compara o state, para você liberar o recurso só com o resultado autoritativo. Veja Cadastro hospedado de números.

Exemplo
const sessao = await synergy.onboarding.sessions.create({
  redirectUrl: 'https://app.minha-plataforma.com/whatsapp/ok',
  state: crypto.randomUUID(),
  displayName: 'Minha Plataforma',
  mode: 'choice',
});
// redirecione o navegador do cliente para sessao.url

// na URL de retorno:
const resultado = await synergy.onboarding.verifyReturn(new URL(req.url).searchParams, { expectedState });

Erros#

Todo erro herda de SynergyError. Os da API são APIError (com status, code, subcode, message, details, fbtraceId, retryAfter, feature, reason e o body cru):

ClasseQuando
AuthenticationError401: chave inválida, revogada ou sem o escopo da rota
PermissionError403: plano sem a feature, conta suspensa, número congelado, janela e limites de assistente
NotFoundError404
BadRequestError400
ConflictError409
IdempotencyError422 (código 1390003): a mesma Idempotency-Key com corpo diferente
RateLimitError429, com retryAfter em segundos
ServerError5xx
MetaErrorErro da Meta repassado (por exemplo 131047, fora da janela de 24 h)
ConnectionError, TimeoutError, AbortError, WebhookSignatureErrorRede, prazo, cancelamento e assinatura

Os códigos estão em Erros. err.toString() e err.toJSON() nunca incluem a chave nem os headers.

Exemplo
import { RateLimitError, MetaError } from '@synergyconnectapp/sdk/errors';

try {
  await wa.messages.text({ to, body: 'Oi' });
} catch (err) {
  if (err instanceof RateLimitError) console.log('tente em', err.retryAfter, 's');
  else if (err instanceof MetaError && err.code === 131047) console.log('janela fechada: use um modelo');
  else throw err;
}

Retentativas#

SituaçãoO que o SDK faz
Rede, prazo ou 5xx num envioReenvia com a mesma Idempotency-Key (ele gera uma se você não passar), então não duplica a mensagem
409 código 1390002Espera e repete com a mesma chave
429 código 130429Respeita Retry-After, até 30 s
429 de volume ou de limite (1390006, 1390010, 1390012, 1390015)Não reenvia: lança RateLimitError com retryAfter
Outros 4xx e erros da MetaNão reenvia
GET e buscas (POST …/search)Reenvia normalmente
Upload, gestão (criar, alterar, apagar), escrita de modelos e cadastro de númerosSó refaz se a rede falhou antes de qualquer resposta; nunca depois de um status

Segurança#

  • A chave só é enviada para a origem exata de baseURL, e o SDK não segue redirecionamentos.
  • Não use o SDK com a chave em código de navegador. O construtor recusa, a menos que você ligue dangerouslyAllowBrowser.
  • O SDK é gerado a partir do mesmo openapi.json que alimenta a referência da API: uma operação nova na API só chega ao pacote depois de passar no teste de contrato.

Ficou com dúvida? Escreva para suporte@synergyconnect.com.br.