Pular para o conteúdo

SDK TypeScript

O @notificme/sdk é o SDK oficial em TypeScript. Ele envia eventos, mantém cards vivos atualizados, pede decisões a pessoas e espera a resposta, e verifica os webhooks assinados que o servidor envia.

  • Roda em Node.js 18+, Bun, Deno, Cloudflare Workers e navegadores.
  • Publica ESM e CommonJS com tipos completos.
  • Não tem dependências em tempo de execução: usa o fetch e o WebCrypto nativos.
  • Repete 429, 5xx e erros de rede (respeitando Retry-After), com timeout em toda requisição.
Janela do terminal
npm install @notificme/sdk

Você escreve os campos em camelCase, e o SDK converte para o snake_case do protocolo. Ele nunca mexe nas chaves dentro de payload. Os exemplos de código ficam em inglês em qualquer idioma.

import { NotificMe } from "@notificme/sdk";
const notific = new NotificMe({ token: process.env.NOTIFICME_TOKEN, source: "billing" });
Opção Padrão Descrição
token variável NOTIFICME_TOKEN Token de serviço (nm_svc_…). Guarde no servidor.
baseUrl NOTIFICME_URL ou https://api.notific.me Endereço da API (o seu servidor, se for self-hosted).
source nenhum source padrão dos seus eventos.
timeoutMs 30000 Timeout por requisição.
maxRetries 3 Novas tentativas em 429, 5xx e erros de rede.
retryBaseDelayMs / retryMaxDelayMs 500 / 10000 Backoff exponencial com jitter.
fetch fetch global Implementação própria de fetch.
userAgent nenhum Texto extra no cabeçalho User-Agent (só Node).

notific.keysUrl é a URL das chaves públicas do servidor, usada para verificar webhooks. Todo método também aceita { signal, timeoutMs } como último argumento.

Envia um evento (POST /v0/events) e resolve com os dados do 202 Accepted.

const result = await notific.send({
type: "container.die", // dotted lowercase
severity: "high", // info | low | medium | high | critical (default: info)
summary: "api-v2 died on vps-01",
to: "jimmy", // handle, member e-mail or group; omit to let the rules decide
dedupKey: "vps-01/api-v2", // merges repeats in the dedup window
expiresWhen: "container.healthy",
payload: { host: "vps-01", exitCode: 137 }, // sent as is
});
// result: { id, status: "queued" | "duplicate", duplicateOf?, senderId, receivedAt, cardId?, replayed }
Campo Descrição
type Obrigatório. Tipo em minúsculas com pontos.
source Padrão: o source do cliente.
severity Padrão: info.
occurredAt Date ou string; padrão: agora.
id Seu id de idempotência. Gerado se omitido, então novas tentativas nunca duplicam.
to Um destinatário ou uma lista.
summary, card Atalhos para content.summary e content.card.
content { summary, card?, rich? }.
payload, dedupKey, ttl, expiresWhen, retention Veja Eventos.
cardId, live Cards vivos: live: true gera um cardId, devolvido no resultado.

É assim que um send() com card aparece, e o código por trás:

Push / relógioapi-v2 died on vps-01 (exit 137)

Container died

api-v2 on vps-01 was killed by the OOM killer and restarted.

Crashes in 24h3+2
  • Exit code 137 (out of memory)
  • Memory limit: 512 MiB
  • Restarted automatically
Open dashboard ↗Restart
import { NotificMe } from "@notificme/sdk";
const notific = new NotificMe({ source: "my-service" });
await notific.send({
type: "container.die",
severity: "high",
summary: "api-v2 died on vps-01 (exit 137)",
card: {
title: "Container died",
theme: { accent: "#E5484D", icon: "🐳" },
blocks: [
{
type: "text",
text: "api-v2 on vps-01 was killed by the OOM killer and restarted.",
},
{ type: "metric", label: "Crashes in 24h", value: 3, delta: "+2" },
{
type: "list",
items: [
{ text: "Exit code 137 (out of memory)", icon: "⚠️" },
{ text: "Memory limit: 512 MiB", icon: "📦" },
{ text: "Restarted automatically", icon: "🔁" },
],
},
{
type: "buttons",
buttons: [
{ label: "Open dashboard", url: "https://dokploy.example.com/project/api" },
{ label: "Restart", action: "docker.restart" },
],
},
],
},
});

Atualiza um card vivo enviando um novo evento com o mesmo cardId. type, source, severity e summary vêm do último evento que este cliente enviou para aquele card; você pode sobrescrever qualquer um.

