Webhooks
Use webhooks when your system needs to react to HollyHR changes without polling. In the beta API, webhook events are emitted for changes made through the public API write surface: people, employment, pending time-off records, time-off categories, time-off settings, working-pattern setup, and org-unit setup.
UI-origin and domain-wide event emission is not part of the beta surface yet. For example, a change made by a user inside the HollyHR app does not emit a public API webhook just because the same event type exists for public API-origin writes.
Create an endpoint
Create webhooks through the API reference or with:
Code
The API key must include webhooks:manage plus the read scopes required for
the subscribed event payloads. For example, person.created requires
people:read, and time_off.created requires time_off:read. Endpoint URLs
must be public HTTPS URLs. Localhost, private IP ranges, local domains, URL
credentials, fragments, and private DNS resolutions are rejected.
Org-unit lifecycle events such as org_unit.created and org_unit.archived
require org_units:read.
Create and rotate responses return the plaintext signing secret once. Store it
securely. Later reads only expose secret_last_four, and idempotency replays
return signing_secret: null.
Event payload
Each delivery uses a small wrapper:
Code
Webhook payloads deliberately exclude compensation, bank details, tax or
government identifiers, dates of birth, home addresses, demographics, document
bytes, and free-text sensitive notes. Fetch elevated personal data through
GET /people/{personId}/personal with people:personal:read when needed.
Event families
Beta webhook events are emitted only for successful public API writes. Each event carries a thin, versioned payload with the event id, type, occurrence time, resource id, and safe projection data. Fetch current state through REST when you need more context.
| Family | Events | Required endpoint scope | Follow-up read route |
|---|---|---|---|
| Synthetic test | webhook.test | webhooks:manage | None; use the returned signed test payload. |
| People and employment | person.created, person.updated, person.ended, person.reactivated | people:read | GET /people/{personId} |
| Time off requests | time_off.created, time_off.updated, time_off.cancelled | time_off:read | GET /time-off/{timeOffId} |
| Time-off configuration | time_off_category.created, time_off_category.updated, time_off_category.archived | reference:read | GET /time-off/categories |
| Time-off settings | time_off_settings.updated | time_off:read | GET /time-off/settings |
| Organisation units | org_unit.created, org_unit.updated, org_unit.archived, org_unit.reactivated | org_units:read | GET /org-units/{orgUnitId} |
| Working patterns | working_pattern.created, working_pattern.updated, working_pattern.archived | working_patterns:read | GET /working-patterns/{workingPatternId} |
Endpoint subscriptions are scope-checked when you create or update a webhook.
For example, a webhook that subscribes to both person.updated and
working_pattern.updated needs webhooks:manage, people:read, and
working_patterns:read.
Verify signatures
HollyHR signs the raw JSON request body with HMAC-SHA256 over:
Code
The signature is sent as:
Code
During the 24-hour secret rotation overlap, HollyHR includes signatures for the current secret and the still-valid previous secret.
Always verify:
HollyHR-Webhook-SignatureHollyHR-Webhook-Timestamp- the raw request body, before JSON parsing changes it
- the event id, so duplicate deliveries are ignored safely
Headers
| Header | Meaning |
|---|---|
HollyHR-Webhook-Id | Stable event id, for deduplication. |
HollyHR-Webhook-Delivery-Id | Delivery id for this endpoint/event pair. |
HollyHR-Webhook-Event | Event type. |
HollyHR-Webhook-Timestamp | Unix timestamp included in the signed input. |
HollyHR-Webhook-Signature | HMAC signatures for current/previous secret. |
Retries and duplicates
Deliveries are at-least-once. Your endpoint should return a 2xx response only
after it has safely accepted the event. Non-2xx responses, timeouts, 408,
429, and 5xx responses are retried with bounded backoff. Non-retryable
4xx responses become failed deliveries.
Deduplicate by HollyHR-Webhook-Id or your own stored delivery id before
performing side effects.
If an active endpoint accumulates 10 retained terminally failed deliveries, HollyHR automatically disables the endpoint and creates an in-app notification for tenant system admins. The notification includes the endpoint name, failure threshold, and latest sanitized failure summary, and links to API settings. It does not include payload bodies, response bodies, signatures, signing secrets, or raw delivery payload data.
Fix the receiver, inspect the health and delivery logs, then re-enable the
webhook with PATCH /webhooks/{webhookId} before redelivering failed events.
Automatic disablement is audited and does not delete delivery history.
Delivery logs
Check endpoint health when you need an operational summary before paging through delivery logs:
Code
The health response includes the endpoint status, derived health status,
delivery counts by status, recent success/failure timestamps, retry timing, and
the latest sanitized failure summary. failing means at least one retained
delivery is terminally failed and needs inspection. recovering means delivery
is still pending or scheduled for retry. unknown means no retained delivery
history exists yet.
Use delivery logs when you need to debug failed or delayed webhook deliveries:
Code
List responses include delivery status, attempt count, retry timing, last HTTP status, failure class, and sanitized failure message. Fetch one delivery to see ordered attempt history:
Code
Delivery logs never return payload bodies, request bodies, response bodies,
signatures, or signing secrets. The resource field is null unless the API
key also has the read scope for the event resource, for example people:read
for person.updated.
Failure classes are network, http_retryable, and http_non_retryable.
Terminal delivered and failed delivery logs are retained for 30 days.
Pending and retry_scheduled deliveries are not pruned by retention cleanup.
Do not depend on indefinite delivery-log availability.
Redeliver a failed delivery
If your receiver was down or rejected a delivery, fix the receiver first, then queue the existing delivery for another attempt:
Code
Redelivery is allowed only when the webhook is active and the delivery is
failed or retry_scheduled. HollyHR keeps the same event and delivery ids,
preserves prior attempt history, and resets the delivery for the next worker
run. Continue deduplicating by HollyHR-Webhook-Id.
Test delivery
Use the test endpoint to queue a synthetic webhook.test event:
Code
The response includes the signed headers and payload shape so you can verify your receiver before relying on production events.
API reference
Use the Webhooks section in the API reference for the exact request and response schemas:
GET /webhooksPOST /webhooksGET /webhooks/{webhookId}PATCH /webhooks/{webhookId}DELETE /webhooks/{webhookId}POST /webhooks/{webhookId}/rotate-secretPOST /webhooks/{webhookId}/testGET /webhooks/{webhookId}/healthGET /webhooks/{webhookId}/deliveriesGET /webhooks/{webhookId}/deliveries/{deliveryId}POST /webhooks/{webhookId}/deliveries/{deliveryId}/redeliver