Skip to main content
This guide walks you through pushing employee absences — vacations, out-of-office days, sabbaticals — from an HRIS (or any system of record for time off) into Zeeg. Once a period exists in Zeeg, that user becomes unavailable for bookings on the affected days, on every scheduling page they own or share.
The endpoints in this guide live under the Availability Schedule API and require a token with schedules:write for mutations and schedules:read for reads. Mapping HRIS users to Zeeg additionally needs users:read (or admin:full).

Concepts

Three different objects affect a user’s availability. Pick the right one for the job: This guide focuses on time-off periods. Holiday subscriptions and weekly schedules are covered briefly at the end.

Time-off shape

A time-off period is all-day by default and identified by date, not datetime — there is no startTime/endTime/timeZone on the resource. Half-day flags (startHalfDay, endHalfDay) with HH:MM cutoffs let the first or last day be partial; the cutoff is interpreted in the user’s schedule timezone.

Prerequisites

  • A Zeeg admin or owner account with API access enabled.
  • An API token with the following scopes:
    • users:read (or admin:full) — list workspace users.
    • schedules:read — list existing time-off.
    • schedules:write — create, update, delete time-off.
  • The token owner must have edit rights on each target user’s schedules (i.e. be the user themselves, an admin/owner, or the team manager). Targets the token can’t edit are reported as failed — they don’t fail the whole request.

End-to-end sync flow

1

Map HRIS users to Zeeg users

Time-off endpoints accept any of three identifiers per user: email, slug, or workspace uuid. Email is the most stable join key against most HRIS systems, so cache an email → Zeeg uuid map at the start of every sync.
Lowercase emails on both sides of the join — Zeeg stores them as the user typed them. Skip inactive users (isActive: false); attempting to write time-off for them returns failed.
HRIS users with no Zeeg account. This will happen — contractors, new hires not yet provisioned, deactivated employees still in the HRIS. Log them and skip; do not try to invite them implicitly.
2

Create time-off periods

POST /time-off is bulk-only. Each request creates the same period for one set of users (up to 10) or every active member of one team. There is no single-user create endpoint — to create one period for one person, send a users.emails array with one entry.Provide exactly one of users or team. Inside users, provide exactly one of emails, slugs, or uuids.Example: Alice is on vacation Mon–Fri next week.
The response is a per-target outcome list. Each entry’s status is created, replaced, or failed — handle all three:
The HTTP status is 200 if at least one entry succeeded; 422 only when every target failed. Always inspect each entry’s status rather than trusting the HTTP code alone.Half-day example. Alice leaves at 13:00 on Friday afternoon:
The cutoff is interpreted against the user’s schedule, not the caller’s timezone — verify against your users’ configured timezones if half-day precision matters.
3

Update or delete when the HRIS record changes

PATCH /time-off/{uuid} partially updates a period. Only supplied fields change; omitted fields are left alone.
DELETE /time-off/{uuid} removes a period:
PATCH rejects overlap. If the new startDate/endDate would overlap another time-off period the same user already owns, PATCH returns 422 with This time-off period overlaps with an existing one. This is the opposite of POST — see the next step.
4

Solve the dedup / idempotency problem

The /time-off resource has no native external-id lookup, so re-running a sync needs care to avoid duplicates. Use the two mechanisms Zeeg gives you:1. Built-in overlap-replace on POST. When you POST /time-off for a user and the new range overlaps an existing period, Zeeg deletes the old period(s), creates the new one, and returns status: "replaced" with the deleted UUIDs in replaced[]. This makes re-posting the same vacation safe-by-default — the second call replaces the first instead of duplicating.
2. An external-id → Zeeg-uuid map on your side. Overlap-replace handles re-syncs of the same vacation, but it doesn’t help when:
  • The HRIS shifts a vacation to a non-overlapping range (old: May 11–15, new: May 18–22). POST creates a second period instead of moving the first. You need the original UUID to PATCH or DELETE.
  • The HRIS deletes a vacation. Zeeg doesn’t know about the source-of-truth deletion; you must DELETE explicitly.
  • You want to detect drift between HRIS and Zeeg (e.g. someone manually created a Zeeg-side period that has no HRIS counterpart).
So the canonical pattern is: keep a persistent map (hris_record_id → zeeg_period_uuid, plus the user identifier and the last-seen range) on the integration side. The map is your primary lookup. The note field is a fine forensic breadcrumb (e.g. [hris-id:bamboo-12345]) — it shows up in the dashboard and helps humans audit — but note is not query-filterable, so it’s a complement to the map, not a substitute.
When you want to move a period to a non-overlapping range, choose between POST and PATCH:
  • PATCH if you have the UUID and the new range doesn’t overlap any other of that user’s periods. PATCH is cheaper and preserves the same UUID downstream.
  • POST if you don’t have the UUID, or the new range overlaps another existing period you’d actually like to absorb.
5

Reconcile: a worked sync loop

Putting it together. The sync runs against three sources of state — your HRIS, your local map, and Zeeg — and produces three sets of operations: create, update, delete.
Python
A few things this loop intentionally handles:
  • Idempotency on first run. If your map is empty but Zeeg already has matching periods, the POST overlap-replace logic absorbs them — you don’t end up with double-booked time-off.
  • Range moves. PATCH first (preserves the UUID), POST as fallback when PATCH hits a 422 overlap.
  • Upstream deletions. Anything in the map but not in this HRIS pull is treated as deleted.
  • Failed entries. upsert raises on failed status (often: token can’t edit that user’s schedules) so you can log and continue.

Adjacent topics

Public holidays

For country- or region-wide closures (Christmas, Bavarian regional holidays, US Thanksgiving), use holiday subscriptions instead of creating time-off for every user every year:
Subscriptions auto-roll forward each year — you subscribe once and the holidays keep coming. See the Holidays API reference for the full surface.

Recurring weekly availability

For permanent working-hour changes (someone moves to a 4-day week, or starts an hour later on Wednesdays), update the user’s availability schedule with PATCH /schedules/{uuid}. Use weeklyHours for the recurring pattern and specialHours for date-specific overrides. See PATCH /schedules/{uuid}. Don’t use specialHours to model vacations — that’s what /time-off is for. specialHours is for altering the working day on a date (e.g. “I’m only available 09:00–11:00 on this date”); time-off is for removing it.

Rate limits and error handling

  • The bulk POST endpoint accepts up to 10 users per request or one team of up to 200 active members. Chunk larger HRIS pulls into multiple requests.
  • For 429 Too Many Requests responses, follow the exponential backoff guidance — 1s, 2s, 4s, … capped at 60s, with a small jitter.
  • See Errors for the standard error envelope. Common time-off-specific cases:
    • 200 with status: "failed" — token can’t edit that user’s schedules. Don’t retry; surface to a human.
    • 422 No updatable fields were provided on PATCH — the request body was empty.
    • 422 This time-off period overlaps with an existing one on PATCH — fall back to POST or pick a non-overlapping range.
    • 404 Time-off period not found on PATCH/DELETE — the UUID was already deleted, or the token can’t see it. Treat DELETE 404 as success; treat PATCH 404 as a stale map entry to be cleaned up.
Run the sync on a schedule (hourly or every 15 minutes is typical), not in response to every HRIS webhook. Reconciliation is naturally idempotent and absorbs missed events; per-event delivery isn’t worth the complexity for time-off.
Last modified on August 25, 2026