const deploy = await notific.send({
type: "deploy.progress",
summary: "Deploying api-v2",
live: true,
card: { blocks: [{ type: "progress", value: 0.1, steps: ["Build", "Tests", "Push image", "Rollout"], current: 0 }] },
});
await notific.update(deploy.cardId!, {
card: { blocks: [{ type: "progress", value: 0.6, steps: ["Build", "Tests", "Push image", "Rollout"], current: 2 }] },
});
Push / relógioDeploying api-v2

Deploy api-v2

  1. Build
  2. Tests
  3. Push image
  4. Rollout

Commit 4c69fac by Jimmy: verification with the authenticator app.

import { NotificMe } from "@notificme/sdk";
const notific = new NotificMe({ source: "my-service" });
const deploy = await notific.send({
type: "deploy.progress",
severity: "info",
summary: "Deploying api-v2",
live: true,
card: {
title: "Deploy api-v2",
theme: { accent: "#FF6A00", icon: "🚀" },
blocks: [
{
type: "progress",
value: 0.6,
steps: ["Build", "Tests", "Push image", "Rollout"],
current: 2,
},
{
type: "text",
text: "Commit 4c69fac by Jimmy: verification with the authenticator app.",
},
],
},
});

Cria um grupo de perguntas e espera (long-poll) até uma pessoa responder ou o prazo acabar. O resultado é tipado pelo id de cada pergunta.

const r = await notific.ask({
to: "jimmy",
title: "Agent wants to delete 340 duplicate records",
context: "The CRM cleanup agent found 340 contacts duplicated by e-mail.",
questions: [
{ id: "action", type: "single", text: "What should we do?",
options: ["Approve", "Only the first 10", "Deny"], allowOther: true },
{ id: "backup", type: "yesno", text: "Back up first?" },
{ id: "note", type: "text", text: "Instructions for the agent", optional: true },
],
level: 2,
timeout: "30m",
onTimeout: { action: "Deny" },
});
if (r.status === "answered" && r.answers.action === "Approve" && r.answers.backup) {
// r.answers.action: "Approve" | "Only the first 10" | "Deny" | UntrustedText (because allowOther)
// r.answers.backup: boolean
// r.answers.note: UntrustedText | undefined
}
console.log(r.asText); // natural-language summary, ready for an AI agent
Push / relógioAgent wants to delete 340 duplicate records
import { NotificMe } from "@notificme/sdk";
const notific = new NotificMe({ source: "my-service" });
const reply = await notific.ask({
title: "Agent wants to delete 340 duplicate records",
questions: [
{
id: "action",
type: "single",
text: "What should we do?",
options: ["Approve", "Only the first 10", "Deny"],
allowOther: true,
},
{ id: "backup", type: "yesno", text: "Back up first?" },
{
id: "note",
type: "text",
text: "Instructions for the agent",
optional: true,
},
],
level: 2,
timeout: "30m",
onTimeout: { action: "Deny" },
card: {
title: "CRM cleanup",
theme: { icon: "🤖" },
blocks: [
{
type: "text",
text: "The CRM cleanup agent found 340 contacts duplicated by e-mail.",
},
{
type: "table",
columns: ["E-mail", "Copies"],
rows: [
],
},
{ type: "options", questionId: "action" },
{ type: "options", questionId: "backup" },
{ type: "input", questionId: "note" },
],
},
});
console.log(reply.status, reply.answers, reply.asText);
Campo Descrição
title Obrigatório, até 160 caracteres.
questions Obrigatório, de 1 a 20. Tipos: yesno, single, multi, text, voice, number, date.
to Destinatários.
context Por que você está perguntando (até 4000 caracteres).
level De 0 a 4, padrão 1. O workspace pode exigir mais.
quorum Quantas pessoas precisam responder (2 ou mais).
offline "allowed" ou "denied".
timeout "30s", "30m", "2h", "3d" ou número de segundos.
onTimeout Respostas aplicadas se ninguém responder no prazo, por id de pergunta.
escalate Quem é avisado quando o prazo está perto.
callbackUrl Webhook que recebe a resposta assinada.
card Um card com contexto. Posicione as perguntas com { type: "options", questionId } e { type: "input", questionId }.
Opção Descrição
idempotencyKey Para o POST /v0/asks; gerada se omitida, então uma nova tentativa nunca cria o grupo duas vezes.
maxWaitMs Espera máxima no cliente. Quando acaba, lança AskPendingError.
signal, timeoutMs Cancelam a chamada e a espera; timeout por requisição.
Tipo de pergunta Tipo da resposta
yesno boolean
single uma das options (| UntrustedText com allowOther: true)
multi lista de options (texto livre extra em r.details[id].freeText)
text UntrustedText
voice { text?: UntrustedText; audioRef?: string }
number number
date string (YYYY-MM-DD ou RFC 3339)
  • status: "answered", "timeout" (as respostas de onTimeout foram aplicadas) ou "rejected". Depois de checar "answered", toda resposta obrigatória está presente.
  • answers: tipadas pelo id da pergunta.
  • asText: resumo em linguagem natural para um agente de IA.
  • answeredBy (identity, deviceId, levelUsed, authMethod), answeredAt, signature e contentHash.
  • details[id]: o texto da pergunta, o valor cru e o texto livre.
  • raw: a resposta exatamente como o servidor enviou.

