HollyHR Developer Docs
  • HollyHR
  • Sign in
  • Manage API keys
  • Start Here
  • Core API
  • AI and MCP
  • API Reference
  • Recipes
  • Resources
RequestsPagination examplesEnvironments and testingWebhooksProvider readinessOpenAPI imports
Core API

Requests

The API accepts and returns JSON. List endpoints use cursor pagination where a large result set is possible.

Pagination

List responses that can be paginated return a pagination object:

Code
{ "data": [], "pagination": { "next_cursor": "cursor_...", "has_more": true, "limit": 50 } }

Pass next_cursor back as the cursor query parameter to request the next page. Do not parse cursor values; treat them as opaque strings. For endpoint-specific loops, filters, and retry behavior, use the pagination examples.

Incremental sync

People, time-off, and document metadata support updated_since for polling jobs:

Code
GET /api/v1/people?updated_since=2026-06-16T09:00:00.000Z

Use ISO-8601 UTC timestamps and follow pagination until has_more is false.

Idempotency

Write requests require an Idempotency-Key header so retries cannot create or apply the same HR change twice:

Code
Idempotency-Key: idem_20260618_update_ada_title

Reusing the same key with the same method, route, path resource, version precondition, and request body replays the original response. Reusing it with a different body, person id, or If-Match value returns idempotency_conflict.

Conditional updates

Mutable updates use ETags to avoid lost updates. First read the resource you are going to update and keep the exact strong ETag response header:

TerminalCode
curl -i "$HOLLYHR_API_BASE_URL/people/{person_id}" \ -H "Authorization: Bearer $HOLLYHR_API_TOKEN" \ -H "Accept: application/json"

Then send that exact value back as If-Match when patching safe setup fields:

TerminalCode
curl -X PATCH "$HOLLYHR_API_BASE_URL/people/{person_id}" \ -H "Authorization: Bearer $HOLLYHR_API_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: idem_20260618_update_ada_title" \ -H 'If-Match: "etag-from-get-response"' \ --data '{"job_title":"Principal Engineer"}'

The same If-Match rule applies to lifecycle actions such as ending or reactivating a person. Focused employment updates use the employment resource's own ETag:

TerminalCode
curl -X POST "$HOLLYHR_API_BASE_URL/people/{person_id}/end" \ -H "Authorization: Bearer $HOLLYHR_API_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: idem_end_ada_001" \ -H 'If-Match: "etag-from-get-response"' \ --data '{"end_date":"2026-07-31"}'
TerminalCode
curl -i "$HOLLYHR_API_BASE_URL/people/{person_id}/employment" \ -H "Authorization: Bearer $HOLLYHR_API_TOKEN" \ -H "Accept: application/json"
TerminalCode
curl -X PATCH "$HOLLYHR_API_BASE_URL/people/{person_id}/employment" \ -H "Authorization: Bearer $HOLLYHR_API_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: idem_20260618_update_employment" \ -H 'If-Match: "employment-etag-from-get-response"' \ --data '{"job_title":"People Lead","start_date":"2026-02-01"}'

Employment-history creation is append-style and requires Idempotency-Key. Unsupported free-text reason/notes and compensation fields are rejected:

TerminalCode
curl -X POST "$HOLLYHR_API_BASE_URL/people/{person_id}/employment-history" \ -H "Authorization: Bearer $HOLLYHR_API_TOKEN" \ -H "Accept: application/json" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: idem_20260618_add_employment_history" \ --data '{"job_title":"People Lead","start_date":"2026-03-01"}'

Missing If-Match returns precondition_required. A stale or non-exact ETag returns precondition_failed; read the person again, reconcile the current data, then retry with a new idempotency key.

Webhooks

Webhook management is available through /api/v1/webhooks for API keys with webhooks:manage. Webhook mutations require Idempotency-Key, and create/rotate responses reveal the plaintext signing secret once.

For setup, signatures, retries, duplicate handling, and testing, use the Webhooks guide. For exact request and response schemas, use the Webhooks section in the API reference.

Errors

Errors use a stable JSON envelope. HollyHR is deliberately keeping this custom v1 envelope instead of switching to RFC 7807 because every public API error can carry the same compact shape, stable machine code, and request id:

Code
{ "error": { "code": "permission_denied", "message": "Missing required scope.", "request_id": "req_..." } }

Validation errors may include details, an array of field-level issues. The message is human-readable and may become clearer over time; clients should branch on error.code, the HTTP status, and field identifiers in details.

Common codes:

StatusCodeMeaning
400invalid_requestInvalid query, path, cursor, or body shape.
401authentication_requiredMissing, malformed, unknown, or revoked key.
403permission_deniedPlan entitlement or scope is missing.
404not_foundResource not found or not visible to org.
409idempotency_conflictIdempotency key reused for a different write.
412precondition_failedConditional update used a stale ETag.
428precondition_requiredConditional update omitted If-Match.
429rate_limitedToo many requests.

Keep the request_id when asking HollyHR support to investigate a failed API request.

Rate limits

Rate limits are tenant/key aware. Public API requests are limited before authentication and again after authentication:

BucketDefault
Pre-authentication abuse protection60 requests per minute per IP and credential fingerprint.
Authenticated route bucket600 requests per minute per organisation, API key, and route.

Individual endpoints may use lower limits when a request is more expensive or triggers delivery side effects. Follow the API reference for endpoint-specific 429 responses.

Successful authenticated responses include:

HeaderMeaning
RateLimit-LimitThe current authenticated route bucket limit.
RateLimit-RemainingRequests remaining in the current route bucket window.
RateLimit-ResetSeconds until the current route bucket resets.

A 429 response includes Retry-After when a retry window is available. When rate-limit metadata is available, the response also includes RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. Back off until the reset window instead of retrying immediately.

Last modified on June 23, 2026
Pagination examples
On this page
  • Pagination
  • Incremental sync
  • Idempotency
  • Conditional updates
  • Webhooks
  • Errors
  • Rate limits
JSON
JSON