Saltar al contenido

SDK de TypeScript

@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.

  • Funciona en Node.js 18+, Bun, Deno, Cloudflare Workers y navegadores.
  • Publica ESM y CommonJS con tipos completos.
  • No tiene dependencias en tiempo de ejecución: usa el fetch y el WebCrypto nativos.
  • Reintenta 429, 5xx y errores de red (respetando Retry-After), con timeout en cada petición.
Ventana de terminal
npm install @notificme/sdk

Escribes 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.

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.

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:

Push / relojapi-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" },
],
},
],
},
});

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 }] },
});
Push / relojDeploying 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.",
},
],
},
});

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 agent
Push / relojAgent 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 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:

Push / relojRelease window for api-v2

Release window

Which regions?
Postpone until
How many servers per batch?
Anything else? (voice) (opcional)🎙 Mantén para grabar
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);

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?)

Sección titulada «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.

El 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.