DOCS / API / WEBHOOKS
VIEW RAW

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

TypeFiresPayload
event.createdAfter every successful POST /v1/trackThe full track response
agent.first_eventFirst time an agent_id appears in your org{ agent_id, event }
weekly.digestEvery Monday 09:00 UTCAggregates 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

AttemptDelay
1immediate
230s
32min
410min
51h

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.


Found a typo or want to suggest an edit? Email support@triadagency.ai.