Hanzo

Webhooks

Deliver any platform event to your own HTTPS endpoints -- signed, retried, and logged. One registry at api.hanzo.ai/v1/webhooks.

Webhooks

Webhooks push platform events to your own HTTPS endpoints as they happen. You register an endpoint, choose which event subjects it receives, and Hanzo delivers each matching event with a signed, retried POST. Every service that publishes to the platform event bus is deliverable this way -- one registry, one delivery contract, for all global events.

There is one place for this: api.hanzo.ai/v1/webhooks. (Webhooks are a first-class resource, not nested under any single service.)

Register an endpoint

curl -X POST https://api.hanzo.ai/v1/webhooks \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/hanzo",
    "events": ["commerce.order.*", "commerce.checkout.*"],
    "description": "Order + checkout events"
  }'

The response includes a secret -- shown exactly once. Store it now; you use it to verify every delivery. If you lose it, rotate (below) rather than recreate.

events is a list of subject patterns using NATS wildcard semantics:

  • * matches exactly one token: commerce.order.* matches commerce.order.created but not commerce.order.line.added.
  • > matches one or more trailing tokens: commerce.> matches every commerce. subject.

Only events belonging to your own organization are ever delivered to your endpoints -- there is no cross-tenant delivery.

Manage endpoints

MethodPathPurpose
GET/v1/webhooksList your endpoints (with 7-day delivery/failure counts)
POST/v1/webhooksRegister an endpoint (returns the reveal-once secret)
GET/v1/webhooks/:idFetch one endpoint
PATCH/v1/webhooks/:idUpdate url, events, description, or status
DELETE/v1/webhooks/:idRemove an endpoint

Set "status": "disabled" via PATCH to pause delivery without losing the endpoint or its history; "active" resumes it.

Verify a delivery

Every delivery carries three headers:

HeaderValue
X-Webhook-Signaturet=<unix>,v1=<hex hmac-sha256>
X-Webhook-Eventthe event subject, e.g. commerce.order.completed
X-Webhook-Deliverya UUID, stable across retries of the same event (use it to dedupe)

The signature is computed over the timestamped raw body: HMAC-SHA256(secret, "<t>.<raw-body>"), hex-encoded. Recompute it from the raw request body -- not a re-serialized copy -- and compare in constant time. Reject anything that doesn't match.

import crypto from 'node:crypto'

export function verifyWebhook(secret, signatureHeader, rawBody) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((kv) => kv.split('=')),
  )
  const t = parts.t
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1 ?? '')
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"strings"
)

// VerifyWebhook reports whether sig ("t=…,v1=…") authenticates rawBody.
func VerifyWebhook(secret, sig string, rawBody []byte) bool {
	var t, v1 string
	for _, p := range strings.Split(sig, ",") {
		if kv := strings.SplitN(p, "=", 2); len(kv) == 2 {
			switch kv[0] {
			case "t":
				t = kv[1]
			case "v1":
				v1 = kv[1]
			}
		}
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(t + "."))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(v1))
}

Delivery, retries, and logs

Delivery is at-least-once. A fresh timestamp and signature are computed for each attempt, so a slow retry is never rejected as stale. Failed attempts back off:

  • Up to 3 attempts at roughly 1s, 5s, 25s (plus jitter), 10s timeout each.
  • Retries only on a network error, 5xx, or 429. Any 4xx is treated as permanent and stops immediately -- fix your endpoint, then resend with a test (below).

Because delivery is at-least-once and retries share one X-Webhook-Delivery id, make your handler idempotent: dedupe on that id, and return 2xx as soon as you've durably accepted the event.

Every attempt is logged. Read them back:

# recent attempts, newest first
curl "https://api.hanzo.ai/v1/webhooks/$ID/deliveries?limit=50" \
  -H "Authorization: Bearer $HANZO_API_KEY"

# only failures
curl "https://api.hanzo.ai/v1/webhooks/$ID/deliveries?status=failed" \
  -H "Authorization: Bearer $HANZO_API_KEY"

Each row carries the delivery id, subject, attempt number, status, HTTP status, duration, and any error.

Test an endpoint

Send a synthetic webhook.test event to any endpoint -- including a disabled one -- and get the result inline:

curl -X POST "https://api.hanzo.ai/v1/webhooks/$ID/test" \
  -H "Authorization: Bearer $HANZO_API_KEY"
# → {"delivered":true,"httpStatus":200,"durationMs":142}

The test runs through the exact signing and POST path a real delivery uses (single attempt, no retry ladder), so a passing test means live events will verify the same way. The attempt is recorded in the delivery log.

Rotate the secret

curl -X POST "https://api.hanzo.ai/v1/webhooks/$ID/rotate-secret" \
  -H "Authorization: Bearer $HANZO_API_KEY"
# → {"secret":"whsec_…"}  (shown once)

The old secret is invalid immediately. Update your verifier to the new value before rotating in production, or briefly accept either during the switch.

Events you can subscribe to

Subjects follow <service>.<object>.<action>. Live today:

  • commerce.order.created, commerce.order.completed, commerce.order.refunded
  • commerce.checkout.started

Subscribe with a pattern to future-proof: commerce.> receives every commerce subject, including ones added later. More event sources (agents, functions, identity, world) publish to the same bus and become deliverable through the same registry as they come online.

In the console

The Cloud console has a Webhooks product under Dev: register and enable endpoints, reveal and rotate secrets, send a test delivery and see the result inline, and browse per-endpoint delivery logs with a failures-only filter. The signature scheme and copy-paste verifiers above are shown right in the endpoint's Security panel.

How is this guide?

Last updated on

On this page