GET/api/v1/appointment-requests
GET /api/appointment-requests — the staff queue (optionally narrowed
by status), newest first.
Parameters
| Name | In | Type | Required | Description |
|---|
status | query | null | AppointmentRequestStatus | No | Exact status: `pending | approved | declined | cancelled`. |
limit | query | integer | No | |
offset | query | integer | No | |
Responses
Security: bearer
POST/api/v1/appointment-requests/{id}/approve
POST /api/appointment-requests/{id}/approve — book the requested slot
on the staff member's calendar. Re-validates availability + conflicts
at decision time (the client's listing may be stale); a slot that
stopped being free is a typed 409, never a wedged booking. Emits BOTH
`appointment.created` (the booking enters the world through the
normal creation event) and `appointment_request.approved`.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
DecideAppointmentRequest
Responses
| Status | Description |
|---|
200 | |
404 | request not found in scope |
409 | not pending, slot no longer free, double-booking backstop, version conflict, or idempotency key reused |
Security: bearer
POST/api/v1/appointment-requests/{id}/decline
POST /api/appointment-requests/{id}/decline — refuse a pending
request. No calendar effect; the note reaches the client.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
DecideAppointmentRequest
Responses
| Status | Description |
|---|
200 | |
404 | request not found in scope |
409 | not pending, version conflict, or idempotency key reused |
Security: bearer
POST/api/v1/appointments
POST /api/appointments — create a booking (409 on overlap).
Request body
CreateAppointmentRequest
Responses
| Status | Description |
|---|
201 | |
409 | slot already booked |
Security: bearer
GET/api/v1/appointments/{id}
GET /api/appointments/{id} — fetch one booking in the caller's org.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
PATCH/api/v1/appointments/{id}
PATCH /api/appointments/{id} — edit fields/status (times unchanged;
moves use the dedicated endpoint so clients get rescheduled events).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
PatchAppointmentRequest
Responses
Security: bearer
POST/api/v1/appointments/{id}/cancel
POST /api/appointments/{id}/cancel — frees the slot (status flip, never
deletion, so history survives).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/appointments/{id}/move
POST /api/appointments/{id}/move — reschedule with re-validation
(dry-run semantics inline: a conflicting move is rejected atomically
and emits a `rescheduled` event only when it lands).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
MoveAppointmentRequest
Responses
| Status | Description |
|---|
200 | |
404 | |
409 | target slot already booked |
Security: bearer
GET/api/v1/availability
GET /api/availability — list one user's weekly templates.
Parameters
| Name | In | Type | Required | Description |
|---|
user_id | query | — | No | Whose windows. Defaults to the caller; another user requires owner/admin. |
Responses
Security: bearer
POST/api/v1/availability
POST /api/availability — add a weekly template for a user.
Request body
CreateAvailabilityRequest
Responses
Security: bearer
DELETE/api/v1/availability/{id}
DELETE /api/availability/{id}?user_id= — remove one template.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
user_id | query | string | Yes | Owner of the row being deleted (must match self-or-admin rule). |
Responses
Security: bearer
GET/api/v1/calendar-exclusions
GET /api/calendar-exclusions?user_id=&from=&to=
Parameters
| Name | In | Type | Required | Description |
|---|
user_id | query | — | No | |
from | query | — | No | |
to | query | — | No | |
Responses
Security: bearer
POST/api/v1/calendar-exclusions
POST /api/calendar-exclusions — record time-off.
Request body
CreateExclusionRequest
Responses
Security: bearer
DELETE/api/v1/calendar-exclusions/{id}
DELETE /api/calendar-exclusions/{id}?user_id=
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
user_id | query | string | Yes | Owner of the row being deleted (must match self-or-admin rule). |
Responses
Security: bearer
GET/api/v1/calendar-sync
GET /api/calendar-sync — the caller's (or target user's) connections.
Parameters
| Name | In | Type | Required | Description |
|---|
user_id | query | — | No | Whose windows. Defaults to the caller; another user requires owner/admin. |
Responses
Security: bearer
POST/api/v1/calendar-sync
POST /api/calendar-sync — create a connection. Outbound-capable
directions get a feed token generated immediately (returned once).
Request body
CreateConnectionRequest
Responses
Security: bearer
GET/api/v1/calendar-sync/feed/{token}
GET /api/calendar-sync/feed/{token} — the OUTBOUND direction: the
user's live appointments as a subscribable iCalendar document.
Unauthenticated but token-scoped and rate-limited (the scheduling-links
pattern); the token resolves org + user, then the read is org-scoped. The
token-to-org resolution runs through the `app_resolve_calendar_feed_target`
SECURITY DEFINER bridge (migration 0132) — the token is in the URL with no
session, so the row is default-deny off the bare pool under RLS.
Parameters
| Name | In | Type | Required | Description |
|---|
token | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | unknown token |
429 | |
PATCH/api/v1/calendar-sync/{id}
PATCH /api/calendar-sync/{id} — edit name/direction/feed_url/status.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
UpdateConnectionRequest
Responses
Security: bearer
DELETE/api/v1/calendar-sync/{id}
DELETE /api/calendar-sync/{id} — the connection, its mirror rows, and
the exclusion rows the sync owns all go away (external busy time
frees the calendar).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
GET/api/v1/calendar-sync/{id}/events
GET /api/calendar-sync/{id}/events — the inbound mirror (the
conflict/duplicate surface Wave 14 lane 7 renders).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/calendar-sync/{id}/rotate-token
POST /api/calendar-sync/{id}/rotate-token — invalidates the old
outbound feed URL and returns the new raw token once.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/calendar-sync/{id}/sync
POST /api/calendar-sync/{id}/sync — poll now (manual trigger; the
worker lane calls the same seam on its schedule). Network fetch
happens OUTSIDE the transaction; the apply + poll-state record + event
emission run inside ONE org transaction.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
force | path | boolean | Yes | Force a poll even when the connection is disabled (admin repair). |
Responses
| Status | Description |
|---|
200 | |
404 | |
503 | external calendar unreachable/unreadable |
Security: bearer
GET/api/v1/calendar/day
GET /api/calendar/day — one local day for one user.
Parameters
| Name | In | Type | Required | Description |
|---|
user_id | query | — | No | Whose calendar. Defaults to the caller; another user requires
owner/admin. |
date | query | — | No | Local date in the TARGET user's timezone. Day view: the day itself.
Week view: any date inside the week (Monday-start). Defaults to
today in that timezone. |
Responses
Security: bearer
GET/api/v1/calendar/deadlines
GET /api/calendar/deadlines — the ORG-WIDE ticket-deadline read model
(Wave 17 lane 13): /calendar team mode overlays THIS instead of the
bounded page-1 tickets fetch. Alive tickets only (category not closed)
whose due date falls in the window; hydrated in the row itself.
Parameters
| Name | In | Type | Required | Description |
|---|
from | query | — | No | Absolute UTC window [from, to); capped at 62 days like the team
view (a deadline overlay never walks quarterly ranges). |
to | query | — | No | |
assignee_id | query | — | No | Optional per-person narrowing (personal mode rides the same model). |
Responses
| Status | Description |
|---|
200 | |
422 | validation failed |
Security: bearer
GET/api/v1/calendar/team
GET /api/calendar/team — every org member over an absolute UTC range.
Parameters
| Name | In | Type | Required | Description |
|---|
from | query | — | No | Inclusive window start (default: today's UTC midnight minus 1 day). |
to | query | — | No | Exclusive window end (default: `from` + 8 days). |
Responses
Security: bearer
GET/api/v1/calendar/week
GET /api/calendar/week — Monday-start local week for one user.
Parameters
| Name | In | Type | Required | Description |
|---|
user_id | query | — | No | Whose calendar. Defaults to the caller; another user requires
owner/admin. |
date | query | — | No | Local date in the TARGET user's timezone. Day view: the day itself.
Week view: any date inside the week (Monday-start). Defaults to
today in that timezone. |
Responses
Security: bearer
GET/api/v1/day-pack
day_pack_read
Parameters
| Name | In | Type | Required | Description |
|---|
date | query | — | No | The day to assemble; defaults to today (UTC). Callers with a local
timezone pass their own date so the panel agrees with the wall clock. |
Responses
Security: bearer
GET/api/v1/day-pack/picks
day_pack_picks_list
Parameters
| Name | In | Type | Required | Description |
|---|
ticket_id | query | string | Yes | |
Responses
Security: bearer
POST/api/v1/day-pack/picks
day_pack_pick_create
Request body
CreatePickRequest
Responses
| Status | Description |
|---|
200 | |
409 | |
422 | |
Security: bearer
DELETE/api/v1/day-pack/picks/{id}
day_pack_pick_delete
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
GET/api/v1/day-pack/rules
day_pack_rules_list
Responses
Security: bearer
POST/api/v1/day-pack/rules
day_pack_rule_create
Request body
CreateRuleRequest
Responses
| Status | Description |
|---|
200 | |
409 | |
422 | |
Security: bearer
PATCH/api/v1/day-pack/rules/{id}
day_pack_rule_patch
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
PatchRuleRequest
Responses
Security: bearer
DELETE/api/v1/day-pack/rules/{id}
day_pack_rule_delete
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/day-pack/ticks
day_pack_tick_create
Request body
CreateTickRequest
Responses
Security: bearer
DELETE/api/v1/day-pack/ticks
day_pack_tick_delete
Parameters
| Name | In | Type | Required | Description |
|---|
date | query | string | Yes | |
line_key | query | string | Yes | |
Responses
Security: bearer
POST/api/v1/dispatch/assign
POST /api/dispatch/assign — place an unassigned ticket on a
technician's board: creates the booking AND flips the ticket
assignee in one transaction.
Request body
AssignRequest
Responses
| Status | Description |
|---|
201 | |
409 | slot already booked |
Security: bearer
POST/api/v1/dispatch/bulk-move
POST /api/dispatch/bulk-move — atomic batch reschedule.
Every move is re-validated against the TARGET user's live bookings
in the union window (each moved booking vacates its old slot, so
swaps are legal); the first violating move rejects the WHOLE batch
with a typed 409 naming the move index and blocking booking.
Request body
BulkMoveRequest
Responses
| Status | Description |
|---|
200 | |
409 | target slot already booked (typed conflict) |
Security: bearer
GET/api/v1/dispatch/capacity
GET /api/dispatch/capacity — multi-day capacity horizon: the day view's
capacity math repeated per UTC day over the window, one org-wide
availability read + one exclusion read + one appointment read.
Parameters
| Name | In | Type | Required | Description |
|---|
from | query | — | No | |
days | query | — | No | Horizon length in days (1..=31). |
Responses
Security: bearer
GET/api/v1/dispatch/day
GET /api/dispatch/day — the whole board for one day.
Parameters
| Name | In | Type | Required | Description |
|---|
date | query | — | No | The day to assemble; defaults to today (UTC). |
Responses
Security: bearer
GET/api/v1/dispatch/journey-start
GET /api/dispatch/journey-start — the resolved start point plus the
explicit override, if any.
Parameters
| Name | In | Type | Required | Description |
|---|
user_id | query | — | No | Whose journey; defaults to the caller (another user requires
owner/admin). |
date | query | — | No | The day; defaults to today (UTC). |
Responses
Security: bearer
PUT/api/v1/dispatch/journey-start
PUT /api/dispatch/journey-start — set (or replace) the technician's
start point for one day.
Request body
SetJourneyStartRequest
Responses
| Status | Description |
|---|
200 | |
404 | site not found in scope |
409 | idempotency key reused |
422 | invalid start point |
Security: bearer
GET/api/v1/dispatch/match
GET /api/dispatch/match — for every unassigned open ticket carrying
required skills, the active technicians sorted best-first by how many
required skills they are missing (0 = fully qualified). Expired
certifications never count as held.
Parameters
| Name | In | Type | Required | Description |
|---|
limit | query | — | No | |
Responses
Security: bearer
GET/api/v1/dispatch/planning/allocations
GET /api/dispatch/planning/allocations — who is committed to which phase.
Parameters
| Name | In | Type | Required | Description |
|---|
project_id | query | — | No | |
Responses
Security: bearer
PUT/api/v1/dispatch/planning/allocations
PUT /api/dispatch/planning/allocations — state (or restate) a
technician's commitment to a phase.
Upsert on `(phase_id, user_id)`, so restating a share updates the row.
Appending instead would leave two contradictory shares that SUM in the
forecast and report a technician as over-committed by construction.
Request body
PhaseAllocationMutation
Responses
Security: bearer
DELETE/api/v1/dispatch/planning/allocations/{allocation_id}
DELETE /api/dispatch/planning/allocations/{id} — release a commitment.
Parameters
| Name | In | Type | Required | Description |
|---|
allocation_id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | no such allocation |
Security: bearer
GET/api/v1/dispatch/planning/forecast
GET /api/dispatch/planning/forecast — committed project load per
technician over a window.
Parameters
| Name | In | Type | Required | Description |
|---|
from | query | — | No | |
days | query | — | No | Horizon length in days (1..=31). |
Responses
Security: bearer
GET/api/v1/dispatch/queue
GET /api/dispatch/queue — the full unassigned queue with dispatcher
overrides applied: overridden tickets sort by explicit rank first
(ties fall back to the computed order), then everything else in the
computed order (priority rank, then effective SLA/due deadline).
Parameters
| Name | In | Type | Required | Description |
|---|
limit | query | — | No | Page size, clamped to [1, 500]. |
Responses
Security: bearer
PUT/api/v1/dispatch/queue/{ticket_id}
PUT /api/dispatch/queue/{ticket_id} — pin a ticket's queue position.
The ticket must exist and still be open+unassigned (an override for a
placed ticket is meaningless); idempotent via the instant envelope.
Parameters
| Name | In | Type | Required | Description |
|---|
ticket_id | path | string | Yes | |
Request body
SetQueueRankRequest
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible or not open/unassigned |
409 | version conflict or idempotency key reused |
Security: bearer
DELETE/api/v1/dispatch/queue/{ticket_id}
DELETE /api/dispatch/queue/{ticket_id} — clear a rank override.
Idempotent: clearing an unset override still returns 200 with
`cleared: false` (the dispatcher's intent is already satisfied).
Parameters
| Name | In | Type | Required | Description |
|---|
ticket_id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible |
Security: bearer
GET/api/v1/dispatch/reconciliation
GET /api/dispatch/reconciliation — planned-vs-actual rollup over the
existing travel drafts and their linked expenses: mileage, time, and
reimbursements per technician, in the org's base currency.
Parameters
| Name | In | Type | Required | Description |
|---|
from | query | — | No | Inclusive window start (default: 14 days before `to`). |
to | query | — | No | Exclusive window end (default: now). Capped at 366 days. |
user_id | query | — | No | |
Responses
Security: bearer
GET/api/v1/dispatch/routing
GET /api/dispatch/routing — the day's journey plans, typed warnings,
and (measured providers only) suggestion-grade route improvements.
Parameters
| Name | In | Type | Required | Description |
|---|
date | query | — | No | The day to assemble; defaults to today (UTC), matching the board. |
Responses
Security: bearer
GET/api/v1/dispatch/skills
GET /api/dispatch/skills — the org's skill/certification catalog.
Responses
Security: bearer
PUT/api/v1/dispatch/tickets/{ticket_id}/required-skills
PUT /api/dispatch/tickets/{ticket_id}/required-skills — full-replace
the job's required-skill set. The ticket must exist and be open +
unassigned (requirements on placed work are history, not planning).
Parameters
| Name | In | Type | Required | Description |
|---|
ticket_id | path | string | Yes | |
Request body
ReplaceTicketSkillsRequest
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible in scope (rls_sparse) |
409 | version conflict or idempotency key reused |
422 | validation failed (labels, duplicates, count) |
Security: bearer
GET/api/v1/dispatch/travel-drafts
GET /api/dispatch/travel-drafts — the caller's drafts (all users for
owner/admin via `user_id`), newest first.
Parameters
| Name | In | Type | Required | Description |
|---|
user_id | query | — | No | |
status | query | — | No | `draft` | `submitted`. |
limit | query | — | No | Page size, clamped to [1, 200]. |
Responses
Security: bearer
POST/api/v1/dispatch/travel-drafts
POST /api/dispatch/travel-drafts — a draft mileage row against a
ticket. Instant-first (IF1/IF2): idempotency-keyed, version-gated,
write-trailed.
Request body
CreateTravelDraftRequest
Responses
| Status | Description |
|---|
200 | |
404 | ticket not visible in scope (rls_sparse) |
409 | version conflict or idempotency key reused |
422 | validation failed |
Security: bearer
PATCH/api/v1/dispatch/travel-drafts/{id}
PATCH /api/dispatch/travel-drafts/{id} — edit a draft. Submitted
drafts are frozen (the time entry already exists).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
PatchTravelDraftRequest
Responses
| Status | Description |
|---|
200 | |
404 | not visible, or already submitted |
409 | version conflict or idempotency key reused |
422 | validation failed |
Security: bearer
DELETE/api/v1/dispatch/travel-drafts/{id}
DELETE /api/dispatch/travel-drafts/{id} — discard a draft. Submitted
drafts are immutable history (delete the TIME ENTRY through invoicing
corrections, never the draft's backlink).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | not visible, or already submitted |
Security: bearer
POST/api/v1/dispatch/travel-drafts/{id}/expense
POST /api/dispatch/travel-drafts/{id}/expense — book the draft's
mileage as a ticket expense at the org rate (the reimbursement the
business owes the technician or bills the client). One expense per
draft, idempotent through the standard envelope.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
CreateTravelExpenseRequest
Responses
| Status | Description |
|---|
200 | |
404 | draft not found in scope |
409 | draft already expensed or key reused |
422 | invalid amount |
Security: bearer
POST/api/v1/dispatch/travel-drafts/{id}/submit
POST /api/dispatch/travel-drafts/{id}/submit — convert a draft into a
real time entry through the EXISTING time-entry path
(`insert_time_entry` + the `time_entry.created` event), then freeze
the draft with the entry backlink. The entry's notes carry the
machine-readable provenance marker (`travel_draft:<id>`); the write
trail records the full before/after so "who did this" and undo stay
queryable. Idempotent: the same submit replays the SAME entry.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
SubmitTravelDraftRequest
Responses
| Status | Description |
|---|
200 | |
404 | draft not visible in scope (rls_sparse) |
409 | already submitted, version conflict, or idempotency key reused |
Security: bearer
PUT/api/v1/dispatch/users/{user_id}/skills
PUT /api/dispatch/users/{user_id}/skills — full-replace the
technician's skill set. The technician must be an active member of
this org.
Parameters
| Name | In | Type | Required | Description |
|---|
user_id | path | string | Yes | |
Request body
ReplaceUserSkillsRequest
Responses
| Status | Description |
|---|
200 | |
404 | user not visible in scope |
409 | version conflict or idempotency key reused |
422 | validation failed (labels, expiry, duplicates) |
Security: bearer
GET/api/v1/dispatch/work-next
GET /api/dispatch/work-next — the caller's curated queue, or another
technician's when the caller may see theirs.
Parameters
| Name | In | Type | Required | Description |
|---|
user_id | query | — | No | Whose queue to read. Admins may read another technician's (a
dispatcher setting up a shift); anyone may read their own, and a
non-admin asking for someone else is refused rather than silently
served their own. |
Responses
Security: bearer
PUT/api/v1/dispatch/work-next
PUT /api/dispatch/work-next — add a ticket to a technician's queue at a
position, or move it there if the pair already exists.
Request body
WorkNextMutation
Responses
Security: bearer
DELETE/api/v1/dispatch/work-next
DELETE /api/dispatch/work-next?ticket_id=… — drop a ticket from one
technician's queue, or from every queue when `user_id` is absent.
Parameters
| Name | In | Type | Required | Description |
|---|
ticket_id | query | string | Yes | |
user_id | query | string | No | |
Responses
Security: bearer
POST/api/v1/dispatch/work-next/reorder
POST /api/dispatch/work-next/reorder — move an entry and return the
densified queue.
Returns the WHOLE queue rather than just the moved row because the move
shifts everything between the old and new rank. A response with only the
moved entry would leave the client guessing the positions of everything
it did not drag, which is exactly the state a drag-and-drop list must not
be in.
Request body
WorkNextMutation
Responses
Security: bearer
GET/api/v1/meeting-types
GET /api/meeting-types
Responses
Security: bearer
POST/api/v1/meeting-types
POST /api/meeting-types
Request body
MeetingTypeBody
Responses
| Status | Description |
|---|
201 | |
422 | validation failure |
Security: bearer
PATCH/api/v1/meeting-types/{id}
PATCH /api/meeting-types/{id}
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
MeetingTypeBody
Responses
| Status | Description |
|---|
200 | |
404 | |
409 | version conflict or idempotency key reused |
Security: bearer
DELETE/api/v1/meeting-types/{id}
DELETE /api/meeting-types/{id} — HARD delete with audit (Wave 17 lane
13): unlike the Pause toggle (PATCH `is_active`), this removes the
template outright. A scheduling link still referencing the row refuses
with a typed conflict (the FK RESTRICT surfaces) — pause the links
FIRST or re-target them; history rows (bookings) are untouched by the
template's deletion.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | meeting type not visible in scope |
409 | a scheduling link still references this meeting type |
Security: bearer
GET/api/v1/on-call-blocks
GET /api/on-call-blocks?user_id=&from=&to=
Parameters
| Name | In | Type | Required | Description |
|---|
user_id | path | — | Yes | Whose calendar. Defaults to the caller; another user requires
owner/admin. |
date | path | — | Yes | Local date in the TARGET user's timezone. Day view: the day itself.
Week view: any date inside the week (Monday-start). Defaults to
today in that timezone. |
Responses
Security: bearer
POST/api/v1/on-call-blocks
POST /api/on-call-blocks — add a rotation segment (409 when stacking).
Request body
CreateOnCallRequest
Responses
| Status | Description |
|---|
201 | |
409 | overlapping rotation |
Security: bearer
DELETE/api/v1/on-call-blocks/{id}
DELETE /api/on-call-blocks/{id}?user_id=
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
user_id | query | string | Yes | Owner of the row being deleted (must match self-or-admin rule). |
Responses
Security: bearer
GET/api/v1/public/scheduling-links/{token}
GET /api/public/scheduling-links/{token} — unauthenticated, token-
scoped, rate-limited. First view stamps the link's lifecycle
(`first_viewed_at` + count) and emits `scheduling_link.viewed`.
Parameters
| Name | In | Type | Required | Description |
|---|
token | path | string | Yes | |
Responses
| Status | Description |
|---|
200 | |
404 | unknown token |
409 | revoked or already used |
429 | |
POST/api/v1/public/scheduling-links/{token}/book
POST /api/public/scheduling-links/{token}/book — unauthenticated but
token-scoped and rate-limited. Race-safe exactly like portal
appointment-request approval: the availability + conflict gate and
the single-winner link flip run INSIDE the booking transaction, and
the appointments EXCLUDE constraint is the concurrency backstop.
Parameters
| Name | In | Type | Required | Description |
|---|
token | path | string | Yes | |
Request body
BookRequest
Responses
| Status | Description |
|---|
201 | |
404 | unknown token |
409 | revoked/used/expired link, slot no longer free, double-booking backstop, or idempotency key reused |
422 | slot outside the link's availability or booking window |
429 | |
GET/api/v1/scheduling-groups
GET /api/scheduling-groups — the org's free-form groups (availability
pools; deliberately NOT the org chart).
Responses
Security: bearer
POST/api/v1/scheduling-groups
POST /api/scheduling-groups — create a pool.
Request body
CreateGroupRequest
Responses
| Status | Description |
|---|
201 | |
409 | name already taken |
Security: bearer
GET/api/v1/scheduling-groups/{id}
GET /api/scheduling-groups/{id}
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
PATCH/api/v1/scheduling-groups/{id}
PATCH /api/scheduling-groups/{id}
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
UpdateGroupRequest
Responses
| Status | Description |
|---|
200 | |
404 | |
409 | version conflict or idempotency key reused |
Security: bearer
DELETE/api/v1/scheduling-groups/{id}
DELETE /api/scheduling-groups/{id} — removes the pool (memberships go
with it; meeting types pointed at it retire with their FK CASCADE,
mirroring Calendly's delete semantics).
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/scheduling-groups/{id}/members
POST /api/scheduling-groups/{id}/members — add a calendar to the pool.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Request body
AddMemberRequest
Responses
Security: bearer
DELETE/api/v1/scheduling-groups/{id}/members/{user_id}
DELETE /api/scheduling-groups/{id}/members/{user_id}
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
user_id | path | string | Yes | |
Responses
Security: bearer
GET/api/v1/scheduling-links
GET /api/scheduling-links — newest-first (optionally per ticket) so
the tech sees the created / viewed / scheduled / expired lifecycle.
Parameters
| Name | In | Type | Required | Description |
|---|
ticket_id | query | — | No | |
Responses
Security: bearer
POST/api/v1/scheduling-links
POST /api/scheduling-links — mint a shareable link (token returned
once). A replay (same key + same body) replays the SAME token.
Request body
CreateSchedulingLinkRequest
Responses
| Status | Description |
|---|
201 | |
404 | meeting type not found |
Security: bearer
GET/api/v1/scheduling-links/{id}/bookings
GET /api/scheduling-links/{id}/bookings — the link → booking trail.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/scheduling-links/{id}/revoke
POST /api/scheduling-links/{id}/revoke — the link can never book
again; an already-used link keeps its `used` state so the booking
trail never changes meaning.
Parameters
| Name | In | Type | Required | Description |
|---|
id | path | string | Yes | |
Responses
Security: bearer
POST/api/v1/scheduling/conflict-check
POST /api/scheduling/conflict-check — dry-run overlap probe that writes
nothing: returns every blocking booking for the candidate slot.
Request body
ConflictCheckRequest
Responses
Security: bearer