Skip to content
HaneDOCS日本語

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.

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 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 body is CloudEvents JSON.

POST /hooks/hane HTTP/1.1
content-type: application/cloudevents+json
webhook-id: <delivery ID>
webhook-timestamp: 1793952000
webhook-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":"..."}}

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.

  • 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-id value.
  • Hane does not read or keep the receiver’s response body.

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 list
hanectl subscriptions deliveries <id> --limit 20
hanectl subscriptions delivery <id> <delivery_id> # result of each attempt (status code, failure reason, latency)
hanectl subscriptions resume <id>
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 sign

For 24 hours, deliveries are signed with both the old and new values, so move your receiver to the new value within that window.