# ShiftForest developer kit

This archive contains the versioned TypeScript source client, dependency-free JavaScript modules for Node 22.23.2 or later, a webhook signature verifier and read-only examples. It is not an npm registry package or a hosted sandbox. No credentials or workforce fixtures are included. Obtain an explicitly scoped integration key from Company → API and keep it in your server's secret store.

Base URL: https://api.shiftforest.com/v2
Guide: https://app.shiftforest.com/developers
v2 contract: https://api.shiftforest.com/openapi-v2.json
v1 compatibility contract: https://api.shiftforest.com/openapi.json
Browser billing contract: https://api.shiftforest.com/openapi-billing.json (separate session-authenticated service; payments remain unavailable until configured).

Extract the ZIP, set SHIFTFOREST_API_KEY and SHIFTFOREST_ORGANIZATION_ID in the server environment, then run `node examples/read-schedule.mjs`. The example verifies the organization before fetching a single page and prints counts, not HR rows. It needs `shifts:read`; it performs no mutations. The client exposes the fetch transport for isolated tests. Follow next_cursor while has_more is true for complete reads. Bind stored replicas to organization, principal identity and permissions; rebuild them when that binding changes.

Import `ShiftForest` from `./shiftforest.mjs` for JavaScript or use `shiftforest.ts` in a TypeScript project. Responses have `{data, version, requestId}`. A list's data contains `{data, has_more, next_cursor}`. Preserve opaque cursors exactly. Shifts use public numeric IDs and ISO timestamps with offsets. Other workflow-specific IDs may be UUIDs; follow the schema rather than guessing from names. Source types and the OpenAPI contract describe available methods.

Persist one unique Idempotency-Key per logical write before sending it. The client accepts it as the final argument of mutation helpers and preserves it across its bounded retries. Reuse the same method, path, query, payload and If-Match after a timeout. Receipts last 24 hours; a mismatched request returns 409. After expiry, reconcile current state before retrying. On 412 fetch the latest state and review your intended change. Never silently replace a version and repeat an old update. v2 version ETags are numeric; v1 optional record ETags use a different opaque sf1 format.

The SDK retries network failures and 429/502/503/504 at most three attempts with bounded backoff. Record requestId for support, without credentials or HR payloads. Permission, authentication and stale-write failures require explicit handling. Current attempt limits are 120 per principal and 600 per organization per minute; ingress limits may apply sooner. Keys are finite and can be revoked; independent integration rotation permits a reviewed overlap up to 60 minutes.

To check a captured webhook, run `node examples/verify-event.mjs raw-body.bin signature.txt` with SHIFTFOREST_WEBHOOK_SECRET set, and optionally SHIFTFOREST_PREVIOUS_WEBHOOK_SECRET during rotation. The signature file must contain the unmodified RotaTree-Signature header. Verification operates on raw bytes before JSON parsing and uses a five-minute timestamp tolerance. The command prints only the verified event ID. This command does not receive or acknowledge delivery. Your receiver must durably deduplicate event_id and commit effects/checkpoints before acknowledging. Use the changes feed for gaps, reordering and minimal access-removal tombstones. Webhook subscriptions are managed separately from API-key workplace scopes.

Independent integrations cannot attest employee consent or forge attendance. Use human sessions for those actions. Statutory payroll calculation/provider certification and document signing are not currently promised by the API. Configurable SSO/SCIM uses the separate provisioning contract described below and requires real-provider commissioning. Inspect organization/capabilities and the deployed schemas before building against an operation.

manifest.json identifies the exact SHA-256 and byte length of every kit file. Verify them after download. The manifest is delivered over HTTPS; it is not an independently signed supply-chain attestation. Keep deployment versions aligned when upgrading your client.

## Python local replica

`examples/sync_shifts.py` is a standard-library Python 3.10+ changes-feed consumer. Set `ROTATREE_API_KEY` in the environment, optionally set `ROTATREE_API_BASE_URL`, and run `python3 examples/sync_shifts.py --database /private/path/replica.sqlite --organisation SOURCE_ORGANISATION_ID`. It checks organization and integration identity before opening a private SQLite file, applies pages and checkpoints atomically, and stores personnel data locally. Use one process per replica.

HTTP 410 from synchronization requires replacing the replica. The Python consumer clears its local rows and checkpoint in one transaction, enables SQLite secure deletion, stops, and asks you to run again for a new baseline. It does not retain old payloads as an error fallback. Filesystem snapshots, copied databases and downstream systems require separate handling; this is not evidence that those copies were removed. The TypeScript SDK exposes the HTTP status through `ShiftForestError.status`; applications must implement equivalent replica clearing and rebuilding.

## Historical resources

Independent integrations can request attendance_history:read, leave:read, credentials:read, or payroll_exports:read. These permissions are not added to existing keys. Payroll exports additionally require pay:read and all-workplace access. Administrator sessions can also read these resources; creator-bound compatibility keys cannot.

Use attendance(query), leave(query), credentials(query), payrollExports(query), their corresponding Record(id) methods, and payrollExportLines(id, query). Attendance requires offset timestamps from/to (start time in [from,to), maximum 93 days). Leave requires inclusive overlapping from/to dates (maximum 366 days). Payroll optionally accepts a complete overlapping date window of up to 366 days. Pages default to 100, maximum 250; retain exactly the same filters and limit when following next_cursor. Cursors bind to the integration and current permissions. They exclude later insertions but existing records remain live: these are not immutable historical database snapshots ; cross-resource catch-up is available separately through the checkpoint workflow below.

Attendance visibility follows the actual clock workplace. Leave and credentials follow current worker workplace membership. Credential status is recorded review status; evaluate expires_on/deadline against the intended work date and approved policy. Reasons, notes, device metadata and reviewer identities are omitted.

Payroll snapshots expose basic reviewed hourly exports, not tax calculation or payroll-provider certification. total_minor, amount_minor and fractional minutes are strings: retain them as decimal values, not JavaScript Number. Payroll line indexes are one-based and stable within the frozen export. Names and notes are omitted; employee_code supports payroll matching. Check the published schema for all fields and nullability.

## Recorded attendance summaries

Use `attendanceSummary({ from, to, user?, location? })` or GET `/attendance/summary` with `attendance_history:read` on an independent integration, or an active administrator session. `from` and `to` are offset-qualified instants; the half-open clock-in window is at most 93 elapsed days. Optional worker/workplace IDs are public numeric IDs. Captured attendance workplace governs permission; only older records without it fall back to the linked shift workplace.

The response contains `totals` and `days` grouped by the company-local clock-in date. Empty days are omitted. `workers` is a distinct count within each group, so daily worker counts must not be summed to calculate the whole-window count. Open sessions contribute to counts but never to duration totals; voided sessions are excluded. `gross_seconds`, `break_seconds` and `net_seconds` are exact decimal strings. Breaks are capped at each closed session's duration. No rounding, paid-break policy, pay rates or overtime rules are applied. These recorded durations are not payroll values; approval counts do not certify payroll.

One statement snapshot is limited to 100,000 matching records. A 422 response contains no partial totals: narrow the dates or select a worker/workplace. A 422 also identifies invalid retained durations that need correction before totals can be calculated. There is no pagination or cursor for this aggregate. `calculated_at` identifies the calculation time; later corrections can change a repeated query. Use immutable reviewed payroll exports for frozen payroll evidence. Existing attempt quotas and `no-store` responses also apply.

```ts
const summary = await client.attendanceSummary({
  from: "2026-10-01T00:00:00+01:00",
  to: "2026-10-08T00:00:00+01:00",
  location: 12,
});
console.log(summary.totals.closed_sessions, summary.totals.net_seconds);
```

## Cross-resource replica checkpoints

`resourceCheckpoint(resources)` selects up to ten supported resource types and requires every corresponding read scope. Create it BEFORE reading initial resource pages. Build a replacement local snapshot, then process `resourceChanges(cursor, limit)` until caught up before exposing the replacement. Bind all local state to the organization, integration and resource selection. Older creator-bound keys cannot use this workflow.

The feed contains only resource, ID, action, decimal-string sequence and occurrence time. For `refresh`, GET the current resource by ID using the same identity, then apply your local date/status filters and upsert or remove it. Authorized 404 means remove; authentication, permission, rate-limit or server failures must stop the page without advancing its checkpoint. For `remove`, delete the cached payload without fetching it. GET results reflect current state, not a historical payload from the event time. Pages and effects must be committed in one local transaction; replay is safe when these operations are idempotent. Do not compare sequence strings lexicographically or convert them to Number.

