Webhooks

Delivery and engagement events, POSTed to your endpoint and signed so you can prove they came from us.

Setup

Console → your app → SettingsWebhooks → add an HTTPS endpoint. Pick the events you want (or none, which means all). The signing secret is shown once — store it like a password; it's sealed at rest and we can't show it again.

HTTPS only. Private hostnames and raw IP addresses are rejected — that guard stops a webhook being used to probe internal networks.

Events

EventFires when
message.sentA device accepted the push (the provider returned success).
message.failedDelivery failed — error_code carries the normalized reason.
message.clickedA recipient opened the notification (all platforms).

Payload

POST https://api.yourco.com/hooks/notibase
content-type: application/json
notibase-event: message.sent
notibase-signature: t=1787260000,v1=5f2b…c91

{
  "event": "message.sent",
  "created_at": "2026-08-20T18:04:11.238Z",
  "data": {
    "message_id": "9f1c…",
    "device_id": "2b70…",
    "channel": "webpush",
    "error_code": null
  }
}

Verifying the signature

The signature is HMAC-SHA256 over {timestamp}.{raw body}. Verify against the raw request body — parsing and re-serializing changes the bytes and the check will fail.

import crypto from "node:crypto";

export function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => {
      const i = kv.indexOf("=");
      return [kv.slice(0, i), kv.slice(i + 1)];
    })
  );
  const ts = Number(parts.t);
  if (!Number.isFinite(ts)) return false;
  // Reject replays of an old, previously-valid request.
  if (Math.abs(Math.floor(Date.now() / 1000) - ts) > toleranceSec) return false;

  const expected = crypto.createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(parts.v1, "hex");
  return a.length === b.length && crypto.timingSafeEqual(a, b);   // constant-time
}

Express, keeping the raw body:

app.post("/hooks/notibase",
  express.raw({ type: "application/json" }),
  (req, res) => {
    if (!verify(req.body.toString(), req.get("notibase-signature"), process.env.NB_WEBHOOK_SECRET)) {
      return res.sendStatus(401);
    }
    const evt = JSON.parse(req.body.toString());
    // …do your work asynchronously, then:
    res.sendStatus(200);
  });

Delivery behaviour

Treat events as at-least-once and possibly out of order. Make your handler idempotent — key on message_id + device_id + event.

Troubleshooting

SymptomCause
Signature never verifiesYou're hashing a re-serialized body. Use the raw bytes.
Hook went disabled15 straight failures. Check the log for the status code, fix, re-enable.
No attempts logged at allThe event isn't in the hook's subscription list, or the app has sent nothing since it was created.
Timeouts in the logYour handler is doing work inline. Acknowledge first, process after.
← REST APIDelivery errors →