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.
npm install @synergyconnectapp/sdkComeç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ção | Padrão | Para quê |
|---|---|---|
apiKey | SYNERGY_API_KEY | A chave syn_… |
baseURL | https://api.synergyconnect.com.br | Só https. http apenas para localhost, com allowInsecureLocalhost: true |
graphVersion | v25.0 (fixa em cada versão do SDK) | A versão da Graph API nos caminhos de envio e mídia |
maxRetries | 2 | Tentativas extras (veja "Retentativas") |
timeout | 30000 (ms) | Prazo de cada chamada |
fetch | o global | Para injetar outro fetch (testes, Workers) |
dangerouslyAllowBrowser | false | Sem isso, criar o cliente num navegador lança um erro. A chave de API não pode ir para o navegador |
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.…{ "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.
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)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).
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.
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#
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)devolvetrueoufalsee nunca lança por entrada ruim.webhooks.constructEvent(...)confere a assinatura e devolve{ deliveryId, timestamped, object, instanceId, wabaId, changes }. LançaWebhookSignatureErrorse 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 nodeliveryId. Sem ele, cai paraX-Hub-Signature-256e devolvetimestamped: false. Veja Assinatura dos webhooks. - O
rawBodyprecisa serstring,ArrayBufferouUint8Array. Passar um objeto já interpretado lançaTypeError: o SDK nunca reserializa o JSON. - Segredo vazio é erro de configuração, nunca "válido".
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 rotateSecretimport { 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.
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):
| Classe | Quando |
|---|---|
AuthenticationError | 401: chave inválida, revogada ou sem o escopo da rota |
PermissionError | 403: plano sem a feature, conta suspensa, número congelado, janela e limites de assistente |
NotFoundError | 404 |
BadRequestError | 400 |
ConflictError | 409 |
IdempotencyError | 422 (código 1390003): a mesma Idempotency-Key com corpo diferente |
RateLimitError | 429, com retryAfter em segundos |
ServerError | 5xx |
MetaError | Erro da Meta repassado (por exemplo 131047, fora da janela de 24 h) |
ConnectionError, TimeoutError, AbortError, WebhookSignatureError | Rede, prazo, cancelamento e assinatura |
Os códigos estão em Erros. err.toString() e err.toJSON() nunca incluem a chave nem os headers.
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ção | O que o SDK faz |
|---|---|
| Rede, prazo ou 5xx num envio | Reenvia com a mesma Idempotency-Key (ele gera uma se você não passar), então não duplica a mensagem |
| 409 código 1390002 | Espera e repete com a mesma chave |
| 429 código 130429 | Respeita 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 Meta | Não reenvia |
GET e buscas (POST …/search) | Reenvia normalmente |
| Upload, gestão (criar, alterar, apagar), escrita de modelos e cadastro de números | Só 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.jsonque 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.