Build apps
Webhooks
Register event subscriptions that deliver Hane events to your systems as webhooks, verify signatures, and handle retries and pauses.
This page explains how to receive events such as case completions and new Tasks in your own systems through webhooks. Hane records each event as a webhook delivery per subscription, signs it, and keeps retrying until it is delivered.
Events you can subscribe to
Section titled “Events you can subscribe to”| type | Fields in data |
|---|---|
workflows.case.completed |
case_id, workflow, outcome |
workflows.case.failed |
step |
workflows.case.cancelled |
— |
approvals.task.created, approvals.task.completed |
case_id (plus outcome on completion) |
data.record.created, data.record.updated |
type, version |
data.import.completed |
type, rows, created, updated, failed |
documents.version.created |
document_id, version |
integrations.sync.succeeded, integrations.sync.failed |
id, connection |
integrations.export.completed, integrations.export.failed |
id, connection, type, and more |
agents.run.succeeded, agents.run.failed, agents.run.cancelled, agents.run.limit_exceeded |
run_id, agent, status, case_id |
data contains only fields that are safe to send outside. Fields that may contain business values are left out, so receivers read the details again from the public API by ID.
Register a subscription
Section titled “Register a subscription”Register with the workflows:event-subscriptions.manage scope. The destination must be an HTTPS URL with a public DNS name.
const sub = await unwrap( hane.POST("/v1/workflows/event-subscriptions", { body: { name: "erp-sync", url: "https://erp.example.com/hooks/hane", event_types: ["workflows.case.completed", "approvals.task.created"], }, }),);// sub.signing_secret (whsec_...) is returned only in this response. Store it with your secrets- A project can have up to 20 subscriptions. A subscription with the same name returns 409.
- Change the destination and types with
PATCH /v1/workflows/event-subscriptions/{id}(If-Match required). - Deleting a subscription cancels unfinished deliveries and deletes the signing secret. Delivery records stay readable for 30 days.
The request you receive
Section titled “The request you receive”The body is CloudEvents JSON.
POST /hooks/hane HTTP/1.1content-type: application/cloudevents+jsonwebhook-id: <delivery ID>webhook-timestamp: 1793952000webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{"specversion":"1.0","id":"...","source":"...","type":"workflows.case.completed","time":"...","datacontenttype":"application/json","tenantid":"...","projectid":"...","environment":"dev","data":{"case_id":"...","workflow":"lease-expiry","outcome":"..."}}Verify the signature
Section titled “Verify the signature”Signatures follow Standard Webhooks, so you can verify them with an off-the-shelf library. To verify yourself, compute HMAC-SHA256 over <webhook-id>.<webhook-timestamp>.<body> with the key obtained by base64-decoding the part of the secret after whsec_, and compare it with the base64 after v1,.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret: string, headers: Headers, body: string): boolean { const id = headers.get("webhook-id") ?? ""; const ts = headers.get("webhook-timestamp") ?? ""; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // refuse requests that are too old const key = Buffer.from(secret.slice("whsec_".length), "base64"); const expected = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest(); // while a secret is being rotated, two signatures are listed, separated by a space. Accept if any matches return (headers.get("webhook-signature") ?? "").split(" ").some((sig) => { const given = Buffer.from(sig.replace(/^v1,/, ""), "base64"); return given.length === expected.length && timingSafeEqual(given, expected); });}Verify against the body exactly as received. Don’t parse the JSON and re-serialize it before verifying.
Retries and duplicates
Section titled “Retries and duplicates”- A 2xx response counts as delivered. A 3xx response is a failure (redirects are not followed), and no response within 10 seconds is a timeout.
- After a failure, Hane retries up to 8 times, waiting 10 seconds, 30 seconds, 2 minutes, 10 minutes, 30 minutes, 1 hour, 3 hours, and 6 hours (about 10.6 hours in total). A delivery that uses up its retries fails, and you can requeue it from
/v1/audit/dead-letters. - Delivery is at least once, so the same delivery can arrive twice. Receivers should drop duplicates by the
webhook-idvalue. - Hane does not read or keep the receiver’s response body.
Pause and resume
Section titled “Pause and resume”A subscription whose receiver has failed for 5 days or more and at least 20 times in a row is paused (paused). While paused, events are kept as deliveries and never discarded. After fixing the receiver, resume the subscription. On resume, Hane gradually resends the undelivered deliveries with the same webhook-id.
hanectl subscriptions listhanectl subscriptions deliveries <id> --limit 20hanectl subscriptions delivery <id> <delivery_id> # result of each attempt (status code, failure reason, latency)hanectl subscriptions resume <id>Rotate the secret
Section titled “Rotate the secret”const r = await unwrap(hane.POST("/v1/workflows/event-subscriptions/{id}/rotate-secret", { params: { path: { id } }, body: {} }));// r.signing_secret is the new value. Until r.previous_secret_expires_at, both old and new values signFor 24 hours, deliveries are signed with both the old and new values, so move your receiver to the new value within that window.