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

@rockdev/messaging

v0.2.0

Published

SDK oficial da plataforma Messaging: envio e consulta de mensagens por e-mail, WhatsApp e web push

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/messaging

Requer 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

E-mail

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.

WhatsApp

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 entrou

subject é 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.createdAt

Idempotê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 confirmaram

attemptLog é 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?.hasMore

Pá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.