A espera pode terminar em dois erros: AskClosedError quando o grupo expira ou é cancelado, e AskPendingError quando maxWaitMs acaba antes. Retome depois com waitForReply().

Um formulário com vários tipos de pergunta:

Push / relógioRelease window for api-v2

Release window

Which regions?
Postpone until
How many servers per batch?
Anything else? (voice) (opcional)🎙 Segure para gravar
import { NotificMe } from "@notificme/sdk";
const notific = new NotificMe({ source: "my-service" });
const reply = await notific.ask({
title: "Release window for api-v2",
questions: [
{
id: "regions",
type: "multi",
text: "Which regions?",
options: ["BR", "US", "EU"],
},
{ id: "until", type: "date", text: "Postpone until" },
{ id: "batch", type: "number", text: "How many servers per batch?" },
{ id: "voice", type: "voice", text: "Anything else? (voice)", optional: true },
],
level: 1,
card: {
title: "Release window",
blocks: [
{ type: "options", questionId: "regions" },
{ type: "input", questionId: "until" },
{ type: "input", questionId: "batch" },
{ type: "input", questionId: "voice" },
],
},
});
console.log(reply.status, reply.answers, reply.asText);

Cria o grupo de perguntas sem esperar. Use com callbackUrl para receber a resposta num webhook, ou chame handle.wait() depois.

const handle = await notific.createAsk({
title: "Approve refund #881?",
questions: [{ id: "ok", type: "yesno", text: "Approve?" }],
callbackUrl: "https://crm.example.com/hooks/notificme",
});
// handle: { id, status: "open", version, level, title, createdAt, timeoutAt?, recipients?, pollUrl?, wait() }
import { AskPendingError } from "@notificme/sdk";
try {
const r = await notific.ask(input, { maxWaitMs: 60_000 });
} catch (err) {
if (err instanceof AskPendingError) {
// later, maybe in another process:
const r = await notific.waitForReply(err.askId, { questions: input.questions });
}
}
// One read, optionally waiting up to 60 s for a change:
const view = await notific.getAsk(id, { waitSeconds: 30 });

Passe as mesmas questions para waitForReply() para receber respostas tipadas.

import { verifyWebhook, receiveWebhook, receiveWebhookRequest } from "@notificme/sdk";
import { expressWebhook, expressWebhookOf, fastifyWebhookPlugin, fastifyWebhook, honoWebhook } from "@notificme/sdk";
Função Uso
verifyWebhook(rawBody, signatureHeader, key, options?) Verifica a assinatura Ed25519; lança WebhookVerificationError.
receiveWebhook(rawBody, headers, key, options?) Verifica e interpreta; no tópico reply, devolve um reply tipado.
receiveWebhookRequest(request, key, options?) O mesmo para um Request do fetch (Next.js, Workers, Deno, Bun).
expressWebhook({ key }), expressWebhookOf(req) Middleware do Express.
fastifyWebhookPlugin, fastifyWebhook Plugin e hook preHandler do Fastify.
honoWebhook({ key }) Middleware do Hono (c.get("notificme")).
notific.verifyWebhook(), notific.receiveWebhook() O mesmo, usando o keysUrl deste cliente.

Veja exemplos completos em Webhooks.

Texto livre digitado por uma pessoa chega como UntrustedText, não como string. .text devolve o texto cru; .quoted() devolve uma string JSON para incluir num prompt como dado. Nunca execute como comando, SQL, HTML ou instruções.

Erro Quando
NotificMeApiError Resposta fora de 2xx. Tem status, code (estável, como validation_failed ou rate_limited), details e requestId.
NotificMeError Falha no cliente. code é timeout, network_error, aborted, invalid_response ou missing_token.
AskClosedError O grupo de perguntas expirou ou foi cancelado sem resposta.
AskPendingError maxWaitMs acabou com o grupo ainda aberto (askId).
WebhookVerificationError Assinatura de webhook inválida (reason).

Todos estendem NotificMeError.