Webhooks send TimeLeaf events, such as an approved leave request or a submitted timesheet, to a URL you choose as they happen. TimeLeaf has no public REST API for reading or writing data; webhooks are how you connect other systems.
What you need
- The Business plan or above. On a lower plan the Webhooks menu entry is hidden and nothing is sent. Endpoints you already set up are kept, and sending starts again after an upgrade.
- An admin account.
- An https URL on a public address that can receive a
POST. Private, loopback, link-local and cloud-metadata addresses are refused when you save the URL, and again at every delivery.
Add an endpoint
Open Webhooks
Go to Integrations › Webhooks and click Add endpoint.

Enter the URL and events
Enter the Endpoint URL and an optional Description. Under Events, tick the events you want, or tick All events, including ones added later. Click Add endpoint.
Copy the signing secret
TimeLeaf shows the Signing secret once. Click Copy, store it with your receiving system, and click Done. After this only its first characters are shown. There is no way to show it again or rotate it; to get a new one, delete the endpoint and add it again.
Send a test
Click Send test on the endpoint. TimeLeaf sends a webhook.test event once (it is never retried) and tells you the result.
Manage endpoints
Each endpoint on Integrations › Webhooks shows its URL, whether it is Enabled or Disabled, its events and the start of its secret.
- Disable stops deliveries. Retries that were waiting resume when you Enable it again.
- Deliveries shows the 50 most recent deliveries: event, status (delivered, pending or failed), response code, attempts and time.
- Delete removes the endpoint and its delivery history, after you confirm.
Event types
TimeLeaf supports 21 event types:
| Event | Trigger |
|---|---|
leave.requested | An employee submits a leave request |
leave.approved | A leave request is approved |
leave.rejected | A leave request is rejected |
leave.cancelled | A leave request is cancelled |
timesheet.submitted | An employee submits a weekly timesheet |
timesheet.approved | A timesheet is approved |
timesheet.rejected | A timesheet is rejected |
employee.created | A new employee is added |
employee.updated | An employee profile is updated |
employee.deactivated | An employee is deactivated |
shift.created | A shift is assigned |
shift.updated | A shift is modified |
job.created | A job is created. The shifts it makes do not fire shift.created |
job.updated | A job is edited, a day's hours change, someone is added to a day, or one of its days is cancelled. The shifts it changes do not fire shift.updated |
job.cancelled | A job is cancelled |
overtime.alert | An overtime threshold is reached |
timebank.accrued | Overtime hours are banked |
timebank.used | Banked hours are used |
timebank.adjusted | A manual time bank adjustment is posted |
timebank.expired | Banked hours expire |
timebank.payout | A time bank payout is requested or paid |
Job events
Jobs send their own events instead of one shift.created or shift.updated per shift. Creating, editing or cancelling a job, or one of its days, sends one job.* event and no per-shift events for the shifts it makes, changes or cancels. Taking someone off a place, replacing them, assigning an open place, or filling places from the usual people does send shift.updated for each shift it changes, with assignmentId, userId (null when the place is left open) and date.
| Event | data |
|---|---|
job.created | jobId, locationId (the job site), title (or null), startDate, endDate, days (days booked), shifts (shifts made) and openShifts (places nobody holds yet) |
job.updated | After an edit of the whole job: jobId, locationId, title, startDate, endDate, and retimedShifts, madeShifts, cancelledShifts and openedShifts. After a change to one day's hours: the same, plus changedDay. After someone is added to a day: jobId, addedPlaceDay and the four counts. After one day is cancelled: jobId, cancelledDay and cancelledShifts |
job.cancelled | jobId and cancelledShifts |
timebank.payout carries hours (the banked hours debited from the balance), payoutMultiplier (the cash-out rate stored on the payout, 1 when none) and payableHours (the hours to pay in cash: hours × payoutMultiplier). Pay from payableHours, not hours.
The request
A webhook is a POST with a JSON body { "event", "timestamp" (ISO 8601), "data" }, and these headers:
Content-Type: application/jsonX-Webhook-Signature: sha256=<hex-encoded HMAC>— an HMAC-SHA256 of the raw request body, keyed with the endpoint's signing secretX-Webhook-Id: <delivery id>User-Agent: TimeLeaf-Webhooks/1.0
To verify a delivery: read the raw body, compute an HMAC-SHA256 of it with your signing secret, and compare against the value after sha256=.
A 2xx response within 10 seconds counts as success. Redirects are never followed — a 3xx counts as a failed attempt.
Webhook payloads include the event type, a timestamp, and the key fields for that event (the request, employee, shift, or time-bank record involved) — not necessarily every column on the underlying record.
Retries and delivery guarantees
- A failed delivery is tried up to 3 times in total (the original attempt plus 2 retries): the first retry ~30 seconds after the failure, the second ~2 minutes after that.
- Retries run on a periodic sweep, so a retry can arrive up to about 15 minutes later.
- After the third attempt fails, the delivery is marked failed and not retried again.
- A delivery still unsent 24 hours after the event occurred expires and is never sent.
- Delivery is at least once — the same delivery can arrive more than once, so de-duplicate on
X-Webhook-Id. There's no ordering guarantee across events.