Save `next_cursor` even on empty/idle pages. At most 250 notices and 1000 organization journal rows are processed per request; hidden changes can produce an empty page with `has_more=true`. Continue until false, then poll later. Page size can change between polls. A 410 response requires immediately clearing the old replica and repeating checkpoint plus snapshot: membership changes, workplace deletion or shift workplace moves invalidate the visibility generation. A privacy reset also invalidates all older shift-feed, resource-feed and catalogue cursors with 410. Invalid or mismatched checkpoints also return 410. Keys rotating within the same integration retain checkpoints; new integration identities do not.

Ten supported resource types are shifts, users, locations, roles, availability, shift-requests, attendance, leave, credentials and payroll-exports. Fetch paths use these exact names and the returned ID. Payroll line changes are represented by the parent export. No HR payloads are copied into this journal. The existing shift payload feed and its sequence are preserved; this new feed has an independent sequence and no new webhook deliveries. Events are retained without automatic pruning pending an approved retention policy. Frozen payroll exports, workforce resource reads and initial snapshot filters retain their documented limits.

Run one poller per replica or compare-and-swap the stored cursor when committing a page. Never overwrite a newer local checkpoint with an older page. Use idle backoff with jitter and respect Retry-After. If a record refresh fails with 401, 403, 429 or a server error, keep the prior checkpoint; do not turn the failure into a deletion. For retained endpoints that return a deletion flag, remove that record. Rebuilds must apply the same intended local filters before exposing the replacement snapshot.

Time passing does not create a change notice. Requery newly included dates when a rolling date window moves, and evaluate expiry/deadline rules against the intended work date. A fixed filtered replica can converge through the feed; moving filters also need this scheduled reconciliation.

After an authentication or permission failure, stop serving cached data until organization, principal and permissions are verified again. Preserve the checkpoint without advancing it while resolving the failure; if the verified identity or permission boundary changes, clear and rebuild the replica.

## Worker creation and profile updates

Use an independent integration with `users:read` and `users:write`. Creating a worker also requires `pay:read` and `pay:write`; these pay permissions are required for changes to the hourly rate, weekly contracted hours or payroll code. Existing keys gain no new permissions. Review a replacement integration through Company → API if needed.

Persist a UUID for the logical command before starting. Call `workerCreationContext(requestId, profile)`, review its details, then `createWorker(requestId, profile, reviewToken, idempotencyKey)`. The profile requires first/last name, email, explicit `hourly_rate_minor` (integer currency minor units, including zero when intended), `weekly_hours`, numeric `default_role` and numeric `locations`. Optional `employee_code` must be unique; omission generates one. The UUID becomes the workforce identity. Creation does not create an Auth account, enable login or send email. Invitation and verified identity linking use the separate workspace workflow.

For an existing employee, GET `/users/{id}` and pass its current version to both `workerProfileContext(id, version, requestId, changes)` and `updateWorkerProfile(id, version, requestId, changes, reviewToken, idempotencyKey)`. Supported changes are names, role, workplaces, rate, hours and payroll code. Email, passwords, account roles, login and PIN changes are not accepted. Administrator/manager profiles require the workspace workflow. A workplace-limited integration must have access to every current and proposed worker workplace; future shifts prevent removal of their workplace.

Keep the same UUID, path, body, review token, version and Idempotency-Key for uncertain retries. Durable command receipts survive the generic 24-hour receipt period. They return the original version, which may now be stale; read again before making another change. Access is rechecked before any replay. A new reviewed intention needs a new UUID; do not silently replace a 409/412 review. Human administrator writes require recent MFA. Rate changes remain unverified for payroll until separately reviewed.

## Human accounts with multiple workspaces

Create one immutable client per login and company: `new ShiftForest({ accessToken, workspaceId: companyUuid })`. This sends `X-ShiftForest-Workspace` on every request. The server verifies that the Auth account has active membership in that company and uses that membership's workforce ID, role and workplace assignments. `workspaceId` is rejected with `apiKey`; integration keys already belong to one company. Refresh an expired token by creating a new client.

Without this header, the active canonical primary membership is selected, or the sole active membership if there is no canonical primary. Multiple noncanonical memberships require an explicit choice. An unauthorized choice never falls back to a different workspace. Store cursors, resource IDs, versions and write receipts with their company and principal identity. Changing workspaces requires fresh reads and reviews; do not reuse another workspace's command context.

## Manager-approved shift cover

`recoveryStatus({from,to,location?,limit?,cursor?})` reads a bounded operational status page. An independent integration needs the new `recovery:read` permission; existing and creator-bound keys gain no authority. Workplace restrictions apply. Dates are inclusive shift rota dates, with a maximum 93-day window. Page size defaults to 25, maximum 50. Preserve filters and the opaque cursor between pages. Poll for current state: recovery campaigns are not a change-feed resource or a webhook topic. Status omits the manager's reason, recipient list and eligibility details. `examples/read-recovery.mjs` checks the organization and prints counts only.

Managers use their own human session and selected workspace: `recoveryCandidates(shiftId, cursor?, limit?, stage?)`, then `prepareRecovery({shift,stage,recipients,expires_at})`. Shift and recipient IDs are public numeric IDs. An optional candidate stage filters the complete directory before pagination. Stages are `internal`, `bank` and `agency`; recipients must be active linked workspace members in that stage. Review the exact shift, recipient list, expiry, eligibility and remaining staffing findings. An eligible candidate can reduce an existing coverage deficit without filling every gap. `can_launch` must be true. Expiry must be 1 minute–24 hours away and no later than shift start.

Persist a command UUID and complete reviewed body before calling `launchRecovery({...preparation, request_id, review_token, reason}, idempotencyKey)`. Launch requires recent MFA, current workplace management access and a reason of 10–1000 characters. It creates up to 20 offers, reserves the shift and makes no assignment. Ordinary claims cannot bypass the reservation; scheduling edits cancel the campaign. In-app offers are available immediately. External email still depends on enabled delivery and the recipient's opt-in.

Employees use their own session: `recoveryOffers(cursor?, limit?)` returns pending, unexpired offers in `{items,next_cursor}`; `recoveryOffer(offerId)` returns the exact shift, current actions and review token. After the person explicitly chooses, call `respondRecovery(offerId, "accept" | "decline", {request_id,review_token}, idempotencyKey)`. The first eligible acceptance wins under the company lock; other offers close atomically. Access, issuer authority, current shift, leave, availability, overlap, contractual caps and configured staffing checks are revalidated. Integrations cannot give consent. No automatic reassignment or off-platform agency acceptance is supported.

`recoveryCampaign(campaignId)` gives a manager current responses and the cancellation review token. `cancelRecovery(campaignId,{request_id,review_token,reason},idempotencyKey)` requires recent MFA and closes pending offers without changing assignment. Campaign/offer/command IDs are UUIDs. Candidate and offer lists use `{items,next_cursor}`; the status endpoint uses the normal `{data,has_more,next_cursor}` page. Candidate pages also include the shift and active campaign.

For uncertain retries, keep the same command UUID, action, body, token and Idempotency-Key. The durable command survives the generic 24-hour receipt. Do not silently replace a 409 review or switch an uncertain acceptance to a decline. Reload and obtain a new explicit decision after a conflict. A previously completed result is historical: reread current state before making a different change. Permission failures stop retries, including receipt replay. Record request IDs without HR details or credentials.

## Staffing comparisons

A signed-in manager can call `const { data } = await client.simulateStaffing(shiftId, [workerId]);` with public numeric IDs and the selected workspace. This evaluates 1–5 workers independently against the same future published open shift (maximum 25 elapsed hours), current coverage and configured recovery eligibility. No assignment, offer, consent or notification is created. API keys are denied. The SDK omits Idempotency-Key for this fresh read; raw requests to `POST /recovery/simulate` must omit both Idempotency-Key and If-Match and contain exactly `{shift, workers}`.

`baseline` and each evaluated `proposed` result contain total, blocking and warning counts, counts by rule/severity and up to 50 redacted findings. `findings_truncated` distinguishes a limited list from complete counts. `improved_blocking_findings` and `worsened_blocking_findings` compare each keyed finding and its deficit amount; they are counts, not a combined score or hours. A basic eligibility failure returns `evaluated=false`, `proposed=null` and null change counts. A newly blocking rule still returns its evaluated proposal. Eligibility can coexist with unchanged existing deficits. Each option is independent; do not sum their effects.

