Webhooks
Push humanhours events into your stack. Pro and above.
We sign every delivery with HMAC-SHA256 in an X-Humanhours-Signature header. Failed deliveries retry up to 5 times with exponential backoff.
Subscribe
Open /webhooks in the dashboard, or POST to /v1/webhooks:
curl -X POST https://humanhours.dev/v1/webhooks \
-H "Authorization: Bearer $HUMANHOURS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-stack.example/webhooks/humanhours",
"event_types": ["event.created", "weekly.digest"],
"description": "Production hook"
}'
The response includes the webhook secret. It's shown once. Store it; you'll use it to verify signatures.
Event types
| Type | Fires | Payload |
|---|---|---|
event.created | After every successful POST /v1/track | The full track response |
agent.first_event | First time an agent_id appears in your org | { agent_id, event } |
weekly.digest | Every Monday 09:00 UTC | Aggregates for the prior 7 days |
Verify signatures
The X-Humanhours-Signature header looks like:
X-Humanhours-Signature: t=1714998000,v1=a1b2c3...
To verify: build signed_payload = "{t}.{raw_body}", HMAC-SHA256 it with your webhook secret, and compare in constant time against the v1= value.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=") as [string, string]),
);
const t = parts["t"];
const v1 = parts["v1"];
if (!t || !v1) return false;
const signed = `${t}.${rawBody}`;
const expected = createHmac("sha256", secret).update(signed).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Reject any request older than 5 minutes (use the t value) to defeat replay attacks.
Retries
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | 30s |
| 3 | 2min |
| 4 | 10min |
| 5 | 1h |
After 5 consecutive failures we mark the webhook failing in the dashboard. After 20 we deactivate it and email the org admins.
Test from the dashboard
The webhook detail page has a Send test ping button that fires a synthetic test.ping payload signed with the same secret. The full delivery (status code, response body, signature) is logged for debugging.