Skip to content

Webhooks (replies)

Instead of keeping a process waiting on ask(), you can create the question set with a callback_url. When it is answered (or times out), notific.me sends the signed reply to that URL. These outgoing calls are the webhooks. (Calls that other systems send to notific.me are 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"

POST to your callback_url with the reply as the JSON body and these headers:

Notificme-Signature: t=<unix>,kid=<key id>,sig=<base64url(Ed25519(t + "." + body))>
Notificme-Webhook-Id: <id> (the same on every retry: use it to deduplicate)
Notificme-Topic: reply
Notificme-Attempt: 1
  • The reply and the webhook are written in the same transaction (transactional outbox), so a decision never gets lost.
  • Any 2xx counts as delivered. Errors and timeouts (15 s) are retried with exponential backoff for up to 24 hours.
  • Webhooks in app.notific.me lists every delivery with its status, attempts and last error, and lets you resend one.

The server signs with Ed25519. The public keys are published at <api>/.well-known/notificme-keys.json (JWKS). Signatures more than 5 minutes away from your clock are rejected.

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);
});

Every helper answers 401 {"error":{"code":"invalid_signature"}} when a signature is invalid, so your handler never sees that request.

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;

The key can be the keys URL (downloaded and cached), a base64url string, raw bytes, a JWK or a JWKS. WebhookVerificationError.reason is missing_header, malformed_header, timestamp_out_of_tolerance, unknown_key, invalid_key or invalid_signature.