Developer docs
Webhooks.
Hours AI tells your other systems what happened. Paste a Slack or Teams URL for a message, or your own https endpoint for signed JSON.
1. Add a webhook
In Hours AI, open Settings → Webhooks, give it a name, paste the URL and tick the events you want (none ticked means all). Slack and Teams URLs are recognised and get a message card; anything else gets JSON. The signing secret is shown once.
Send test delivers a webhook.test event immediately and shows the response code, so you can confirm the connection before anything real happens.
2. Events
| Type | When |
|---|---|
schedule.published | A schedule was published |
timeoff.requested | An employee requested time off |
timeoff.approved | A time-off request was approved |
swap.approved | A shift swap or pickup was approved |
clock.in | An employee clocked in |
clock.out | An employee clocked out |
timesheet.approved | A week of timesheets was approved |
payroll.exported | A payroll file was exported |
sales.recorded | Sales were recorded through the ingest |
Payloads carry ids, the location, and the fields that changed. They never include wages, pay rates or contact details; use the ids with your own records.
3. What you receive
POST https://your-endpoint.example/hooks/hours Content-Type: application/json X-Hours-Event: schedule.published X-Hours-Delivery: 9d7e… (unique per delivery) X-Hours-Signature: t=1789070400,v1=3f9a…
{
"id": "5b2c8f4e-1a0d-4d1e-9e0c-2a3f4b5c6d7e",
"type": "schedule.published",
"createdAt": "2026-09-10T20:00:00.000Z",
"location": { "id": "6cd5105c-…", "name": "Harvest Table" },
"data": {
"scheduleId": "a1b2c3…",
"weekStart": "2026-09-14T00:00:00.000Z",
"shiftCount": 42
}
}Reply with any 2xx within 10 seconds. Anything else, or a timeout, is retried.
4. Verify the signature
The signature is an HMAC-SHA256 of <t>.<raw body> with your secret, hex-encoded, where t is the Unix time the delivery was signed. Compare in constant time and reject timestamps older than five minutes.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, signatureHeader, rawBody) {
const parts = Object.fromEntries(signatureHeader.split(",").map((kv) => kv.split("=")));
const t = Number(parts.t);
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // 5 minutes
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "hex"), b = Buffer.from(parts.v1, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}5. Retries and pausing
- A failed delivery retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 12 hours, then is marked dead.
- Five consecutive failures pause the webhook and show the last error in Settings. Resume it once the endpoint is back; any delivery can be resent from the log.
- Deliveries are at-least-once. Use
X-Hours-Delivery(or the envelopeid, shared by every subscriber of one event) to ignore repeats.
Security
- Only https URLs are accepted.
- Secrets are stored encrypted and can be rotated in Settings.
- Webhooks are per location and can only be managed by that location’s managers.
Sending sales to Hours AI instead? Connect any POS.