Container died
api-v2 on vps-01 was killed by the OOM killer and restarted.
- Exit code 137 (out of memory)
- Memory limit: 512 MiB
- Restarted automatically
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.
fetch e o WebCrypto nativos.429, 5xx e erros de rede (respeitando Retry-After), com timeout em toda requisição.npm install @notificme/sdkVocê 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.
new NotificMe(options)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.
send(event)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:
api-v2 on vps-01 was killed by the OOM killer and restarted.
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" }, ], }, ], },});{ "source": "my-service", "type": "container.die", "severity": "high", "occurred_at": "2026-11-05T12:00:00Z", "content": { "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" } ] } ] } }}update(cardId, event)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 }] },});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.", }, ], },});{ "source": "my-service", "type": "deploy.progress", "severity": "info", "occurred_at": "2026-11-05T12:00:00Z", "card_id": "deploy/api-v2", "content": { "summary": "Deploying api-v2", "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." } ] } }}ask(input, options?)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 agentThe CRM cleanup agent found 340 contacts duplicated by e-mail.
| Copies | |
|---|---|
| [email protected] | 4 |
| [email protected] | 3 |
| [email protected] | 2 |
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);{ "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" ], "allow_other": true }, { "id": "backup", "type": "yesno", "text": "Back up first?" }, { "id": "note", "type": "text", "text": "Instructions for the agent", "optional": true } ], "level": 2, "timeout": "30m", "on_timeout": { "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": [ [ 4 ], [ 3 ], [ 2 ] ] }, { "type": "options", "question_id": "action" }, { "type": "options", "question_id": "backup" }, { "type": "input", "question_id": "note" } ] }}| 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:
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);{ "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", "question_id": "regions" }, { "type": "input", "question_id": "until" }, { "type": "input", "question_id": "batch" }, { "type": "input", "question_id": "voice" } ] }}createAsk(input, options?)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() }waitForReply(id, options?) e getAsk(id, options?)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.
UntrustedTextTexto 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.