Webhooks (respuestas)
En lugar de dejar un proceso esperando en ask(), puedes crear el grupo de preguntas con un callback_url. Cuando haya respuesta (o venza el plazo), notific.me envía la respuesta firmada a esa URL. Esas llamadas salientes son los webhooks. (Las llamadas que otros sistemas envían a notific.me son los hooks.)
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, handle.status === "open"Qué llega
Sección titulada «Qué llega»Un POST a tu callback_url con la respuesta como cuerpo JSON y estas cabeceras:
Notificme-Signature: t=<unix>,kid=<key id>,sig=<base64url(Ed25519(t + "." + body))>Notificme-Webhook-Id: <id> (igual en cada reintento: úsalo para deduplicar)Notificme-Topic: replyNotificme-Attempt: 1Entrega y reintentos
Sección titulada «Entrega y reintentos»- La respuesta y el webhook se escriben en la misma transacción (outbox transaccional), así la decisión nunca se pierde.
- Cualquier
2xxcuenta como entregado. Los errores y los timeouts (15 s) se reintentan con backoff exponencial durante hasta 24 horas. - Webhooks en app.notific.me lista cada entrega con su estado, intentos y último error, y permite reenviar.
Verificar la firma
Sección titulada «Verificar la firma»El servidor firma con Ed25519. Las claves públicas están en <api>/.well-known/notificme-keys.json (JWKS). Se rechazan las firmas con más de 5 minutos de diferencia con tu reloj.
import express from "express";import { NotificMe, expressWebhook, expressWebhookOf } from "@notificme/sdk";
const notific = new NotificMe();const app = express();
// Put the middleware before any JSON parser (it reads the raw body).app.post("/hooks/notificme", expressWebhook({ key: notific.keysUrl }), (req, res) => { const webhook = expressWebhookOf(req)!; // verified console.log(webhook.reply?.status, webhook.reply?.asText); res.sendStatus(204);});import Fastify from "fastify";import { NotificMe, fastifyWebhookPlugin } from "@notificme/sdk";
const notific = new NotificMe();const app = Fastify();
await app.register(fastifyWebhookPlugin, { prefix: "/hooks/notificme", key: notific.keysUrl, handler: async (webhook) => { console.log(webhook.reply?.asText); }, // answers 204 when the handler returns nothing});import { Hono } from "hono";import { NotificMe, honoWebhook, type NotificMeWebhook } from "@notificme/sdk";
const notific = new NotificMe();const app = new Hono<{ Variables: { notificme: NotificMeWebhook } }>();
app.post("/hooks/notificme", honoWebhook({ key: notific.keysUrl }), (c) => { const webhook = c.get("notificme"); console.log(webhook.reply?.asText); return c.body(null, 204);});import { receiveWebhookRequest } from "@notificme/sdk";
const keys = "https://api.notific.me/.well-known/notificme-keys.json";
export async function POST(request: Request) { try { const webhook = await receiveWebhookRequest(request, keys); console.log(webhook.reply?.asText); return new Response(null, { status: 204 }); } catch { return new Response(null, { status: 401 }); }}Todos los helpers responden 401 {"error":{"code":"invalid_signature"}} cuando la firma no es válida, así tu handler nunca ve esa petición.
Bajo nivel
Sección titulada «Bajo nivel»import { receiveWebhook, verifyWebhook } from "@notificme/sdk";
// Throws WebhookVerificationError (error.reason says why).await verifyWebhook(rawBody, headers["notificme-signature"], keys);
// Verify and parse: headers, JSON payload and, for the "reply" topic, a typed result.const webhook = await receiveWebhook(rawBody, headers, keys, { questions });webhook.reply?.answers.ok;La clave puede ser la URL de las claves (se descarga y se guarda en caché), un string base64url, los bytes, un JWK o un JWKS. WebhookVerificationError.reason es missing_header, malformed_header, timestamp_out_of_tolerance, unknown_key, invalid_key o invalid_signature.