`evaluated_at`, `snapshot_token`, enabled `rules`, confirmation/control flags and explicit `assumptions` describe the result. The snapshot token is not an approval or a historical policy archive. Facts may change immediately; use the normal reviewed recovery flow and explicit employee acceptance to act. The prototype does not optimize a whole rota, predict costs or certify staffing safety.

## Privacy rights, retention controls and personal exports

Privacy methods require `accessToken`, with `workspaceId` to select membership. API keys cannot impersonate a requester or decide privacy rights. `examples/read-privacy.mjs` verifies organization identity before reading the caller’s request list and prints counts only. Never put access tokens in URLs or source files.

`listPrivacyRequests`, `submitPrivacyRequest`, `getPrivacyRequest`, `reviewPrivacyRequest` and `applyPrivacyRequestAction` support own requests and administrator review. Workspace-wide lists and another person’s case require an administrator with recent MFA. Requesters do not need MFA just to submit/reply/withdraw. Administrators cannot approve their own requests. `listPrivacySubjects`, `reviewPrivacyIntake` and `submitPrivacyIntake` support verified requests received outside the application, including former workers, without granting login access. Store an evidence reference, not identity documents.

`getPrivacyGovernance` returns category policies, paged holds and separate deletion-worker gates. `reviewPrivacyPolicy` / `approvePrivacyPolicy` require the current owner and recent MFA; no default retention period is invented. `reviewPrivacyHold` / `applyPrivacyHold` require a verified administrator. Place with a new UUID `hold_id`, numeric `subject` and distinct categories. Release with the existing `hold_id` and null `subject`/`categories`. There is no automatic hold expiry. Any active hold conservatively preserves composite export copies.

Privacy POST bodies use a persisted `request_id` UUID, plus the reviewed token (or revision for revocation). The SDK deliberately omits `Idempotency-Key`; these operations use durable, authority-rechecked command receipts instead of short-lived response caches. It also rejects `If-Match`. Save the exact body across timeouts/restarts. A 409 requires a new review and command ID. GET pages use signed cursors bound to the route, query, principal and workspace; preserve filters and limit (default 25, maximum 50). These records are polled, not included in the workforce change feed or webhooks.

For an active access/portability request, call `reviewPersonalExport({case_id,from,to,expires_at})`, inspect coverage/omissions/counts, then `createPersonalExport({...sameFields,request_id,review_token,confirmed:true})`. Recent MFA is required for creation/list/download, including the subject. An owner-approved exports policy is required. Windows are inclusive company-local dates, maximum 366 days; limits are 1,000 records per section, 10,000 total, 8 MiB and 64 linked payroll batches. Oversized scopes fail explicitly without truncation. Access expires between one minute and 24 hours ahead. The request remains open; omissions need separate review before an administrator records an outcome.

`listPersonalExports(caseId)` shows copy metadata. `downloadPersonalExport(exportId)` returns exact `canonical_json` with SHA-256, UTF-8 byte length, workspace and expiry. Prefer `verifiedPersonalExport(exportId,expectedOrganizationId)`, which verifies those properties and returns `Uint8Array` in the usual SDK `data` envelope. Save those exact bytes using restricted permissions; reserializing JSON changes its checksum. `verifyPrivacyExport` is also exported for separately obtained responses. Do not log payloads.

`revokePersonalExport(exportId,{request_id,revision,reason})` stops further downloads but does not delete the retained payload or recall copies already downloaded. Expired/revoked/purged downloads return 410. Stored copy retention is separately operator-gated; the longer of captured/current approved periods and all active subject holds apply. General HR-source erasure, external-copy recall and automatic legal compliance are not claimed by these methods. A human-recorded case status does not execute deletion.

## Approved leave entitlements

`listLeaveBalances({from,to,subject?,limit?,cursor?})` reads accounts overlapping an inclusive window of up to 366 days. `getLeaveLedger(accountId,{limit?,cursor?})` reads immutable signed entries and captured daily calculations. Use an independent integration with `leave:read`; workplace restrictions apply before pagination. Legacy creator-bound keys cannot read these routes. Human sessions may read their own or managed workers. `examples/read-entitlements.mjs` checks organization identity before reading, and prints counts only.

A missing account means **unconfigured**, not zero entitlement. Each account has an explicit `hours` or `days` unit; never sum unlike units. `balance` includes approved future leave. Pending requests do not reserve units. Calendar absence days are a separate measure. Account IDs are UUIDs; worker and leave IDs are public numbers; ledger sequences are decimal strings. API reads omit free-text policy evidence, adjustment reasons and approver identities. Store personal data only within your authorised scope.

Human sessions use `reviewLeavePolicy(policy)` then `createLeavePolicy({request_id,policy,review_token,confirmed:true})`. Another administrator with recent MFA must approve a worker’s account. Supply an explicit period, opening entitlement before approved-leave deductions, negative-balance choice, evidence reference and effective work patterns. A week has seven arrays, Monday first, with zero to four non-overlapping `{from,to,quantity}` local-minute intervals per weekday. Empty means non-working. Hours must match configured paid minutes; policy-day fractions total at most one per weekday. Intervals may end at 1440. Charges use differences of cumulative interval quantities rounded to four decimal places, so adjacent partial requests remain additive. Twelve effective patterns and 366 days per account are supported. The review reconciles existing approved leave. Policy/units remain immutable after creation; no statutory allowance, automatic accrual, proration or expiry is inferred.

For an explicit contractual accrual schedule, use `getLeaveAccrualRule(accountId)`, `reviewLeaveAccrualRule(accountId, policy)` and `setLeaveAccrualRule(accountId, {request_id, policy, review_token, confirmed:true})`. The write policy contains `period_quantity`, `already_awarded`, inclusive `eligible_from`/`eligible_to`, `frequency` (`daily`, `calendar_month_end` or `upfront`), an IANA `timezone`, `enabled`, `reference` and `reason`. `period_quantity` is the allowance for the entire account period before calendar-day proration; `already_awarded` is the base grant already credited outside automation, excluding carryover, exceptional adjustments and automatic entries. It is not the remaining balance after leave taken. Upfront grants start on eligibility start; daily and month-end schedules credit completed local days or calendar months. Month-end uses day weighting, not equal twelfths, and final partial months are credited after month end. Cumulative rounding to four decimal places makes catch-up converge with daily execution.

Approval posts the reviewed current correction atomically; disabled policies retain their ledger without posting. Repeat the exact request after an uncertain response. A 409 requires a new review. Future posting stops on inactive workers, changed approver access or changed work patterns; negative corrections always require review. Once configured, base-grant changes go through rule review, including while paused. TOIL, carryover, expiry and exceptional adjustments remain separate reviewed ledger operations. GET is available to scoped independent integrations with `leave:read`; writes require another human administrator with recent MFA. Read projections omit approval notes and identities. Ledger entries expose nullable `accrual_rule_id` and `accrual_calculation` for reconciliation. These endpoints accept no query parameters, `Idempotency-Key` or `If-Match`; poll them separately from the workforce change feed. Full response schemas are in OpenAPI v2.31.

Use `reviewLeaveAdjustment` / `applyLeaveAdjustment` for independently approved accrual, correction, proration, adjustment, expiry and TOIL earned. Quantities have at most four decimal places. Carryover is a negative `carryover_out` with `destination_id` for a later account of the same worker, leave type and unit; debit and credit commit atomically. These are recorded calculations, not a substitute for agreeing the organisation’s HR policy.

`createLeaveRequest` submits a pending absence. `start_minute` is 0 or 720; `end_minute` is 720 or 1440. Morning ends at noon and afternoon starts at noon, in the captured organisation timezone. These are local clock boundaries, not a promise of twelve elapsed or contractual hours. Another authorised manager uses `reviewLeaveDecision` / `applyLeaveDecision` to approve/deny a pending request or cancel approved leave. The review includes account impact. Cancellation reverses captured charges; charged request intervals cannot be overwritten or deleted. Submit a new corrected request after cancellation.

