Webhooks Overview
Webhooks send an HTTPS request to your server when something happens in BookYourPTO, such as a leave request being approved, so your integration does not have to poll the API for changes.
Webhooks follow the same rules as the REST API. An endpoint only receives events about records its owner could read through the API, and a payload never contains more than the matching API endpoint returns.
Requirements
- The same conditions as the API: a paid plan (Pro, Business or Enterprise), the API switched on, and a subscription in good standing. They are checked again before every delivery attempt, not only when the endpoint is created. If the API is switched off, the plan no longer includes it or the subscription lapses, nothing more is sent: queued deliveries are cancelled, not sent later.
- Only Administrators and Executives can create, own and manage webhook endpoints. This is fixed: allowing other roles to create API tokens does not let them manage webhooks.
Endpoint owner
Every endpoint is owned by one person. By default that is the person who creates it. They can instead choose someone whose role is below their own: an Executive can create an endpoint owned by an Administrator, but an Administrator cannot create one owned by another Administrator or by an Executive.
Payloads are built with the owner's visibility, exactly as a token owned by that person would read them:
- An event about a record the owner cannot see is not delivered to that endpoint. Nothing is sent: no empty payload, no id.
- A private leave is redacted the same way it is for the owner in the API.
- The owner must still be eligible when each delivery is attempted. If the owner is deactivated, offboarded or no longer an Administrator or Executive, queued deliveries are cancelled instead of sent.
- When the owner is offboarded, or their personal data is deleted, every endpoint they own is turned off at the same moment. Reactivating the person later does not turn it back on: an Administrator or Executive gives the endpoint a new owner and resumes it.
Who can change an endpoint
Because an endpoint reads with its owner's visibility, changing where it sends or what it receives is held to the same rule as choosing its owner:
| Action | Who may do it |
|---|---|
| See the endpoint and its delivery log | Any Administrator or Executive |
| Pause or delete it | Any Administrator or Executive |
| Resume it, send a test event, redeliver | The owner, or anyone whose role is the same as or above the owner's |
| Change its URL, events or owner, or rotate its signing secret | The owner, or anyone whose role is above the owner's |
The full receiver URL is shown only to people who may change the endpoint; everyone else sees the host name, because a receiver URL often works as a credential.
Creating an endpoint
Endpoints are managed on the Webhooks tab under Settings > API. Choose Add endpoint, then fill in:
| Setting | Rules |
|---|---|
| Endpoint URL | Must start with https://, use port 443 or 8443, contain no user name or password, and name a public host rather than an IP address. At most 2048 characters. Every address the host name resolves to must be public; this is checked when you save and again before every delivery attempt. |
| Events | One or more event types from the events reference, or Every event, which subscribes to every event the owner's role may receive, including event types added later. |
| Description | Optional, up to 200 characters. |
| Owner | You, or someone whose role is below yours (see above). |
When you save, the signing secret is shown once. It starts with whsec_. Copy it into your receiver's secret store; it cannot be displayed again. If it is lost, rotate it.
You can also manage endpoints through the API itself, which is how integration platforms subscribe with "REST hooks". See Managing endpoints through the API below.
Status
| Status | Shown as | Meaning |
|---|---|---|
active | Active | Receives events. |
paused | Paused | Paused by an Administrator or Executive. Pausing cancels every queued delivery. |
disabled | Turned off | Turned off automatically, either after a long run of failed deliveries (see Retries & Failures) or because its owner was offboarded. |
Resuming an endpoint replays nothing. Events raised while it was paused or turned off are not queued for it; failed deliveries can be redelivered from the delivery log.
Limits
- Up to 10 webhook endpoints per organization.
- Up to 50 event types per endpoint. Every event is a single entry and cannot be combined with individual events.
The payload
Every delivery is a POST with a JSON body:
{
"id": "evt_01JABCDEFGHJKMNPQRSTVWXYZ",
"type": "leave.approved",
"api_version": "v1",
"created_at": "2026-10-12T17:04:11.512Z",
"organization_id": "cmorg0000000000000000000",
"data": {
"object": {
"id": "cmlve0hmarsh2026080300",
"status": "APPROVED",
"...": "the other fields GET /leaves/{id} returns"
}
},
"previous": { "status": "PENDING" }
}
| Field | Meaning |
|---|---|
id | The event id: evt_ followed by 26 characters. Unique per event and the same on every retry and redelivery. Use it to discard duplicates. It is also sent in the webhook-id header. |
type | The event type, for example leave.approved. |
api_version | The API version the payload follows, currently v1. |
created_at | When the event happened, as an ISO 8601 UTC instant. |
organization_id | The organization the event belongs to. |
data.object | The record, in the same shape the API returns it, as the endpoint's owner sees it. |
previous | On update and status-change events: the old values of the fields that changed, for example { "status": "PENDING" }. Left out when there is nothing to report. |
The events reference has a complete example payload for every event type.
What a payload never contains
Payloads carry only the standard fields of a record, whatever scopes the owner holds. Personal details (date of birth, home address, emergency contact), government identifiers, compensation, timesheet pay figures, clock-in and clock-out locations and document files are never sent by webhook, and neither are secrets, tokens or storage paths. Fetch those through the API if your integration needs them and its token is allowed to read them.
Large records and deletions
A payload is limited to 64 KB. A record that would exceed it is sent as a thin event: data.object contains only its id and an object field naming the kind of record, for example { "id": "cmexp...", "object": "expense_report" }. Fetch the full record through the API.
department.deleted is always a thin event, because the department no longer exists.
Delivery guarantees
- Delivery is at least once: the same event can arrive more than once. De-duplicate on
id. - Delivery is not ordered: a later event can arrive before an earlier one. Use
created_at, or fetch the current record from the API, when order matters. - The first attempt is normally made within about a second of the change being saved. Anything that attempt does not send is picked up by a job that runs every minute.
Testing an endpoint
Choose Send test event in the endpoint's menu to send a webhook.test event right away. The endpoint must be active. Open the endpoint's Delivery log to see the status code your server returned and how long it took.
A test event is signed exactly like every other event, so it is the quickest way to check your signature verification.
Managing endpoints through the API
Tokens owned by an Administrator or Executive can manage webhook endpoints with two scopes:
| Scope | Allows |
|---|---|
webhooks:read | List endpoints, read one, read its delivery log, and list the event types (GET /webhook-events) |
webhooks:write | Create, update, pause, resume and delete endpoints, send a test event and redeliver |
webhooks:write does not let a token read anything it could not read already. Subscribing an endpoint to an event, or changing an endpoint's URL, also needs the token to hold the read scope of every event the endpoint will receive (for Every event, ["*"], all of them); otherwise the answer is 403 SCOPE_MISSING. The read scope for each event is listed in the events reference and returned as required_scope by GET /webhook-events.
curl -X POST https://app.bookyourpto.com/api/v1/webhooks \
-H "Authorization: Bearer $BOOKYOURPTO_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0f8e2a41-6c3d-4b7e-9a15-2d4c6e8f1b30" \
-d '{
"url": "https://hooks.example.com/hr/events",
"events": ["leave.approved", "leave.cancelled"],
"description": "Payroll system: leave changes"
}'
The 201 response includes signing_secret, once. Through the API an endpoint's url is always returned masked to its host, for example https://hooks.example.com/…. Rotating the signing secret and redelivering every failed delivery at once are done in Settings > API.
Every operation is listed under Webhooks in the API Reference.
document.signed and document.declined, with the same receiver URL. Deliveries now use the signature scheme described in Verifying Signatures. Update the receiver, then resume the endpoint.Next steps
Expenses & Documents
Export expense reports to accounting, approve or reject them, and read document metadata, signers and files through the API, with the visibility rules that apply.
Events Reference
Every webhook event type, when it fires, the read scope its endpoint's owner needs, and a complete example payload for each.