Version

Webhooks Quickstart

Register a target, subscribe to an event, receive a signed payload, verify it and respond 200, end to end.

Zuse Bifrost delivers webhook POSTs to any publicly reachable HTTPS URL you register. This page walks the happy path from zero.

Register a target

In the app, open Settings → Webhooks and add an endpoint: a name, the https:// URL, the events to subscribe, and optionally the project it should be limited to and the addresses it may resolve to. Over the REST API the same registration is one call on the API host, behind the webhook-endpoint:create ability:

POST /v1/webhook-endpoints HTTP/1.1
Authorization: Bearer $ZUSEGENIUS_TOKEN
Idempotency-Key: 1b4e28ba-2fa1-11d2-883f-0016d3cca427
Content-Type: application/json

{
  "name": "CI listener",
  "url": "https://hooks.example.com/zusegenius",
  "events": ["project.created", "project.status_changed"]
}

The response includes a one-time secret field. Store it: you need it to verify signatures and you cannot retrieve it again.

The URL has to be https:// on a public host. A loopback, private or link-local address, a local name, or a public name that resolves to a private address is refused at registration, and so is a name that does not resolve at all. See security.

Receive the POST

Every delivery lands on your URL as a JSON POST:

POST /zusegenius HTTP/1.1
ZG-Event-Id: 01HXDELIVERY000000000TST
ZG-Event-Name: project.created
ZG-Signature: t=1762041600,v1=4a2b...
Content-Type: application/json
User-Agent: ZuseBifrost-Webhooks/1.0

{"id":"evt_01HX...","type":"project.created",...}

Verify the signature

Every payload is signed with HMAC-SHA256 over <timestamp>.<raw_body>. The signature verification page has the recipe in three languages. The short form in Node:

import crypto from "node:crypto";

function verify(header: string, body: string, secret: string): boolean {
  const [tPart, v1Part] = header.split(",");
  const t = tPart.split("=")[1];
  const v1 = v1Part.split("=")[1];
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${body}`)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Reject the request if the signature does not match or the timestamp is more than five minutes old.

Respond 200 quickly

Return 200 OK well inside the 30-second budget. Anything else counts as a failure and triggers a retry. If your work takes longer, enqueue a background job and respond immediately.

De-duplicate on the event id

The ZG-Event-Id header (and the id in the payload, with its evt_ prefix) is unique per event. Store it and short-circuit if you have seen it before: retries can make the same event arrive more than once, and two endpoints subscribed to the same event each receive it.

Prove it works

Before any real event fires, send a test delivery from the endpoint's menu in Settings → Webhooks. It posts a synthetic project.created envelope whose data carries synthetic: true, signed with the current secret and routed through the same queue as a real event, so DNS, TLS and your verification are exercised end to end. The delivery and its outcome appear in Settings → Webhooks → Deliveries like any other.

What next

How is this guide?

Last updated on

Version

On this page

What changed recentlyEvery release of Zuse Bifrost, with the pages it touched.

For AI agents: llms.txt · llms-full.txt