All twelve entitlement routes omit `Idempotency-Key` and `If-Match`. Keep the exact `request_id` and body across uncertain retries. A 409 requires a fresh review and command ID; the SDK does not silently regenerate either. GET pages default to 25, maximum 50, and use signed live cursors bound to route, query, principal and workspace. Poll accounts/entries separately; they are not in the general workforce change feed. Legacy allowance writes are blocked once governed accounts exist, and legacy allowance-list reads direct governed workspaces to v2. Historical decorative v1 half-day metadata is normalised to its actual full-day absence; new half-day requests govern scheduling and accounting.

### Reviewed work-pattern amendments (v2.11)

Use `getLeavePolicyHistory(accountId, {limit,cursor})` for the original policy and immutable amendments. The original is a separate object on every page; `items` contains only amendments, ordered by their decimal-string IDs. Independent `leave:read` integrations retain workplace restrictions; evidence text and approval identities are omitted.

An administrator with recent MFA, approving another worker, calls `reviewLeavePolicyAmendment(accountId, {patterns,policy_reference,reason})`. Supply the complete effective pattern set for the account period. Keep earlier patterns when a later contractual change takes effect. Review every affected approved leave request, its old/new daily charge, the signed correction and resulting balance. The account period, unit, opening entries and negative-balance choice stay fixed. An amendment cannot overspend a nonnegative account; approve any justified entitlement adjustment separately first.

Confirm with `amendLeavePolicy(accountId, {...sameFields,request_id,review_token,confirmed:true})`. The server checks current worker, policy, leave, balance and authority, appends one immutable policy version and explicit `leave_correction` entries, and retains all previous records. A correction with zero quantity may record a changed daily allocation. No accrual or statutory formula runs. Uncertain retries reuse the exact body; conflicts need a fresh review and command ID.

Accounts expose `policy_version` (`"0"` means the original pattern). That field is absent on preserved command receipts issued before v2.11; those receipts still replay unchanged. Ledger charges/corrections/reversals include their captured version, while unrelated balance entries have null. Correction `breakdown` contains the revised daily leave charge; `quantity` is the signed balance correction. Cancellation reverses every unreversed original charge and correction exactly once. Policy history and ledger reads remain live pages and are polled separately from the general workforce change feed.

### Staffing evidence packs (v2.12)

Use a workplace manager's human `accessToken` and selected `workspaceId`. API keys cannot create or read these packs. `listStaffingPacks({workplace,limit,cursor})` returns metadata; use the opaque next cursor unchanged with the same workplace and page size. Review, creation, download and revocation require recent MFA. The owner must first record an approved export-retention policy.

Call `reviewStaffingPack({pack_id,workplace,from,to,expires_at})` using a new UUID for `pack_id`, a public numeric workplace ID, 1–31 inclusive company-local dates and an explicit-zone expiry 1 minute–24 hours ahead. Present all preview sections, limitations and retention terms. After explicit confirmation, call `createStaffingPack({...reviewBody,request_id,review_token,confirmed:true})` with a separate new command UUID. Retry the exact command after an ambiguous response. Source or policy changes require a fresh review; never silently confirm a replacement. Do not add `Idempotency-Key` or `If-Match` headers to these durable command routes.

`verifiedStaffingPack(packId,organizationId)` checks exact UTF-8 length, SHA-256, format/version, identity, workspace and access expiry before returning parsed evidence. `downloadStaffingPack` returns the original canonical JSON and integrity metadata if your application needs to save those bytes. The checksum describes the JSON source, not a later formatted report. Treat the contents as personal data: worker labels are pack-local pseudonyms, not anonymisation. Do not log pack contents or put access tokens in URLs.

The ten sections separate capture-time schedule, attendance, exceptions and requirements from recorded lifecycle, requirement versions, credential knowledge at shift start, credential changes during the period, and distinct-person timelines. Current credentials do not fill missing historical evidence. Counts exclude open, unapproved and voided attendance; breaks, duties and skill adequacy are not inferred. The pack does not certify staffing adequacy or regulatory compliance. Limits are 1,000 records per source section, 2,000 timeline segments, 10,000 total records and 8 MiB; excessive periods fail instead of returning truncated evidence.

`revokeStaffingPack(packId,{request_id,revision})` uses the displayed decimal-string revision. Exact retries return the saved receipt; changed access requires refresh. Expired, revoked or purged copies return 410. The app supports authenticated internal links and printable reports. Each link recipient still needs current workplace permission and recent MFA; links do not grant access. Revocation cannot recall downloaded files. Retention uses the longer captured/current approved policy and preserves composite packs under any active workspace hold. Automatic deletion remains an operator-controlled capability; access expiry is not deletion.

### Custom-time requests and own pending withdrawal (v2.13)

`POST /leave-requests` accepts integer `start_minute` 0–1439 and `end_minute` 1–1440. For the same date, the end must follow the start. End 1440 means midnight after the final date. The captured organisation timezone determines absolute boundaries. Nonexistent or ambiguous clock-change times are rejected; choose an unambiguous boundary. Approved work patterns, including unpaid gaps, determine actual deductions. Calendar coverage is not entitlement usage.

The requester can review and withdraw their own pending request with a human access token:

```ts
const reason = "My plans changed and I no longer need this absence";
const review = await client.reviewLeaveWithdrawal(leaveId, { reason });
// Show the review and obtain the requester's explicit confirmation.
const command = {
  reason,
  request_id: crypto.randomUUID(),
  review_token: review.data.review_token,
};
const result = await client.withdrawLeave(leaveId, command);
// Preserve this exact command for an uncertain-response retry.
```

Withdrawal changes no entitlement balance. History remains with `status: "denied"` and non-null `withdrawn_at`; display it as withdrawn. The marker is optional on historical command receipts. A manager approval or request edit invalidates a waiting review (409); refresh and review again. Approved leave requires another authorised manager's reviewed cancellation. API keys and another user's session cannot withdraw on the requester's behalf. No MFA is required for this own pending action. Never set `Idempotency-Key` or `If-Match`; use the durable body command identity.

A human requester can `GET /leave/{id}` for their own exact record, including after an uncertain withdrawal. This does not grant collection, another worker's record or credential access. Access is rechecked before receipt replay and after lock waits. Capabilities advertise `leave_custom_times`, `leave_pending_withdrawal`, `leave_own_record_read` and `leave_ambiguous_local_times: "rejected"`.

Legacy v1 responses retain exact minute/timezone fields and include `withdrawn_at`. An am/pm boundary that cannot represent a custom minute is null. v1 writes to a custom-minute record are rejected so older integrations cannot silently broaden it; use the reviewed v2 workflow. Existing durable decision receipts replay unchanged. Request creation preserves identity and returns the current record projection on retry.

## Private workforce files

Eight `/files` operations require an active human `accessToken` and selected `workspaceId`. API keys are rejected. Employees can use their own records; managers require current worker/workplace access and recent MFA. Browser clients use a same-origin `/v2/` base URL; server integrations can use `https://api.shiftforest.com/v2/`. No cross-origin browser access is promised.

Use `listPrivateFiles({subject,limit,cursor})` and `getPrivateFile(id,{limit,cursor})` for metadata and newest-first version history. `subject` is the public numeric worker ID. Cursors bind the caller, workspace, query and page size (1–100). Native document and version IDs are UUIDs, distinct from the retained v1 metadata-only document API. Private files are not included in the ten-resource integration changes feed or webhooks; poll bounded history for scan completion.

Persist new document, version and command UUIDs before `reservePrivateFile`. Pass the exact lowercase SHA-256, bytes (1–4194304), media type and matching `.pdf`, `.png`, `.jpg` or `.jpeg` filename. New documents use revision `"0"`; replacements use the displayed document revision. A version/path cannot be overwritten. Management-issued documents can request an acknowledgement; employees cannot replace those documents or attest another person’s acknowledgement.

`createPrivateFileUpload(versionId)` returns the exact URL, method and headers for the direct provider PUT. Never log the credential or put it in analytics. Complete with `completePrivateFileUpload(versionId,{request_id,revision})` within the 15-minute reservation. Provider upload tokens last two hours; this is not a revocable 15-minute token. Reusing an uncertain upload must not overwrite the path: attempt completion and let the worker verify the expected bytes. Late or changed bytes remain unavailable. Exact command retries return the saved receipt after current authorization is rechecked; changed command contents fail. File operations do not use Idempotency-Key or If-Match headers.

