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
@notificme/sdk es el SDK oficial de TypeScript. Envía eventos, mantiene actualizadas las tarjetas vivas, pide decisiones a personas y espera la respuesta, y verifica los webhooks firmados que envía el servidor.
fetch y el WebCrypto nativos.429, 5xx y errores de red (respetando Retry-After), con timeout en cada petición.npm install @notificme/sdkEscribes los campos en camelCase, y el SDK los convierte al snake_case del protocolo. Nunca toca las claves dentro de payload. Los ejemplos de código están en inglés en cualquier idioma.
new NotificMe(options)import { NotificMe } from "@notificme/sdk";
const notific = new NotificMe({ token: process.env.NOTIFICME_TOKEN, source: "billing" });| Opción | Por defecto | Descripción |
|---|---|---|
token |
variable NOTIFICME_TOKEN |
Token de servicio (nm_svc_…). Guárdalo en el servidor. |
baseUrl |
NOTIFICME_URL o https://api.notific.me |
Dirección de la API (tu propio servidor, si es self-hosted). |
source |
ninguno | source por defecto de tus eventos. |
timeoutMs |
30000 |
Timeout por petición. |
maxRetries |
3 |
Reintentos en 429, 5xx y errores de red. |
retryBaseDelayMs / retryMaxDelayMs |
500 / 10000 |
Backoff exponencial con jitter. |
fetch |
fetch global |
Implementación propia de fetch. |
userAgent |
ninguno | Texto extra en la cabecera User-Agent (solo Node). |
notific.keysUrl es la URL de las claves públicas del servidor, usada para verificar webhooks. Todos los métodos aceptan además { signal, timeoutMs } como último argumento.
send(event)Envía un evento (POST /v0/events) y resuelve con los datos del 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 | Descripción |
|---|---|
type |
Obligatorio. Tipo en minúsculas con puntos. |
source |
Por defecto: el source del cliente. |
severity |
Por defecto: info. |
occurredAt |
Date o string; por defecto: ahora. |
id |
Tu id de idempotencia. Se genera si lo omites, así los reintentos nunca duplican. |
to |
Un destinatario o una lista. |
summary, card |
Atajos para content.summary y content.card. |
content |
{ summary, card?, rich? }. |
payload, dedupKey, ttl, expiresWhen, retention |
Mira Eventos. |
cardId, live |
Tarjetas vivas: live: true genera un cardId, devuelto en el resultado. |
Así se ve un send() con tarjeta, y el código detrá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)Actualiza una tarjeta viva enviando un nuevo evento con el mismo cardId. type, source, severity y summary se toman del último evento que este cliente envió para esa tarjeta; puedes sobrescribir cualquiera.
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?)Crea un grupo de preguntas y espera (long-poll) hasta que una persona responda o venza el plazo. El resultado está tipado por el id de cada pregunta.
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 | Descripción |
|---|---|
title |
Obligatorio, hasta 160 caracteres. |
questions |
Obligatorio, de 1 a 20. Tipos: yesno, single, multi, text, voice, number, date. |
to |
Destinatarios. |
context |
Por qué preguntas (hasta 4000 caracteres). |
level |
De 0 a 4, por defecto 1. El workspace puede exigir más. |
quorum |
Cuántas personas deben responder (2 o más). |
offline |
"allowed" o "denied". |
timeout |
"30s", "30m", "2h", "3d" o un número de segundos. |
onTimeout |
Respuestas aplicadas si nadie responde a tiempo, por id de pregunta. |
escalate |
A quién se avisa cuando el plazo está cerca. |
callbackUrl |
Webhook que recibe la respuesta firmada. |
card |
Una tarjeta con contexto. Ubica las preguntas con { type: "options", questionId } y { type: "input", questionId }. |
| Opción | Descripción |
|---|---|
idempotencyKey |
Para POST /v0/asks; se genera si la omites, así un reintento nunca crea el grupo dos veces. |
maxWaitMs |
Espera máxima en el cliente. Al terminar, lanza AskPendingError. |
signal, timeoutMs |
Cancelan la llamada y la espera; timeout por petición. |
| Tipo de pregunta | Tipo de respuesta |
|---|---|
yesno |
boolean |
single |
una de las options (| UntrustedText con allowOther: true) |
multi |
lista de options (texto libre extra en r.details[id].freeText) |
text |
UntrustedText |
voice |
{ text?: UntrustedText; audioRef?: string } |
number |
number |
date |
string (YYYY-MM-DD o RFC 3339) |
status: "answered", "timeout" (se aplicaron las respuestas de onTimeout) o "rejected". Tras comprobar "answered", toda respuesta obligatoria está presente.answers: tipadas por el id de la pregunta.asText: resumen en lenguaje natural para un agente de IA.answeredBy (identity, deviceId, levelUsed, authMethod), answeredAt, signature y contentHash.details[id]: el texto de la pregunta, el valor original y el texto libre.raw: la respuesta exactamente como la envió el servidor.La espera puede terminar con dos errores: AskClosedError cuando el grupo vence o se cancela, y AskPendingError cuando maxWaitMs termina antes. Retoma después con waitForReply().
Un formulario con varios tipos de pregunta:
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?)Crea el grupo de preguntas sin esperar. Úsalo con callbackUrl para recibir la respuesta en un webhook, o llama a handle.wait() más tarde.
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?) y 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 });Pasa las mismas questions a waitForReply() para recibir respuestas tipadas.
import { verifyWebhook, receiveWebhook, receiveWebhookRequest } from "@notificme/sdk";import { expressWebhook, expressWebhookOf, fastifyWebhookPlugin, fastifyWebhook, honoWebhook } from "@notificme/sdk";| Función | Uso |
|---|---|
verifyWebhook(rawBody, signatureHeader, key, options?) |
Verifica la firma Ed25519; lanza WebhookVerificationError. |
receiveWebhook(rawBody, headers, key, options?) |
Verifica e interpreta; en el tema reply, devuelve un reply tipado. |
receiveWebhookRequest(request, key, options?) |
Lo mismo para un Request de fetch (Next.js, Workers, Deno, Bun). |
expressWebhook({ key }), expressWebhookOf(req) |
Middleware de Express. |
fastifyWebhookPlugin, fastifyWebhook |
Plugin y hook preHandler de Fastify. |
honoWebhook({ key }) |
Middleware de Hono (c.get("notificme")). |
notific.verifyWebhook(), notific.receiveWebhook() |
Lo mismo, usando el keysUrl de este cliente. |
Mira ejemplos completos en Webhooks.
UntrustedTextEl texto libre que escribe una persona llega como UntrustedText, no como string. .text devuelve el texto tal cual; .quoted() devuelve un string JSON para incluirlo en un prompt como dato. Nunca lo ejecutes como comando, SQL, HTML ni instrucciones.
| Error | Cuándo |
|---|---|
NotificMeApiError |
Respuesta fuera de 2xx. Tiene status, code (estable, como validation_failed o rate_limited), details y requestId. |
NotificMeError |
Fallo en el cliente. code es timeout, network_error, aborted, invalid_response o missing_token. |
AskClosedError |
El grupo de preguntas venció o se canceló sin respuesta. |
AskPendingError |
maxWaitMs terminó con el grupo todavía abierto (askId). |
WebhookVerificationError |
Firma de webhook no válida (reason). |
Todos extienden NotificMeError.