@rockdev/messaging
v0.2.0
Published
SDK oficial da plataforma Messaging: envio e consulta de mensagens por e-mail, WhatsApp e web push
Maintainers
Readme
@rockdev/messaging
SDK oficial da plataforma Messaging. Envia e consulta mensagens de e-mail e WhatsApp pela mesma chamada, com tipos completos e sem dependência nenhuma em runtime.
npm install @rockdev/messaging
# bun add @rockdev/messaging · pnpm add @rockdev/messagingRequer Node 18+, Bun, Deno ou qualquer runtime com fetch global.
Começando
import { Messaging } from '@rockdev/messaging'
export const client = new Messaging({ secret: process.env.MESSAGING_API_KEY! })
const { data, error } = await client.emails.send({
to: '[email protected]',
from: 'Financeiro <[email protected]>',
subject: 'Pagamento aprovado',
html: '<p>Recebemos o pagamento da sua mensalidade.</p>',
})
if (error) {
console.error(error.code, error.message)
} else {
console.log(data.id)
}A API key é um segredo de servidor: nunca a coloque em bundle de navegador nem
em app móvel. O escopo dela decide o que alcança — write para enviar, read
para consultar e listar.
Nenhum método lança por erro de API. Toda chamada devolve { data, error };
com error preenchido, data é null, e vice-versa.
Configuração
const client = new Messaging({
secret: process.env.MESSAGING_API_KEY!,
baseUrl: 'http://localhost:3001', // padrão: a API de produção, ou MESSAGING_BASE_URL
timeout: 30_000, // por chamada, em ms
maxRetries: 2, // 0 desliga
headers: { 'x-tenant': 'rockfeller' },
fetch: meuFetch, // para teste, proxy ou instrumentação
})Também aceita só a chave: new Messaging(process.env.MESSAGING_API_KEY!).
Todo método aceita um último argumento com { signal, timeout, headers }, que
vale só para aquela chamada.
Enviar
const { data, error } = await client.emails.send({
to: '[email protected]',
from: 'Financeiro <[email protected]>',
subject: 'Pagamento aprovado',
html: '<p>Recebemos o seu pagamento.</p>',
content: 'Recebemos o seu pagamento.', // alternativa em texto
})subject e html são obrigatórios, e o domínio do from precisa estar
verificado no projeto.
const { data, error } = await client.whatsapp.send({
to: '+5511999999999',
from: 'suporte', // slug do remetente, ou o número em E.164
content: 'Olá! Sua aula de amanhã é às 19h.',
})content é obrigatório; não há assunto nem HTML. O from precisa ser um
remetente cadastrado e habilitado.
Web push
const { data, error } = await client.webPush.send({
to: 'cus_8f3a2c', // o seu id da pessoa, ou { subscriptionId } para um só
from: 'checkout-web', // o appId da aplicação cadastrada
subject: 'Pagamento aprovado',
content: 'Recebemos o pagamento da sua mensalidade.',
})
data?.deliveries.webPush?.targets // em quantos navegadores entrousubject é o título e content o corpo, os dois obrigatórios: notificação
sem um deles aparece truncada na bandeja do sistema, e ali não há "abrir para
ler o resto".
É o único canal em que uma entrega tem vários destinos — a mesma pessoa
costuma ter mais de um navegador autorizado —, e por isso a entrega traz
targets. Ele nunca é zero: destinatário sem nenhum navegador inscrito é
recusado com WEBPUSH_NO_ACTIVE_SUBSCRIPTION, para você saber na hora que
ninguém ia receber.
Antes do primeiro envio a aplicação precisa registrar os navegadores em
POST /v1/webpush/subscriptions/{appId}, que não passa pelo SDK: quem a
chama é a página no navegador da pessoa, sem API key. O passo a passo está na
referência da API, em Web push · inscrições.
messages.send, o caminho comum
emails.send, whatsapp.send e webPush.send são atalhos: os três montam o
destino e chamam messages.send, que aceita o canal explícito e as mesmas
opções.
await client.messages.send({
// ou { whatsapp: '+5511999999999' }, ou { webpush: { recipientId: 'cus_8f3a2c' } }
to: { email: '[email protected]' },
from: 'Financeiro <[email protected]>',
subject: 'Pagamento aprovado',
html: '<p>Recebemos o seu pagamento.</p>',
content: 'Recebemos o seu pagamento.',
template: 'checkout.payment-approved',
data: { nome: 'Hiago', valor: '249,90' },
locale: 'pt-BR',
metadata: { fatura: 'INV-2026-0912' },
priority: 'low',
sendAt: new Date('2026-09-20T22:00:00Z'),
idempotencyKey: 'fatura-INV-2026-0912-aprovada',
})| Opção | Vale para | O que é |
| --- | --- | --- |
| to | todos | { email }, { whatsapp }, { webpush } ou a string — o SDK decide pelo @ |
| from | todos | endereço com domínio verificado, slug/E.164 do remetente, ou appId da aplicação |
| subject | e-mail, push | obrigatório; no push é o título |
| html | e-mail | obrigatório, até 256 KB |
| content | todos | obrigatório no WhatsApp e no push; no e-mail, a alternativa em texto |
| template | todos | id do fluxo que originou a mensagem, para agrupar e filtrar |
| data | todos | variáveis do template, até 64 KB serializados |
| locale | todos | idioma em que a mensagem foi renderizada |
| metadata | todos | os seus dados, devolvidos na consulta e nos webhooks |
| priority | todos | normal (padrão) ou low, que cede a vez na fila |
| sendAt | todos | Date ou ISO-8601 no futuro |
| idempotencyKey | todos | torna o reenvio seguro (veja abaixo) |
A resposta é 202: a mensagem foi aceita e enfileirada, não entregue. O
estado final chega pelos webhooks do projeto ou por messages.get.
data.id // guarde: identifica a mensagem em tudo depois
data.status // 'accepted'
data.deliveries.email?.id // a entrega criada no canal
data.deliveries.webPush?.targets // no push, em quantos navegadores entrou
data.createdAtIdempotência
Passe idempotencyKey e repetir a chamada com a mesma chave devolve a mensagem
original em vez de criar outra — é o que deixa retentar um timeout de rede sem
enviar duas vezes. É também o que autoriza o SDK a retentar um POST sozinho:
sem chave, envios nunca são repetidos automaticamente.
Consultar
const { data: message, error } = await client.messages.get(id)
message?.status // 'delivered', 'bounced', 'failed'…
message?.channels[0]?.deliveredAt
message?.channels[0]?.attemptLog // todo provedor tentado, na ordem
message?.events // o que os webhooks do provedor confirmaramattemptLog é onde se vê um envio que caiu do provedor primário para o
secundário; a entrega mostra só quem ficou com ela.
Listar
const { data: page, error } = await client.messages.list({
status: ['failed', 'bounced'],
channel: 'email',
to: 'hiago@',
search: 'Pagamento',
scheduled: false,
createdAfter: new Date(Date.now() - 24 * 60 * 60 * 1000),
createdBefore: new Date(),
limit: 100, // máximo
})
page?.messages // da mais recente para a mais antiga
page?.nextCursor // null na última página
page?.hasMorePágina a página:
let page = (await client.messages.list({ limit: 100 })).data
while (page) {
processar(page.messages)
page = page.hasMore ? (await page.next()).data : null
}Ou de uma vez só — a página atravessa as seguintes sozinha:
const { data: page, error } = await client.messages.list({ status: 'failed' })
if (error) throw error
for await (const message of page) {
console.log(message.id, message.status)
}Num for await, um erro no meio da travessia é lançado: o { data, error } já
foi consumido na chamada que abriu a página.
Erros
const { data, error } = await client.messages.send({ ... })
if (error) {
error.code // 'FROM_DOMAIN_NOT_VERIFIED'
error.status // 400
error.message // frase da API, pensada para log
error.errors // detalhes por campo, em VALIDATION_ERROR
error.retryable // timeout, conexão, 429 e 5xx
}Ramifique pelo code — a frase muda, o código é contrato.
| code | Status | O que houve |
| --- | --- | --- |
| VALIDATION_ERROR | 400 | campo recusado antes do envio; veja error.errors |
| FROM_DOMAIN_NOT_VERIFIED | 400 | o domínio do from não está verificado no projeto |
| SENDER_NOT_FOUND | 400 | o remetente de WhatsApp não existe ou está desabilitado |
| WEBPUSH_APP_NOT_FOUND | 400 | o from não é uma aplicação web push cadastrada e habilitada |
| WEBPUSH_NO_ACTIVE_SUBSCRIPTION | 400 | o destinatário não tem nenhum navegador inscrito |
| UNAUTHENTICATED | 401 | nenhuma credencial foi apresentada |
| INVALID_API_KEY | 401 | chave inexistente, revogada ou de outro projeto |
| PROJECT_ROLE_REQUIRED | 403 | o escopo da chave não cobre a operação |
| MESSAGE_NOT_FOUND | 404 | a mensagem não existe ou é de outro projeto |
| RATE_LIMIT_EXCEEDED | 429 | requisições demais; veja error.retryAfter |
| INTERNAL_SERVER_ERROR | 5xx | erro da plataforma; vale retentar |
| TIMEOUT | — | a API não respondeu no tempo configurado |
| CONNECTION_ERROR | — | não foi possível falar com a API; nada saiu |
| UNEXPECTED_RESPONSE | — | a resposta não veio no formato esperado |
As classes também são exportadas, para quem prefere instanceof:
MessagingError e as filhas ValidationError, AuthenticationError,
PermissionError, NotFoundError, RateLimitError, ServerError,
ConnectionError, TimeoutError.
Retentativas
GET e envios com idempotencyKey são repetidos até maxRetries vezes em
timeout, falha de rede, 408, 429 e 5xx, com backoff exponencial, jitter e
respeito ao Retry-After. O resto não é repetido: um VALIDATION_ERROR vai
falhar igual na segunda vez, e um envio sem chave poderia sair duplicado.
Cancelamento
const controller = new AbortController()
setTimeout(() => controller.abort(), 2_000)
await client.messages.list({}, { signal: controller.signal })Um aborto seu nunca é retentado.
Tipos
Além dos tipos das respostas (Message, ListedMessage, AcceptedMessage,
MessageChannel, MessageAttempt, MessageEvent), o pacote exporta o
vocabulário da plataforma nas duas formas — a lista, para validar ou montar um
select, e a união de literais:
import { MESSAGE_STATUSES, type MessageStatus } from '@rockdev/messaging'MESSAGE_STATUSES, MESSAGE_DELIVERY_STATUSES, MESSAGE_EVENT_TYPES,
MESSAGE_ATTEMPT_OUTCOMES, MESSAGE_PRIORITIES, PROVIDER_CHANNELS,
PROVIDER_IDS, além dos limites (MESSAGE_LIST_MAX_LIMIT,
MESSAGE_BODY_MAX_BYTES, MESSAGE_JSON_MAX_BYTES,
MESSAGE_SUBJECT_MAX_LENGTH) e do atalho isTerminalDeliveryStatus.