Poll history for `available`, `blocked`, `scan_failed` or `expired`; do not treat an upload receipt as a successful scan. PDF/JPEG/PNG are supported. Encrypted PDFs, embedded files, malformed PDF structure and parser/scan limits fail closed. Scanning cannot guarantee safety. `verifiedPrivateFile(versionId,expected)` checks the immutable reviewed descriptor against the bounded binary response. The API independently checks bytes and access again after provider retrieval. Content is an attachment with no-store; there are no public links. `acknowledgePrivateFile(versionId,{request_id,sha256})` records the current version and digest, not an electronic signature. `retirePrivateFile` requires the displayed revision, meaningful reason and confirmation; existing downloaded copies cannot be recalled.

An owner-approved `documents` retention policy is required before reservations. Limits are 500 lifetime version records and 100 MiB per workspace, with a 256 MiB deployment cap. Unverified versions reserve the full 4 MiB; verified versions use exact bytes. Abandoned reservations release byte capacity only after provider-token expiry and a confirmed missing object. Metadata still counts toward the lifetime limit. Retirement/retention expiry revoke access, not source bytes. Reviewed deletion remains disabled until the worker and recovery lifecycle are commissioned; holds and policy evidence remain recorded. Personal privacy exports include subject-scoped document metadata; file bytes need separate authorized review. Local backups pair database snapshots with private object manifests and verify restored bytes; they are not an off-site or provider restore guarantee.

### Reviewed document deletion (v2.29)

`reviewDocumentDeletion(versionId,reason)` checks the current owner, recent MFA, approved retention, active subject holds and expiry of every possible upload credential. It returns a review token and explicit blockers. `authorizeDocumentDeletion(versionId,{request_id,review_token,reason,confirmed:true})` permanently records the authorisation and immediately revokes file access. Persist this exact body for retry after a lost response. A saved authorisation receipt is not proof that storage deletion finished.

`documentDeletionStatus(versionId)` requires an administrator with recent MFA. Only `provider_absence_verified:true` means independent primary-storage absence verification succeeded. `pending`, `leased`, `retry` and `needs_attention` remain incomplete. For `retry` or `needs_attention`, the owner can call `retryDocumentDeletion(versionId,{request_id,attempts,reason,confirmed:true})` using the displayed attempt count; this schedules the existing authorisation and never approves a different object. Preserve exact command identity across retries. No query parameters, `If-Match` or `Idempotency-Key` are accepted. Metadata and external copies remain separate; no endpoint promises recall of downloaded files or erasure from backups. Restore operators must apply a fresh independently transferred version 5 privacy manifest before exposing restored data, with deletion workers disabled until provider reconciliation.

### Dashboard review counts

`dashboardAttention()` / GET `/dashboard/attention` requires an active administrator or manager human session. Supply `workspaceId` to select a linked workspace. API keys and employee sessions are denied. The response binds native UUID `company_id` and `member_id` to that session, unlike numeric resource identifiers elsewhere in v2.

No query parameters are accepted. Counts cover pending leave excluding the reviewer’s own requests, noncancelled draft shifts in the company’s current configured week, and closed, nonvoid unapproved attendance in the entire current company-local month. Managers see shared-workplace leave, managed-workplace shifts, and attendance attributed to its recorded workplace (with a linked-shift fallback), even when an employee has since moved. Administrators see their workspace. The returned period boundaries, timezone, `calculated_at`, and `scope` describe the snapshot; `rota_week` is the existing rota UI’s Monday navigation anchor.

Each section has a 100,000-record ceiling. Exceeding it returns 422 rather than a truncated count. Display unavailable/retry on failure; do not substitute an incomplete client history or zero. This is a review indicator, not payroll approval or an attendance export. The application refreshes every minute and offers manual refresh.

### Catalogue page boundaries

GET `/users`, `/locations` and `/roles` exclude records inserted after the first page, using a transactionally ordered company sequence rather than numeric ID order. Existing records remain live and current permissions apply on every page. Imported IDs cannot add a later record to an earlier traversal. Keep query parameters unchanged when following `next_cursor`. Cursors issued before this boundary upgrade return 400 with a restart message; discard that cursor and start a fresh traversal. Shifts retain their separate journal snapshot semantics. Use the resource change feed to reconcile changes after the initial traversal.

## Browser notification devices

The API exposes three human-session operations: `GET /notifications/push/devices`, `POST /notifications/push/devices`, and `DELETE /notifications/push/devices/{device}`. Use `workspaceId` to select the person's active workspace. API keys cannot manage personal devices. The SDK provides `pushDevices()`, `enrollPushDevice(subscription, idempotencyKey)` and `revokePushDevice(id, revision)`.

