Quickstart

Register an endpoint, fire a test event and verify its signature — end to end.

This page takes you from nothing to a verified delivery. It uses curl and a small Node receiver, but nothing here is Node-specific.

  1. Check which events you can subscribe to

    The catalogue is the authority, and it is readable before you have registered anything:

    curl -s https://api.nyumbazetu.com/v3/webhooks/events \
      -H "Authorization: Bearer $NZ_TOKEN"
    
    {
      "success": true,
      "data": [
        {
          "eventType": "billing.invoice.issued",
          "version": 1,
          "means": "An invoice was issued to its payer and is now collectable.",
          "entityType": "invoice"
        }
      ]
    }
    

    Registering an event name that is not on this list is refused at registration rather than accepted and silently never delivered. Requires permission 4801 (WebhooksView).

  2. Stand up a receiver that keeps the raw body

    Your endpoint must be reachable over HTTPS on a public address, and it must be able to see the request body as the exact bytes that were sent. In Express that means mounting a raw body parser on the webhook route — express.json() parses and discards those bytes, and the signature can then never verify.

    const express = require("express");
    const crypto = require("crypto");
    
    const app = express();
    const SECRET = process.env.NZ_WEBHOOK_SECRET;
    
    // RAW body on this route. Not express.json().
    app.post(
      "/nyumbazetu",
      express.raw({ type: "application/json" }),
      (req, res) => {
        const ok = verify(req.body, req.headers, SECRET);
        if (!ok) return res.status(400).send("bad signature");
    
        // 2xx first, work afterwards.
        res.status(200).send("ok");
    
        const event = JSON.parse(req.body.toString("utf8"));
        console.log(event.event, event.executionId);
      }
    );
    
    function verify(rawBody, headers, secret, toleranceSec = 300) {
      const ts = headers["x-nz-webhook-timestamp"];
      const sig = headers["x-nz-webhook-signature"];
      if (!ts || !sig) return false;
      if (!/^\d+$/.test(String(ts))) return false;
      if (Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > toleranceSec) return false;
    
      const expected =
        "v1=" +
        crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`, "utf8").digest("hex");
    
      const a = Buffer.from(sig);
      const b = Buffer.from(expected);
      return a.length === b.length && crypto.timingSafeEqual(a, b);
    }
    
    app.listen(3000);
    

    Verifying signatures explains every line of verify, and has Python and PHP equivalents.

  3. Register the endpoint

    curl -s -X POST https://api.nyumbazetu.com/v3/webhooks \
      -H "Authorization: Bearer $NZ_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Ops notifier",
        "description": "Payments and invoices into our ops queue",
        "url": "https://hooks.example.com/nyumbazetu",
        "eventTypes": ["cash.payment.received", "billing.invoice.issued"]
      }'
    
    {
      "success": true,
      "data": {
        "uuid": "9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03",
        "name": "Ops notifier",
        "description": "Payments and invoices into our ops queue",
        "url": "https://hooks.example.com/nyumbazetu",
        "eventTypes": ["cash.payment.received", "billing.invoice.issued"],
        "enabled": true,
        "branchId": null,
        "lastTriggeredAt": null,
        "lastSuccessAt": null,
        "lastFailureAt": null,
        "totalAttempts": 0,
        "totalSuccesses": 0,
        "totalFailures": 0,
        "createdAt": "2026-09-30T09:12:44.000Z",
        "signingSecret": "4f2c…64 hex characters…b7e1",
        "subscriptions": 2
      },
      "message": "Created. Store the signing secret now — it cannot be shown again."
    }
    

    The organisation comes from your token, never the body. Requires permission 4802 (WebhooksCreate).

  4. Fire a test event

    This sends a synthetic event through the real delivery path — same signing, same address checks, same delivery log — so a pass here means a real delivery will pass too.

    curl -s -X POST \
      https://api.nyumbazetu.com/v3/webhooks/9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03/test \
      -H "Authorization: Bearer $NZ_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{ "eventType": "cash.payment.received" }'
    
    {
      "success": true,
      "data": {
        "delivered": true,
        "statusCode": 200,
        "durationMs": 412,
        "error": null
      }
    }
    

    eventType is optional and defaults to cash.payment.received. Requires permission 4803 (WebhooksEdit).

  5. Confirm what the platform saw

    curl -s "https://api.nyumbazetu.com/v3/webhooks/9f1c7e5a-3b2d-4f80-8c11-6a2d9e4b7c03/executions?limit=10" \
      -H "Authorization: Bearer $NZ_TOKEN"
    

    Each row carries the event type, the URL, the response status, the duration and any error message. Sensitive request headers — including the signature itself — are stored redacted, so this log can be read without exposing a live key. Requires permission 4801 (WebhooksView).

You now have

  • An endpoint receiving signed deliveries for two events.
  • A verified signature, checked against the raw body inside a 300-second window.
  • A delivery log you can read when something looks wrong.