Enrollment supplies exactly `endpoint`, `p256dh`, `authentication` (the browser subscription's `auth` key), `application_key`, `label` and `consent: true`. Request browser permission only from an explicit user gesture. Use the public application key returned by discovery; validate `configured` before subscribing. Preserve the idempotency key and exact body across retries. A fresh enrollment lasts 90 days and applies only to new events. At most 20 active devices belong to one Auth identity across its workspaces. The device list returns the up to 100 records, with active devices first and remaining history newest first without endpoint capabilities or encryption secrets.

Revocation uses the listed native UUID and revision (`If-Match`); a 412 requires a fresh list. A replacement cancels old queued messages and increments its binding revision. Do not automatically revoke a replacement after a stale response. A provider's acceptance does not prove that a notification was displayed or read. The UI uses generic notification text and a fixed same-origin destination. Quiet hours apply in the workspace timezone. Previously accepted notifications cannot reliably be recalled.

Browser enrollment and outbound sending are disabled until provider/device commissioning. Service-worker support, browser permission and installed Home Screen behavior on iOS/iPadOS require actual device acceptance. Personal exports include bounded device/delivery metadata without transport secrets.

## Worker directory

`listUsers({search, location, level, limit, cursor})` returns a page of `DirectoryUser`; `getUser(id)` reads one worker. `search` is a case-insensitive literal substring of the worker’s name (1–80 characters); it does not search email, notes or compensation. `location` is a public numeric workplace ID and `level` is `admin`, `manager` or `employee`, not a job role. Filters combine with AND. Unknown or inaccessible workplaces return an empty page. Existing administrator/integration authorization still applies.

Keep every filter and the limit unchanged when following a cursor. The insertion fence excludes workers created after the first page; existing names and memberships remain live. Optional identity and legacy fields depend on authorization and source data. Missing compensation is unavailable, not zero; scoped integration compensation requires `pay:read`. Date fields `dob`, `start_date` and `final_working_date` retain legacy representations; do not assume they are normalized timestamps. Discover `user_directory_filters` in capabilities before using these filters against an older deployment.

## Team summary

`teamSummary({week, search?, location?})` reads `/team/summary` using an active administrator or manager human workspace session. `week` starts a seven-day window; `location` is a native workplace UUID, unlike the numeric directory filter. Integration keys are denied. Name search is literal and case-insensitive. The workplace filter selects workers; administrators see their full-workspace scheduled hours, while managers see only managed-workplace hours and receive `null` for the whole-workspace weekly-limit comparison. Draft shifts count; cancelled shifts do not; retained rota dates and the current paid-break preference apply. Each section is limited to 100000 matching workers or shifts; overflow returns 422 rather than partial totals.

`teamMembers({week, search?, location?, limit?, cursor?})` returns server-paged team rows, scoped workplace memberships and complete weekly hours for each row. `teamMember(nativeUuid)` independently reads a fresh profile and current management scope. Both require a human administrator/manager session; use `new ShiftForest({accessToken, workspaceId})` for the selected workspace. The profile `id` is a native UUID; `public_id` maps to the numeric `/users/{id}` catalogue and reviewed profile commands. Do not confuse these identifiers.

Directory pages default to 100 workers, maximum 100. Preserve all filters and the limit while following `next_cursor`; cursors expire after 24 hours and become invalid if identity or management scope changes. The committed insertion fence excludes newly created workers until a fresh traversal, while existing profiles, memberships and schedules remain live. Each page caps visible memberships at 10000 and scheduled shifts at 100000, returning 422 rather than partial records. Manager profiles include only managed memberships; `managed_locations` describes the caller scope for separately loaded historical rows. Profile reads do not grant write authority. Use `row_revision` and reviewed command flows for edits, and require an explicit review after a conflict.

`searchPeople({search, limit?})` provides a human-session typeahead lookup with 2–80 characters and up to 20 results (default 5). It searches literal name/role-tag substrings and returns only native worker ID, name, role, role tag and avatar colour. Administrators search their workspace; managers and employees search themselves and current workplace colleagues. `has_more` means refine the query; this endpoint has no cursor or total. Recheck on opening and discard responses after workspace, actor or query changes. Integration keys cannot use this human navigation endpoint.

`setupProgress()` (`GET /setup/progress`) is an administrator-only human read of current workspace facts: workplace presence, another workforce record, a noncancelled published assigned shift, and a separately verified linked colleague account. It accepts no query parameters. Existing imported records may satisfy the checks; the response is not first-login history, policy approval or a production-readiness certificate. Recheck after workspace updates and never substitute a partial browser cache for the server result.

## Account email APIs

`email.mjs` exports `ShiftForestEmail`. Pass an async function returning the current human Supabase access token. Optional preferences belong to that person across workspaces; integration keys cannot opt people in. Read `preferences()` and pass its revision to `savePreferences()`. New opt-ins require notice_version `2026-10-30`. On HTTP 412, reload and ask the person to review their changes. `foregroundActivity()` is only for a real visible human interaction, never background polling. `exportOwnEmailData()` returns the account's consent and recent delivery history.

Platform campaign methods require a superadmin session with TOTP authentication within the last ten minutes. Use a stable generated UUID for `createCampaign()` retries. Draft content becomes immutable after approval. Review its content hash, approve with that hash, then schedule using an ISO8601 UTC time. Mutations require the current revision. List pagination uses `next_cursor`. Pause/cancel can stop pending jobs; they cannot retract a provider-accepted email. Full schemas: https://api.shiftforest.com/openapi-email.json.

### Workspace invoice history

Use `ShiftForestBilling` from `billing.mjs` with a current administrator session and an explicit workspace UUID. Workforce integration keys do not grant access to customer billing.

```js
import { ShiftForestBilling } from "./billing.mjs";
const billing = new ShiftForestBilling(getCurrentAccessToken, workspaceId);
const first = await billing.invoices({ limit: 20 });
const older = first.next_cursor
  ? await billing.invoices({ cursor: first.next_cursor, limit: 20 })
  : null;
```

Amounts are integer GBP pence. Records are newest-first, read directly from Stripe, and may change between requests. A deleted cursor requires refreshing the first page. Invoice totals are not a refund or credit-note ledger. Draft invoices may have no number, due date or links. Invoice/PDF links are private: do not log them, publish them, or put them in shared caches. Reads create no invoices, payments or emails. The full error and pagination contract is in `/openapi-billing.json`.

### Current feature access

`await billing.features()` reads the selected workspace's database-owned feature policy. It needs a current administrator session and does not accept workforce integration keys. `can_start` is advisory discovery, not a permission reservation: each creation rechecks the policy under the workspace mutation lock. New bank/agency recovery campaigns and staffing evidence packs require Care for enforced paid accounts; supported pilots and valid trials retain access. Existing records/history, internal recovery and operational continuity remain available subject to normal permissions. The `core_workspace` entry describes attendance/record/export continuity; worker capacity and schedule-expansion expiry checks still apply independently. No subscription or payment is changed by discovery.

## Company provisioning (SCIM)

`scim.mjs` exports `ShiftForestScim`. Use a dedicated server-side provisioning key
and the identity connection UUID; workforce API keys are not accepted. The
client fixes the production origin, rejects redirects, applies a 15-second
request timeout and never automatically retries writes. Read a resource and
pass its weak ETag to `replace`, `patch` or `remove`. A failed write may have
committed before the connection was lost: reconcile by resource ID or externalId
before retrying. `ScimRequestError` exposes status, scimType and retryAfter.

The profile and limits are described in `/openapi-scim.json`. Supports bounded
Users/Groups CRUD and atomic PATCH plus authenticated discovery. The profile
requires stable externalId values and does not grant roles through groups.
Provider commissioning is required before a real customer connection is enabled.

```js
import { ShiftForestScim } from "./scim.mjs";
const client = new ShiftForestScim(
  process.env.SHIFTFOREST_SCIM_KEY,
  process.env.SHIFTFOREST_SCIM_CONNECTION,
);
const result = await client.list("Users", { count: 50, startIndex: 1 });
// Consume result.data.Resources; advance startIndex by itemsPerPage while
// startIndex - 1 + itemsPerPage < totalResults. Never log provisioning keys.
```

## Hosted developer sandbox

An administrator with recent MFA can create, reset or delete a sandbox in Settings → Developer sandbox. The key is shown once; keep it in a server secret store. Provisioning is asynchronous: reload until the allocation is `ready`. Use synthetic data only. The archive itself does not contain credentials or a preconfigured allocation.

```ts
const sandbox = new ShiftForest({
  apiKey: process.env.SHIFTFOREST_SANDBOX_KEY!,
  baseUrl: process.env.SHIFTFOREST_SANDBOX_BASE_URL!,
});
// Base URL: https://api.shiftforest.com/sandbox/{allocationUUID}/v2
// Use the same typed methods and v2 schema as production.
```

The sandbox runs the actual workforce SQL and HTTP adapter in an isolated native PostgreSQL database. Its independent synthetic integration has the documented workforce read/write scopes, including historical-resource reads; operations requiring a human session remain unavailable. Seed records include a synthetic administrator, employee, workplace, draft shift and pending leave request. List shifts with `include=drafts` to see the seeded draft. Use organization/capabilities to inspect permissions.

Limits: one live allocation per workspace, two across the initial hosting pool, 1–24 hour expiry, 120 requests/minute, 64 KiB request bodies, 100 MiB database storage. Provisioning requests are limited to three per workspace per day, with bounded resets. Capacity includes allocations awaiting cleanup. A 429 response includes Retry-After when available; 507 requires a reset. API keys start with `rt_sandbox_`; production rejects them, and sandbox rejects production keys and human sessions. Responses carry `X-ShiftForest-Environment: sandbox` and `X-Sandbox-Expires-At`.

Reset and deletion revoke the old key immediately. Reset creates a fresh synthetic company and fresh IDs; discard all cached IDs, cursors and prior idempotency keys. A sandbox key also stops working if its issuing administrator loses active workspace access. Each generation records its database schema build. Runtime upgrades must remain compatible with active generations; reset to receive a newer schema. Expired data is removed by the cleanup worker; access is denied even if cleanup is delayed. A reset is destructive and cannot be undone.

Supabase sign-in, private file storage, Stripe, email delivery, Realtime and outbound webhook delivery are not connected. No external delivery workers run inside the sandbox. This environment tests the API/SQL integration contract, not identity-provider, payroll-vendor or payment-provider acceptance. Do not use it for employee consent or real attendance records.

### Delegated workplace access

The seven `/access` operations use a human `accessToken` and optional `workspaceId`.
Except for `delegatedStaffingWorkplaces()`, they require a workspace administrator
with recent authenticator MFA. API keys cannot manage or discover these grants.

- `delegationConfiguration()` returns the current decimal revision and complete configuration.
- `delegationMembers({search, after})` and `delegationSCIMGroups({search, after})` return at most 25 current targets per page. Follow `next_cursor`; records remain live.
- `delegatedStaffingWorkplaces()` returns current accessible workplaces with `can_manage`.
- `reviewDelegation({config, revision})` returns an opaque five-minute review ID, resolved recipient labels and saved/proposed configurations.
- `applyDelegation({request_id, review_id, confirmed: true})` applies once. Persist both UUIDs and retry the exact body after an ambiguous response. Never generate a new request ID automatically after a timeout. Reload/review after 409; reauthenticate after 401/403.
- `delegationHistory({before, id})` pages newest first with the complete paired `next_cursor` and immutable previous/applied configurations.

Use opaque UUIDs returned by this namespace for member/workplace references;
these are **not** the public numeric IDs used by general workforce resource routes.
Create new UUIDs for groups, roles and grant IDs. The configuration contains
`groups` (up to 64, maximum depth eight, one direct group per workplace), `roles`
(up to 32), and `bindings` (up to 256). Supported capabilities are `staffing.read`
and `staffing.manage`; manage includes read. Workspace scope includes future
workplaces; group scope includes descendants. Every grant must have a finite
expiry within 366 days. Removing a grant invalidates subsequent requests;
downloaded copies cannot be recalled. Direct grants remain bound to the reviewed
login identity: a relinked login requires removing the old grant and creating a
new grant UUID. SCIM group grants follow future membership changes and require an
enabled connection, active member and no manual hold. No administrator, payroll,
billing, document or scheduling authority is implied by these capabilities.

All access responses are no-store. Writes reject `If-Match`, `Idempotency-Key`
and query parameters. The SDK does not generate those headers for access commands.
HTTP bodies are limited to 64 KiB; shorten large proposals if rejected. Access
changes are polled here, outside the general workforce sync feed. See the shipped
OpenAPI `DelegationConfig` schema for every field and nullable scope identifier.

### Reviewed payroll administration

API 2.24 adds `reviewPayrollChange`, `applyPayrollChange`, `payrollChangeHistory` and `payrollChangeExport`. These require a human administrator in the selected workspace with recent authenticator MFA. Integration keys cannot change verified payroll rates or corrections. General worker-profile rate edits remain unverified.

Read worker UUIDs through the human team directory. Read frozen batch UUIDs through `payrollExports`, then use `payrollChangeExport(id)` to obtain the original attendance UUIDs and frozen pay items. This namespace uses UUIDs, unlike numeric general workforce resource IDs.

Review `{kind:"rate",input:{user_id,effective_from,rate_minor}}` to set an effective-dated hourly rate. It applies until the next dated rate; frozen exports stay unchanged. Review `{kind:"adjustment",input:{export_id,clock_id,desired_minor,reason}}` to correct one exported attendance: `desired_minor` is the desired total **including** previous adjustments, not a delta. The review displays the cumulative previous total and signed difference. Successful application creates one immutable adjustment with its own ID. Import each signed adjustment only once; ShiftForest cannot enforce idempotency in another payroll product.

Review `{kind:"reconciliation",input:{export_id,adjustment_id?,external_reference,reported_minor,reported_items}}` to append an administrator-reported reconciliation. Omit `adjustment_id` for the original batch; supply it for one signed adjustment. Differences are recorded, not silently repaired. This is not provider-verified acceptance or certification.

Show the returned before/after values and notes before applying `{request_id,review_id,confirmed:true}`. Reviews expire after five minutes and there can be at most twenty pending reviews per administrator. Keep both IDs unchanged after an uncertain response. A 409 requires fresh review; a 403 requires restoring current authority/MFA. Omit `If-Match` and `Idempotency-Key`; the SDK does not generate them here. History is newest-first, 25 items per page, with paired `before` and `id` cursors; poll it outside the general sync feed. All responses are `no-store`. New change records use integer minor-unit numbers; existing general payroll resource endpoints retain their documented decimal-string fields.

### Configured overtime, enhancements and sleep-in pay

API 2.25 adds `payrollPolicies({before?})` and the `policy` payroll-change kind. Review `{kind:"policy",input:{effective_from,config}}`, then apply its review through the same durable command method. Policy publication requires a human administrator with recent MFA. No policy is activated by installation. ISO `week_start` is 1–7; the effective date must fall on that day, and the boundary cannot change after the first policy. Versions are immutable; a later reviewed version at the same effective date supersedes it only for future previews, never existing frozen exports. History contains at most 1,024 versions, in pages of 25.

Use whole-minute daily/weekly thresholds, or `null` to disable one. Multipliers use basis points: 10,000 is normal pay and 15,000 is time and a half; supported values are 10,000–50,000. The highest applicable daily overtime, weekly overtime or start-window enhancement wins without stacking. An enhancement has a unique name, ISO weekdays, start/end local minutes and a multiplier; the attendance's local **start** weekday/time classifies the entire record. These rules do not pretend to allocate historical breaks to precise night intervals. Each entire attendance counts in its local start day/week. Earlier approved attendance in that week counts even when outside the export or already exported. Incomplete or overlapping context blocks the affected calculation.

`sleep_in_mode` is `hourly_plus_allowance` or `allowance_and_disturbance`. A nonzero allowance, or disturbance-only hourly mode, requires exactly one attendance matching the complete reviewed sleep-in shift. Disturbance mode pays recorded elapsed disturbance time (including DST correctly), plus the configured allowance; it does not infer break placement. Only disturbance time counts toward overtime in that mode. Configured time rounding applies before pay components. Money uses exact integer arithmetic and positive half-up rounding per attendance/pay-code/multiplier component.

New exports capture `policy.schema_version: 2`, default `basic_rules`, every applied policy version, and per-line unit, base rate, multiplier and policy identifiers. In the general `/payroll-exports` resource, amounts, fractional minutes and microsecond durations remain decimal strings. Raw UUID discovery and human review records use bounded integer numbers; legacy exports outside the supported exact-integer change limit return 422 there and remain readable through the general decimal-string resource. The generic `components-v2` CSV includes separate base rate, multiplier and canonical amount columns. A shift allowance is a `shift` unit, not an hourly rate. Consume the canonical amount rather than multiplying the displayed base rate as if it were already enhanced.

Limits are 93 days, 100,000 attendance context records, 20,000 pay items/issues, 8 MiB per preview and seven days per attendance. No salary proration, statutory formula, tax calculation or payroll-provider acceptance is inferred. Real customer policy approval and payroll reconciliation remain necessary.

### Fixed salary and agreed payments

API 2.26 adds `payrollFixedItems({before?,id?})` and `payrollFixedItem(id)`, plus three human-administrator/MFA review kinds. `fixed_pay` requires `user_id`, `from_date`, `to_date`, `amount_minor`, `pay_code`, `reference` and `reason`. Pay codes are `SALARY`, `BONUS`, `ALLOWANCE` and `OTHER`; the explicit period can span at most 366 days. The amount is an agreed gross total, with no inferred salary proration or statutory calculation. The worker must have a payroll employee code.

Positive salary periods cannot overlap another active salary or already-paid hourly attendance. Correct the earlier items first. An original salary corrected cumulatively to zero allows a replacement period or hourly pay; reinstatement rechecks all overlap rules under the workspace lock. Salary-covered attendance cannot silently create hourly pay or an additional hourly correction, and salary hours are not automatically treated as hourly overtime context. Other explicitly agreed fixed bonuses/allowances remain separate. Identical worker/code/period/reference originals are rejected; durable request IDs also protect retries.

Review `fixed_adjustment` with `{fixed_pay_id,desired_minor,reason}`, referring to the **original** item. The desired amount includes all earlier corrections; application creates a signed delta with `original_id`. Original records, prior corrections and their captured names/codes/currency stay unchanged. The `fixed-pay-v1` CSV has unique pay item/original IDs, employee code/name, period, pay code, signed amount, currency, agreement reference and reason. It does not invent an attendance ID. Import each unique item only once.

Review `fixed_reconciliation` with `{fixed_pay_id,external_reference,reported_minor,reported_items}` for one original or one signed correction. The expected item count is one. This appends administrator-reported evidence, not verified provider acceptance. Read history in pages of 25 using the paired timestamp/UUID cursor. Fixed-payment operations remain workspace-administrator-only; a single workplace payroll grant does not grant employee-wide salary authority. All existing review expiry, MFA, exact-retry and `no-store` rules apply.

### Workplace payroll access

Human sessions discover permitted workplaces through `payrollWorkplaces()`. Money reads and writes require recent authenticator MFA. A base manager role does not grant payroll access: administrators assign finite `payroll.read`, `payroll.export` and `payroll.adjust` permissions, with workplace/group scopes and expiry. Export and adjust each include reading. Company-wide rates, policy versions, fixed salary and reconciliation remain administrator-only.

Call `reviewScopedPayroll({location_id, from_date, to_date})`, inspect all issues and canonical component amounts, then `freezeScopedPayroll({location_id, request_id, review_id, confirmed:true})`. Persist all three UUIDs for exact retries; reviews last five minutes and current authority is checked before replay. Global and scoped exports reserve attendance through the same serialized transaction.

`scopedPayrollExports(location_id, cursor)` and `scopedPayrollExport(location_id, id)` filter older company-wide batches to the permitted workplace and recompute totals/counts. `reviewScopedPayrollAdjustment(location_id, {export_id, clock_id, desired_minor, reason})` reviews the cumulative desired attendance total; apply through `applyPayrollChange`. `scopedPayrollAdjustments` returns only that workplace's signed corrections. Both histories return 25 items plus a paired before/id cursor. SDK methods return `{data, version, requestId}`. API keys and conditional/idempotency headers are rejected; use the reviewed request UUIDs. Responses are no-store, outside the general sync feed. Import each frozen batch/correction once; generic CSV does not establish provider acceptance.

### Reviewed primary-profile removal

`reviewProfileRedaction(caseId,{reason})` prepares a partial action for an open erasure request. It requires the current workspace owner with recent MFA, reviewed offboarding, an elapsed approved profile-retention policy and no active subject holds or pending dependencies. Inspect `blockers`, `enabled`, `removed_fields` and `retained_data` before displaying an explicit confirmation. The deployment must commission this action separately.

Call `applyProfileRedaction(caseId,{request_id,reason,review_token,confirmed:true})` only after confirmation. Preserve the exact request ID and body when retrying an uncertain response; a conflict requires a fresh review. `profileRedactionStatus(caseId)` returns the existing partial result or null and requires an administrator with recent MFA. These endpoints accept human access tokens, not integration API keys, and reject extra query parameters and idempotency headers.

The operation removes primary name, email, role tag, avatar preference and current API profile details/mappings. It preserves worker identifiers and financial fields, shared authentication, historical records, files, audit evidence, backups and external copies. `full_erasure_completed` and `case_closed_by_action` remain false. It invalidates previous sync cursors with 410; consumers must clear their replica and start a new checkpoint/snapshot. Restore operators must apply a fresh version 5 manifest containing profile authorizations before exposing restored data, with manual profile removal disabled until reconciliation.

### Payments, refunds and credit notes

`ShiftForestBilling.payments()`, `.credits()` and `.refunds(paymentId)` read the selected workspace's Stripe customer using an administrator session. Each accepts `cursor`, `limit` (1–50) and `signal`. Refunds require a `ch_` payment returned by the payment list; both the payment and refund cursor are checked before rows are returned. Responses use GBP integer pence and UTC timestamps, exclude provider contact/bank details and metadata, and are private/no-store. Credit-note PDF links must not be logged or published.

These observations are not a frozen accounting statement, revenue report or payout reconciliation. Pending or failed refunds are not successfully returned funds. Credit notes can describe the same adjustment as refunds: never add both totals. The UI exports only its displayed page, with GBP pence columns and spreadsheet-formula escaping. Creating refunds, issuing credits and enabling Bacs require the separate finance workflow; these read endpoints move no money or send emails.

## Reviewed attendance and credential changes

`reviewResourceChange` and `applyResourceChange` use `/resource-changes/review` and `/resource-changes/commands`. Supply a human session and the selected workspace; independent integration keys cannot assert a person's attendance or credential verification. Recent authenticator MFA is required. Credential changes require an administrator. Attendance changes require a manager who can manage the worker and the original attendance workplace; another manager must handle their own attendance. A manager who recorded a correction cannot approve that record.

The review input is a discriminated union:

- `credential`: `user_id` (worker UUID from the human team directory), `kind`, recorded `status`, nullable ISO `expires_on` and `deadline` dates. Supported kinds are `dbs`, `level3`, `first_aid`, `medication`, `driving` and `restraint`.
- `attendance_correction`: `attendance_id` (public numeric ID from `/attendance`), offset-qualified `time_in`/`time_out`, integer `break_minutes`, and a 10–1000-character `reason`. This corrects an existing completed record; it does not submit employee clock evidence or create a new manual record.
- `attendance_approval`: `attendance_ids`, containing 1–100 unique public numeric attendance IDs. All records must be completed, pending, nonvoid and in unlocked payroll periods. The batch is atomic.

Display the returned `before` and `after` facts and obtain the manager's explicit confirmation. Then send `{ request_id, review_id, confirmed: true }` to apply. Reviews expire after five minutes, with at most twenty pending per actor. Preserve both UUIDs exactly for uncertain retries. A 409 requires refreshing and reviewing again; never silently replace a failed review. Replays check current permissions before returning the historical receipt. Omit query parameters, `If-Match` and `Idempotency-Key`.

Review facts and receipts use internal UUIDs for canonical records; input attendance IDs use the existing public numeric namespace. Corrections retain audit evidence and return approval to pending. Approved/frozen payroll cannot be silently rewritten. Credential status records the administrator's attestation; it is not verification with an external certification provider. Successful changes appear through the existing attendance/credential resource change feed. Responses are `no-store` and must not be placed in a shared cache.


## Reviewed linked credential removal

Use `reviewCredentialErasure(caseId)` after primary-profile removal. The current workspace owner needs a human session and recent authenticator MFA. Display the five source counts, approved retention policies, blockers and retained-data list. Event-based retention conservatively starts from the latest linked event or offboarding; pending-review expiry is included. Active workspace holds, retained exports/shared staffing packs, unverified document-provider absence and unlinked legacy evidence block removal.

After explicit confirmation, call `applyCredentialErasure(caseId, { request_id, review_token, confirmed: true })`. Preserve that exact body for uncertain retries. A 409 requires a new review; 503 means the operator has not commissioned this action. `credentialErasureStatus(caseId)` requires an administrator with recent MFA and returns null or the partial result. The status response omits `removed_counts`; those counts belong to the original command receipt.

This removes only the worker-linked credential source, audit, command, pending-review and invalidation records. Other workforce history, verification/actor references in other workers' records, document metadata and external copies still need review. The case remains open; both completion flags are false. New credential copies for that subject are blocked. Synchronization clients must clear replicas and rebuild after a 410 privacy reset. Restore operators need a fresh version 5 deletion manifest and provider reconciliation; manual removal stays disabled after restore. Independent integration keys, extra query parameters and idempotency headers are not accepted.

### Attendance command-history redaction

`reviewAttendanceHistoryErasure(caseId)` previews the two record counts and the approved attendance, audit and integration policies. The current workspace owner must use a human session with recent authenticator MFA. Primary-profile removal, elapsed retention, no active workspace hold, resolved export copies and document-provider checks are required. Unknown legacy shapes or schema changes fail closed.

After displaying the retained-data list and obtaining explicit confirmation, call `applyAttendanceHistoryErasure(caseId, { request_id, review_token, confirmed: true })`. Persist the exact body before sending; reuse it for an uncertain retry. A 409 requires fresh review, and 503 means this action is not commissioned. `attendanceHistoryErasureStatus(caseId)` returns null or the partial result and requires an authorized administrator with recent MFA.

Shared approvals retain colleagues’ entries, actor and retry identities, and original aggregate approval counts. This worker’s entries and attendance IDs are removed from the input, snapshot and dependencies; matching pending review drafts are invalidated. Applied commands with redacted history reject replay. Canonical attendance, clock-event journals, payroll and external copies remain separately governed. The case stays open, and clients must honor the returned privacy synchronization generation. This is not complete workforce erasure or external-copy recall.

Restore operators must capture a fresh version 5 deletion manifest containing attendance-history authorizations, verify its independently transferred checksum and apply it before exposing restored records. Older manifest versions are rejected by the current recovery tools. Restored manual removal remains disabled until reconciliation; an application rollback must not roll recovery tools back to a version that omits a commissioned deletion category.

## Reviewed login recovery

`ShiftForestLoginRecovery` from `login-recovery.ts` / `login-recovery.mjs` calls the configured Supabase project's human-session RPCs. Supply `supabaseUrl`, `publishableKey`, the current user's `accessToken`, and optional administrator `workspaceId`. Never use a service-role key or a v1/v2 workforce API key. The client does not renew tokens or automatically retry requests. Create a new client after changing account/workspace or completing MFA so requests use the newly issued access token.

An administrator calls `review(userId, replacementEmail)`, verifies MFA using the existing account flow, and calls `request({id, userId, replacementEmail, reviewToken, reason})`. Retain the exact input for an ambiguous retry. This suspends the old workspace login immediately; it does not send an email. Share `/account/login-recovery?recovery=ID` through a verified channel. The recipient signs in with the already verified replacement email, calls `preview(id)`, reviews the worker/company/role and explicitly calls `accept(id, revision)`. `cancel(id)` requires administrator MFA and leaves the old login suspended.

Owner recovery requires ownership transfer first; enterprise accounts use their identity provider. A login already attached to another worker in this workspace cannot be merged. Workforce IDs/history and unrelated memberships are preserved. Personal keys, PINs, push subscriptions, pending notification deliveries and owned terminals are revoked; previously accepted provider messages cannot be recalled. Reconfigure notification consent and credentials separately. The schema is published at `/openapi-login-recovery.json` on the application origin.
