# Crisphive Developers > The scheduling & dispatch API for field operations teams. Create bookings, preview emergency reschedule cascades, and map technicians to service boundaries — built to raise billable hours and cut travel, mobilization, demobilization and idle time. Crisphive is the API bridge between a developer and their end customer: it turns a real-world field operations problem — crews in the wrong place at the wrong time — into a booking, a dispatch and an emergency re-plan you can build against. Applies wherever work has wrench time, mobilization, demobilization and travel time (trades, delivery, infrastructure, government, facilities, healthcare, industrial). Every response returns one `{ error_code, message, data }` envelope; authenticate with a `chsk_test_` (sandbox) or `chsk_live_` (production) key as a Bearer token. ## Use cases - [Trades scheduling](https://docs.crisphive.com/use-cases/trades): Build a dispatch board that heals itself when a no-heat call lands. - [Last-Mile Delivery scheduling](https://docs.crisphive.com/use-cases/delivery): Build routing that survives a dock delay without missing a receiver window. - [Infrastructure & Utilities scheduling](https://docs.crisphive.com/use-cases/infrastructure): Build utility work-management that survives a main break under an SLA clock. - [Government & Municipal scheduling](https://docs.crisphive.com/use-cases/government): Build 311/public-works software that meets a statutory response clock. - [Facilities Management scheduling](https://docs.crisphive.com/use-cases/facilities): Build CMMS software that survives a chiller failure during PM week. - [Healthcare & Home Health scheduling](https://docs.crisphive.com/use-cases/healthcare): Build visit scheduling that re-plans a day without moving a medication window. - [Industrial Field Services scheduling](https://docs.crisphive.com/use-cases/industrial): Build turnaround software that unblocks four crews from one slipped inspection. ## Guides - [Get started with the Crisphive API](https://docs.crisphive.com/docs/introduction): Our goal is to help you with everything you’ll need when integrating with Crisphive. In these docs you’ll find answers to common questions, step-by-step how-… - [Sign in and create an API key](https://docs.crisphive.com/docs/login-and-api-keys): Sign in to your dashboard, then mint the API key your integration will authenticate with. Go to crisphive.com/login and enter the phone number or email you r… - [Authentication](https://docs.crisphive.com/docs/authentication): Requests are authenticated with a secret API key sent as a bearer token on every call. Include your API key as a bearer token. Sandbox keys use the chsk_test… - [Environments](https://docs.crisphive.com/docs/environments): Crisphive runs two isolated data environments per business: sandbox for development and live for production traffic — Stripe-style, on the same domain. - [The response envelope](https://docs.crisphive.com/docs/response-envelope): Every response shares one shape so you can parse it identically each time. On failure error_code carries a symbolic code (e.g. CUSTOMER_NOT_FOUND), data is n… - [Create your first booking](https://docs.crisphive.com/docs/create-booking): This guide walks you end-to-end from an API key to a booked job request in five steps. Create a sandbox key under Settings → Developers in your dashboard. It… - [Sync data incrementally](https://docs.crisphive.com/docs/sync-feed): Keep your system in sync without full re-scans by polling the change feed with a cursor. Job requests expose a dedicated feed at GET /v1/job-requests/changes… - [Build a multi-tenant integration with OAuth 2.1](https://docs.crisphive.com/docs/oauth): Let your users connect their own Crisphive business to your product — OAuth 2.1 with a consent screen, no API key ever copied. ## Integrations - [Integrate Crisphive](https://docs.crisphive.com/integrate): Every way to connect Crisphive: automation platforms, voice agents, AI assistants, and the API, SDKs, MCP server, webhooks and OAuth 2.1. - [MCP server](https://docs.crisphive.com/mcp): The hosted MCP server: every tool, the tool profiles (/mcp/voice, /mcp/dispatch, /mcp/crm), OAuth 2.1 and client setup. - [Use Crisphive from Claude, ChatGPT and other AI assistants](https://docs.crisphive.com/docs/ai-assistants): Let Claude, ChatGPT or any other MCP client look up customers and book and reschedule jobs for your business, in plain language. - [Set up Crisphive in Claude](https://docs.crisphive.com/docs/claude): Add the Crisphive connector in claude.ai or Claude Desktop, approve the sign-in, pick live or sandbox, set tool approvals and fix connection errors. Every step. - [Set up Crisphive in ChatGPT](https://docs.crisphive.com/docs/chatgpt): Add Crisphive to ChatGPT in developer mode or from the app directory, approve the sign-in, choose live or sandbox data and fix connection errors. Every step. - [Book jobs from a voice agent](https://docs.crisphive.com/docs/voice-agents): Let an AI phone agent identify the caller and book a confirmed technician visit while they are still on the line. There is nothing to install: each platform… - [Book field jobs by phone with ElevenLabs](https://docs.crisphive.com/docs/elevenlabs): ElevenLabs + Crisphive: build an AI phone receptionist that finds the caller, books and confirms a field visit live. Every click, prompt and test call. - [Book field jobs by phone with Vapi](https://docs.crisphive.com/docs/vapi): Vapi + Crisphive: connect the Crisphive MCP tool to a Vapi assistant so it finds callers and books confirmed field visits during the call. Step by step. - [Book field jobs by phone with Retell](https://docs.crisphive.com/docs/retell): Retell + Crisphive: add the Crisphive MCP server to a Retell agent so it looks up callers and books confirmed field visits live. Setup, test and fixes. - [Connect Zapier, Make, n8n and Pipedream](https://docs.crisphive.com/docs/automation-platforms): Book jobs in Crisphive and react to its events from the automation tool your team already uses — with no code, or very little. - [Connect Crisphive to Zapier](https://docs.crisphive.com/docs/zapier): Connect Crisphive to Zapier: book and confirm field jobs from any form, and start Zaps the moment a job is booked, completed or rescheduled, field by field. - [Connect Crisphive to Make](https://docs.crisphive.com/docs/make): Connect Crisphive to Make: watch job and customer events instantly, book and confirm field jobs, and search customers. Every module and field, step by step. - [Connect Crisphive to n8n](https://docs.crisphive.com/docs/n8n): Install the Crisphive node in n8n, book and confirm field jobs in one step, trigger workflows on job events and give an n8n AI Agent Crisphive tools. - [Connect Crisphive to Pipedream](https://docs.crisphive.com/docs/pipedream): Use Pipedream HTTP steps with the Crisphive API to book and confirm field jobs, look up callers, and run workflows on signed job and customer webhooks. ## API reference - [Authentication](https://docs.crisphive.com/technical-reference/authentication): Every request to the Crisphive API is authenticated with a secret API key sent as a bearer token in the Authorization header. Create keys from your dashboard… - [Pagination & sync feed](https://docs.crisphive.com/technical-reference/pagination): List endpoints accept page (default 1) and limit (default 15, max 1000) query parameters and return the matching page of results inside the response envelope. - [Rate limits](https://docs.crisphive.com/technical-reference/rate-limits): The API allows 600 requests per minute per key. Each response includes X-RateLimit-Remaining and X-RateLimit-Reset headers. - [Errors & response envelope](https://docs.crisphive.com/technical-reference/errors): Every response — success or failure — shares one envelope shape, so you can parse it the same way every time. On failure error_code carries a symbolic code s… - [Error codes](https://docs.crisphive.com/technical-reference/error-codes): All error codes currently returned by the public API, generated from the OpenAPI spec. A failed response carries the code in the envelope’s error_code field. - [List customers](https://docs.crisphive.com/technical-reference/list-customers): GET /v1/customers — Returns a paginated, searchable directory of the business's customer records — the customer database (CRM) behind every booking and work order. Supports the `since`/`next_since` cursor for incremental sync into an external CRM, ERP or marketing tool. - [Create a customer](https://docs.crisphive.com/technical-reference/create-customer): POST /v1/customers — Creates a customer record — the client/account profile a job request (work order) is booked against; use it to import or sync customers from your own CRM, website lead form or intake flow. Address (street, city, postal_code, ...) and coordinates (latitude/longitude) live under the nested `address` object. service_area_id must be a valid service area UUID belonging to this business. - [Delete a customer](https://docs.crisphive.com/technical-reference/delete-customer): DELETE /v1/customers/{id} — Soft-deletes a customer record, removing it from the active customer directory; existing bookings keep their customer snapshot. - [Get a customer](https://docs.crisphive.com/technical-reference/get-customer): GET /v1/customers/{id} — Returns the full customer record: profile, contact details, tier and lifetime spending summary — a 360° client view for support, upsell or CRM enrichment. contact.preferred_technician includes {id, name}. contact.service_area includes {id, name}. contact.address.latitude / contact.address.longitude are null if no coordinates saved. - [Update a customer](https://docs.crisphive.com/technical-reference/update-customer): PUT /v1/customers/{id} — PARTIAL update — send only the fields you are changing; anything you OMIT is left exactly as stored (two-way CRM sync friendly: push one field from your system of record without re-sending the record). To CLEAR a field, send it as an empty string: uid, phone, email, notes, preferred_technician_id, service_area_id. `tier` and `status` are enums with no empty member, so an empty value there is ignored rather than stored. `full_name` cannot be set to empty. The nested `address` object is all-or-nothing: omit it to leave the stored address (and its coordinates) untouched; when present it REPLACES the whole block, and missing latitude/longitude are geocoded from the address. A customer must keep at least one contact channel — an update that would clear both phone and email is refused with PHONE_OR_EMAIL_REQUIRED. - [List job requests](https://docs.crisphive.com/technical-reference/list-job-requests): GET /v1/job-requests — Paginated list of the business's bookings (work orders) with dispatch-oriented filters: workflow status, customer, assigned technician, scheduled date range and free-text search over code/description. This is also the SCHEDULE query: combine technician_id + scheduled_from/scheduled_to to read one technician's agenda for a day or week (e.g. "what is Alex doing tomorrow"), or just the date range for the whole team's calendar. - [Create a job request](https://docs.crisphive.com/technical-reference/create-job-request): POST /v1/job-requests — Books a field-operations job — the work order that enters the dispatch & scheduling pipeline. Send the customer's UUID plus requested `job_dates` (date + morning/afternoon/evening periods, ideally offered from GET /job-requests/booking-windows), optional `job_type_id` (service catalog), `skill_ids` (required technician qualifications) and a free-text description. Quoting, technician/crew assignment and completion then advance the work order through the business's workflow. - [Commit the previewed re-staffing of a technician's day](https://docs.crisphive.com/technical-reference/commit-absence-resolve): POST /v1/job-requests/absence/commit — Applies the plan returned by /absence/preview, ATOMICALLY: every assignment in one transaction (all or nothing), each job re-staffed onto its alternate at its unchanged window, its "needs attention" flag cleared, its status_version bumped. Commit VERIFIES and never re-solves — send `assignments[]` copied from `preview.resolved[]` (you may drop rows, never add or re-point them) and, to ACCEPT a priced alternative from `preview.unresolved[].alternatives[]`, the same row plus its `alternative_kind` and (for a reschedule kind) its `start_at`/`end_at`; the engine re-checks that technician at that window under exactly that relaxation. A reschedule kind rewrites `scheduled_at`, notifies the customer of the NEW TIME (`job_rescheduled`, never `tech_reassigned` on top) and fires `job_request.rescheduled`; the response row then carries `alternative_kind`, `cost`, `original_start_at`/`original_end_at` and `window_preserved: false`. Requires a time-off record covering EVERY day of the range for the technician (pending or approved); a pending one is APPROVED by the commit, because the engine's feasibility filter reads approved time-off only and without it the absent technician stays bookable everywhere else. The response mirrors the preview plus per-job `notification` evidence (dispatched | skipped + reason — what the routing WILL do, never proof of delivery), `attention_cleared`, and `time_off`. Supports Idempotency-Key. Requires job_manage AND schedule_manage (the approval is a scheduling action). See ABSENCE_RESOLVE_DESIGN.md. 409 NEXT STEPS: ABSENCE_RESOLVE_TIME_OFF_REQUIRED — the absence is not recorded well enough to commit against: `data.uncovered_dates[]` (days no eligible record touches) and `data.uncovered_jobs[]` (jobs no single eligible record spans); eligible = approved records, plus PENDING records whose span lies INSIDE the range — a pending record WIDER than the range (somebody's leave request) is never approved by this commit and is listed in `data.pending_wider_time_off_ids[]` for a human to decide. Record a sick-day time-off via `data.create_via` (POST /business/technician-time-off, pending is enough), then commit again. ABSENCE_RESOLVE_PLAN_DRIFTED — the world moved since the preview; `data.drifted[]` names the job (job_id/short_code/expected_version) and `reason` says how: `version` (the row changed or left the technician's lane), `infeasible` (the alternate can no longer take it), `occupied` (the alternate's lane overlapped after the write). No job was written; `data.time_offs[]` lists any pending record the commit had already approved — re-preview, show the new plan, commit again. ABSENCE_RESOLVE_NO_ORPHANED_JOBS — the board is empty for the range. - [Preview re-staffing a technician's whole day (sick call)](https://docs.crisphive.com/technical-reference/preview-absence-resolve): POST /v1/job-requests/absence/preview — Solves (WITHOUT writing) the re-staffing of every job on a technician's board for a date range: each job is handed to an ALTERNATE lead technician at its UNCHANGED window — the customer's appointment never moves, two overlapping jobs never land on the same alternate, and the absent technician is never a candidate. `date`/`until_date` are business-local calendar days (inclusive, ≤ 14 days). Jobs the planner cannot re-staff come back in `unresolved` with a `reason_code` (no qualified technician free / crew job / multi-day job / already in progress) — a partial plan is a normal 200, not an error. `solver.duration_ms` is server-side planner time; `solver.deterministic` is true (same input ⇒ same plan). Read-only, safe to repeat; copy `resolved[]` into the commit body. No time-off record is required to preview. When the strict pass leaves a job unresolved, its row ALSO carries `alternatives[]` — the relaxation ladder's priced options, cheapest constraint first (ABSENCE_RESOLVE_DESIGN.md §9): `reassign_out_of_area` (same window, a lead outside the job's zone — cost.distance_km/travel_minutes), then `reschedule_same_day` / `reschedule_later_day` (the earliest free window on a qualified lead, in-area before out-of-area, up to 3 working days past until_date — cost.customer_renotified, cost.day_offset, cost.sla_breached). Each option is a PROPOSAL: nothing is applied until the coordinator copies it into the commit body with its `alternative_kind` (+ `start_at`/`end_at` for a reschedule). `alternatives` is an empty array when even the ladder found nothing; `solver.alternatives_truncated` is true when the ladder's time budget cut the search short. Displacing another customer's job and overtime are deliberately NOT offered. See ABSENCE_RESOLVE_DESIGN.md. - [Book, schedule and confirm a job in one call](https://docs.crisphive.com/technical-reference/book-and-confirm-job-request): POST /v1/job-requests/book-and-confirm — Creates the job, quotes it (job_duration_minutes, or the job type's default_duration_minutes) and confirms it at scheduled_at on the customer's behalf — the dashboard's "book for a caller" in a single request, built for voice agents and automation platforms that cannot run create → quote → slots → confirm. Customer: send `customer_id`, or `customer` (+ `address`) to match-or-create by phone/email — a caller who already exists is matched, never duplicated. Find a caller first with listCustomers?phone=. Every input is validated BEFORE anything is written (time and business timezone, future start, a duration or a job-type default, active job type, phone format); those failures are ordinary 4xx and create nothing. ⚠️ Once the job is created it is never discarded, and the call answers 200 even if scheduling then fails: `confirmed: false` with `refusal` (stage + the exact error_code/data the quote or confirm endpoint would have returned, e.g. JOB_REQUEST_NO_TECHNICIAN_AVAILABLE with blockers). The job is then quoted and waiting in the coordinator's queue — tell the caller the office will confirm a time. Retry with the SAME Idempotency-Key to replay the result; a new key books a second job. - [Booking availability](https://docs.crisphive.com/technical-reference/list-job-request-booking-windows): GET /v1/job-requests/booking-windows — Real-time appointment availability from the scheduling engine: returns the bookable date + time-period windows given technician capacity, working hours and service-territory coverage. Call this before creating a job request and offer the customer ONLY the returned windows — it prevents unschedulable bookings. - [Poll for new & changed job requests (sync feed)](https://docs.crisphive.com/technical-reference/list-job-request-changes): GET /v1/job-requests/changes — Keep an external system (your CRM, ERP or field-operations tool) in sync with bookings WITHOUT re-listing everything: returns the job requests (work orders) whose state changed (created, status transition, reschedule, soft-delete/archive) at or after the `since` cursor, ordered oldest-change-first (updated_at ASC). How to use it: (1) On your first poll OMIT `since` — the server primes the cursor at "now", returns no items and a `next_since`. (2) Store `next_since` and pass it as `since` on the next poll. (3) Apply each returned item to your store by UPSERTING on `id` (the server re-scans a ~5s safety window, so the same job may appear again — never blindly append). (4) If `has_more` is true the page filled to `limit` and more changes are already waiting — poll again immediately; otherwise wait your normal interval (e.g. 5–15s). This is NOT pagination — it is a time-keyed change feed. Use the paginated GET /job-requests for the initial bulk load, then this endpoint to stay live. Filters (status_keys, customer_id, …) narrow the feed to the slice you care about. - [Rank technicians for a P0 emergency insert](https://docs.crisphive.com/technical-reference/list-emergency-candidates): POST /v1/job-requests/emergency/candidates — Returns the technicians who could take the emergency job at the requested start, ranked FASTEST-ARRIVAL first (arrival beats route efficiency for a P0). The response also carries a historical `crew_recommendation` (median crew size on comparable completed jobs + mandatory disclaimer — AC-2). Booked technicians are still candidates — each entry carries the displacement preview (which lower-priority jobs would be pushed, per day) that committing to them would cause; total_moves=0 means a free slot. P0 jobs are never displaced; P1 only by a P0. ETA is estimated from the technician's start location (no live GPS). `after_hours_override=true` — the coordinator has phoned the technician — drops the non-working-day rejection AND each candidate's working-hours/time-off feasibility check; an affected candidate carries a per-technician TIME_OFF_OVERLAP warning instead. This is THE phone list for an after-hours insert: it ranks even on a day with no working hours once the flag is set. Feed the chosen technician_id into emergency/preview + emergency/commit. 409 NEXT STEPS: EMERGENCY_RESCHEDULE_NOT_ELIGIBLE — the job cannot be emergency-inserted; `data.failed_precondition` names which (not_quoted | archived | completed | not_p0 | smart_assign_unavailable): fix the job state or use a normal confirm. EMERGENCY_RESCHEDULE_CREW_UNSUPPORTED — crew jobs cannot use the emergency flow (v1, `data.crew_size` = lead + buddies): staff via confirm/reassign instead. EMERGENCY_RESCHEDULE_MULTIDAY_UNSUPPORTED — either a confirmed multi-day job (`data.session_count`/`data.session_dates`; use the normal reassign flow) or a single visit longer than the structural span bound, in which case `data.reason=visit_too_long` + `data.blockers[0]` (visit_minutes/max_minutes) name it — no remedy but a shorter visit. EMERGENCY_RESCHEDULE_NO_WORKING_DAY — the chosen date has no working hours (`data.business_timezone`/`data.requested_weekday`): pick a working day, or set after_hours_override=true (the coordinator has phoned someone) to rank candidates anyway. EMERGENCY_RESCHEDULE_IN_PAST — start time already passed: pick a future time; `data` carries the timezone the naive start_at was read in (`business_timezone`) plus the instant it resolved to, so a start that looks future on the caller's own clock can be diagnosed without guessing. - [Commit emergency insert + cascade reschedule](https://docs.crisphive.com/technical-reference/commit-emergency-reschedule): POST /v1/job-requests/emergency/commit — Applies the cascade previewed by /emergency/preview: assigns the emergency job to the technician and pushes the displaced jobs back (or, with `displacement_mode=reassign`, re-staffs them onto their previewed alternates first), atomically. Supports Idempotency-Key. The server recomputes the plan under a lock and fences each job on its status_version — if anything changed since the preview it returns 409 EMERGENCY_RESCHEDULE_PLAN_DRIFTED (re-preview). Same body as preview + optional `emergency_expected_version`. `after_hours_override=true` must match the preview it followed — it drops the non-working-day rejection (this endpoint never runs the technician's working-hours/time-off feasibility check; that only happens on /candidates), and the response carries an AFTER_HOURS warning; the committed job's activity feed also records who authorized the after-hours placement. Isolated feature (see EMERGENCY_RESCHEDULE_DESIGN.md). 409 NEXT STEPS: EMERGENCY_RESCHEDULE_PLAN_DRIFTED — the schedule changed between your preview and this commit (another booking/move won a lane); `data.drifted[]` names the job(s) whose status_version moved when the fence can attribute it (absent, never empty, when only a length mismatch is known): call /preview again, show the fresh plan, then commit. EMERGENCY_RESCHEDULE_SLOT_OCCUPIED — landing window blocked by an immovable anchor (P0/crew/multi-day); `data.conflicts[]` names it: another tech or time. Other codes — same remedies and `data` shapes as /candidates. EMERGENCY_RESCHEDULE_NO_WORKING_DAY fires on a closed day without after_hours_override and carries `data.override_available: true` — confirm with the coordinator, then retry with the flag. - [Preview emergency insert + cascade reschedule](https://docs.crisphive.com/technical-reference/preview-emergency-reschedule): POST /v1/job-requests/emergency/preview — Computes (WITHOUT writing) the cascade of inserting an emergency job onto a technician at a chosen time: where the emergency lands + every job pushed back, grouped per business-local day. `displacement_mode=reassign` instead hands each displaced job to another feasible technician at its ORIGINAL window (same-day promise) — jobs with no alternate capacity fall back to reschedule and stay in `days`. `mode=overtime` keeps everyone same-day (tech works late); `mode=next_day` rolls overflow to the next working day(s). On a day the business does not work the preview does NOT need `after_hours_override`: it plans as the override would and returns `after_hours_override_required: true` plus an AFTER_HOURS warning — ask the coordinator whether they have phoned the technician, then commit with after_hours_override=true (the commit refuses without it). This endpoint never runs the technician's working-hours/time-off feasibility check (that only happens on /candidates) — it validates the named technician exists and builds the cascade. Read-only — safe to call repeatedly; commit is a separate endpoint. Isolated feature (see EMERGENCY_RESCHEDULE_DESIGN.md). 409 NEXT STEPS: EMERGENCY_RESCHEDULE_SLOT_OCCUPIED — the landing window is blocked by a job the cascade may NOT move (another P0, a crew or multi-day job); `data.conflicts[]` names each blocking job (short_code/start_at/end_at/frozen_because): choose another technician (walk the /candidates ranking) or another time; displacement never touches P0/crew/multi-day anchors. EMERGENCY_RESCHEDULE_NOT_ELIGIBLE / CREW_UNSUPPORTED / MULTIDAY_UNSUPPORTED / IN_PAST — same remedies and `data` shapes as /candidates. EMERGENCY_RESCHEDULE_NO_WORKING_DAY is never returned by the preview: since 2026-10-03 a closed day answers 200 with after_hours_override_required=true instead. - [Get a job request](https://docs.crisphive.com/technical-reference/get-job-request): GET /v1/job-requests/{id} — Returns the full work order: current workflow status, quoted duration, confirmed schedule, customer contact snapshot and the assigned technician / crew — everything a dispatcher or an external field-operations system needs to track one job. - [Confirm a booking on behalf of the customer](https://docs.crisphive.com/technical-reference/confirm-job-request): POST /v1/job-requests/{id}/confirm — Fires the customer-actor `confirm_booking` action from the BUSINESS surface (audited as business_on_behalf). Two uses: (1) LIVE — staff confirm a slot for a customer who booked by phone; (2) SANDBOX — the customer magic-token surface is live-only (a sandbox job's link can never reach a real customer), so this is the ONLY way to drive a sandbox test job past booking (book → quote → confirm → assign → complete). Body carries the customer-chosen scheduled_at (business-local naive datetime). DECISION TABLE — every 409 this endpoint returns, and the correct NEXT STEP (branch on error_code, never on the HTTP status): • JOB_REQUEST_STAGE_CONFLICT — the job changed since you read it (NOTE: every FAILED confirm attempt also bumps status_version by design). Next: re-GET the job, retry with the fresh status_version. • JOB_REQUEST_ACTION_NOT_PENDING — the job is no longer at the confirm step (usually: already confirmed). Next: re-GET and show current status; do not retry. • JOB_REQUEST_NO_TECHNICIAN_AVAILABLE — the TIME is infeasible for everyone (outside working hours / the customer window, or nobody qualifies). Best-effort `data` breakdown: `considered` (roster size checked), `blocked_by` (histogram of blockers[0].kind → count, only kinds that actually blocked someone), `truncated` (roster larger than the check covered). Next: pick another time via booking-windows / time-segments. NOT an emergency case — displacement cannot conjure capacity. • JOB_REQUEST_TECH_INFEASIBLE — the FORCED technician can never take the job then; `data.reason` says why (blockers[0].kind): outside_service_area | missing_required_skills | not_lead_tier | no_working_day | on_time_off | off_shift | visit_too_long | cannot_arrive_in_time (see `data.earliest_feasible_at`, RFC3339 UTC — the first same-day time they CAN be on site → offer it) | not_available_today (diagnosis unavailable). `data.blockers[]` names EVERY cause, most-structural first (`data.technician` carries an id; `name` is always empty on this confirm path today — nothing here calls the name lookup move's TECH_NOT_FEASIBLE payload uses); a UI that reads only `reason` still works. Next: keep the tech and reschedule to earliest_feasible_at+, OR keep the time and drop technician_id (auto-pick) / choose another tech from time-segments. NOT an emergency case. • JOB_REQUEST_P0_REQUIRES_DISPLACEMENT — the ONLY code that routes to the EMERGENCY flow: the job is P0, the tech qualifies, but the lane is genuinely occupied. Next: POST emergency/candidates → preview → commit (the commit auto-confirms). Caveat: if the occupying jobs are themselves P0 the preview will reject with EMERGENCY_RESCHEDULE_SLOT_OCCUPIED (P0 never displaces P0) — then pick another tech/time. - [Matching crew candidates for a job](https://docs.crisphive.com/technical-reference/list-crew-candidates): GET /v1/job-requests/{id}/crew-candidates — RE-STAFFING candidates for a CONFIRMED, SCHEDULED job (not yet completed/archived) — any earlier or later stage returns 409 JOB_REQUEST_INVALID_TRANSITION. This is the pool of technicians who could REPLACE the current crew: the currently assigned lead and buddies are deliberately excluded (they are the status quo, not an option), so on a small roster an empty `leads` list is a normal answer, not an error. For pre-booking discovery ("who could take this job before it is confirmed?") use listJobRequestBookingWindows / listMatchingSlots / the time-segments grid instead. Candidates are matched and ranked by the smart-assignment engine — skills per crew slot, weekly availability, existing schedule, time off and travel are all checked; each carries a score breakdown (distance, travel, matched skills) plus the exact on-site session plan they would work. NOT a raw roster list (use GET /technicians for that). Returns the ranked feasible LEAD pool by default; pass include_buddies=true to also return per-slot buddy pools, include_vehicle=true to include the available-vehicle list. force_lead_id checks one specific technician: returns only that lead (with their crew combo) if feasible, else 409 JOB_REQUEST_NO_TECHNICIAN_AVAILABLE. - [Commit a schedule-board job move](https://docs.crisphive.com/technical-reference/commit-job-request-move): POST /v1/job-requests/{id}/move/commit — Applies the move previewed by /move/preview: places the job on the technician at the new time and pushes the displaced jobs back, atomically (per-tech advisory lock; the server recomputes the plan and fences each job on its status_version — drift since the preview returns 409 SCHEDULE_MOVE_PLAN_DRIFTED, re-preview). Same body as preview + optional `expected_version`. See SCHEDULE_BOARD_DESIGN.md. 409 NEXT STEPS: SCHEDULE_MOVE_PLAN_DRIFTED — the schedule changed since your preview (or expected_move_ids/expected_member_ids no longer match): re-preview, show the fresh plan, commit again; carries no further `data` (the fence cannot attribute the drift to one specific job). All other codes — same remedies AND `data` shapes as /move/preview. - [Preview a schedule-board job move](https://docs.crisphive.com/technical-reference/preview-job-request-move): POST /v1/job-requests/{id}/move/preview — Computes (WITHOUT writing) the outcome of moving a confirmed job to a new time and/or technician: where it lands, every later job pushed back per `mode`, and the warnings the coordinator would accept (displaced jobs leaving their confirmed windows, overtime). Same technician = pure time move; different technician = manual reassign. Read-only — safe to call repeatedly while dragging; commit is a separate endpoint. On a day the business does not work, a p0 single-person job previews WITHOUT after_hours_override: the plan is computed as the override would place it and the response carries `after_hours_override_required: true` — ask the coordinator whether they have phoned the technician, then commit with after_hours_override=true. Any other job is refused with SCHEDULE_MOVE_NO_WORKING_DAY and `data.override_available: false`. See SCHEDULE_BOARD_DESIGN.md. Warning detail: a TECH_NOT_FEASIBLE warning carries `reason` (= `blockers[0].kind`) = `outside_service_area` | `missing_required_skills` | `not_lead_tier` | `no_working_day` | `on_time_off` | `off_shift` | `visit_too_long` | `cannot_arrive_in_time` (commute from the tech day-start location / shift start; `earliest_feasible_at`, RFC3339 UTC, is the first same-day time they CAN be on site — suggest it as the drop slot) | `not_available_today` (diagnosis unavailable). `blockers[]` names EVERY hard filter that failed, most-structural first — a client reading only `reason` still works. For a P0 move this warning is advisory (coordinator may commit anyway) and carries the SAME `blockers[]` a p1/p2/p3 move would get as the hard 409 SCHEDULE_MOVE_TECH_INFEASIBLE below. 409 NEXT STEPS: SCHEDULE_MOVE_NOT_ELIGIBLE (job unconfirmed/unquoted/archived/completed — `data.failed_precondition` names which) · SCHEDULE_MOVE_IN_PROGRESS (tech already executing — `data.fired_actions[]` lists the actions already fired; do not move) · SCHEDULE_MOVE_IN_PAST (pick a future time — its `data` carries `business_timezone`, the naive `start_at` and the `start_at_utc` it resolved to, which is what tells a caller whose own clock says otherwise where the difference came from) · SCHEDULE_MOVE_SLOT_OCCUPIED (landing window blocked by an immovable anchor; `data.conflicts[]` names it — another tech/time) · SCHEDULE_MOVE_TECH_INFEASIBLE (non-P0 hard block: target tech not qualified/available — its `data` carries `technician` (id+name), `reason` (same catalog as the TECH_NOT_FEASIBLE warning above), `blockers[]` (every cause, most-structural first) and, for `cannot_arrive_in_time`, `earliest_feasible_at` (RFC3339 UTC) to suggest as the drop slot; change tech or time) · SCHEDULE_MOVE_MULTIDAY_UNSUPPORTED (multi-day jobs not movable v1 — `data.session_count`/`data.session_dates`) · SCHEDULE_MOVE_NO_WORKING_DAY (`data.business_timezone`/`data.requested_weekday` — pick a working day, or set after_hours_override=true for a P0 whose technician has been phoned) · SCHEDULE_MOVE_REQUIRES_FREE_SLOT (non-P0 moves may not displace — `data.would_push[]` names the jobs that would be pushed, `data.allow_non_p0_displacement: false` names the setting that would permit it, unless the crew case sets `data.crew_never_displaces: true` instead — free capacity only) · SCHEDULE_MOVE_CREW_UNSTAFFABLE (a crew slot has no feasible replacement at the new time — another time). A landing outside the customer-confirmed window is NOT an error — it returns 200 with a MOVED_OUTSIDE_WINDOW warning (customer_window attached) that the coordinator overrides. - [Set job priority (scheduling staff)](https://docs.crisphive.com/technical-reference/update-job-priority): PATCH /v1/job-requests/{id}/priority — Sets the P0–P3 priority on a non-archived, non-completed job (Owner / Administrator / Booking Coordinator). Allowed values: "p0" (emergency, interrupt-driven) | "p1" (top — displaced only by p0; may carry an sla_deadline arming auto-escalation) | "p2" (standard) | "p3" (deferrable, first displacement victim). sla_deadline is only valid with p1 and must be in the future (business-local naive datetime); moving away from p1 disarms the SLA clock. Accepts UUID or short_code in :id. Lowering a job to anything but p0 never moves it: when it still sits outside the business's working hours (typically placed there earlier through the after-hours override) the response carries an AFTER_HOURS warning per affected session, and the job should be moved back to a working day. - [Fire quote (FIXED action — business)](https://docs.crisphive.com/technical-reference/quote-job-request): POST /v1/job-requests/{id}/quote — Sends the quote: sets quoted_at + duration cols, advances pending_action to confirm_booking. Status stays `booking`. job_duration_minutes may be omitted when the job's job type has a default_duration_minutes: the type's default duration and buffers are used (a buffer you send still wins). This is how an automation or voice agent schedules work it cannot size. Before writing, checks that the customer will see at least one slot: the same engine as the customer slot picker runs over the windows the customer asked for with THIS quote's duration (working hours, service areas, time-off, existing bookings, crew coverage). If no slot exists the quote is refused with 409 JOB_REQUEST_QUOTE_NOT_SCHEDULABLE; data.reason says why (outside_working_hours, requested_windows_passed, outside_service_area, off_shift, on_time_off, missing_required_skills, no_technician_available, ...) and data.blocked_by counts the roster per blocker. Send force=true to schedule it anyway after agreeing a time with the customer. - [Matching time slots for a quoted job](https://docs.crisphive.com/technical-reference/list-matching-slots): GET /v1/job-requests/{id}/time-segments — Bookable arrival-window slots for a quoted job, computed by the smart-assignment matching engine: each slot lists the technicians actually available to start then (skills, weekly availability, existing schedule, time off and travel all checked), with a per-technician match score. Use it to find and offer appointment times an agent or integration can then confirm (POST /job-requests/{id}/confirm with the slot's business_time.datetime). Same grid the end-customer's slot picker shows; slot width defaults to the business's arrival window — override via ?step_minutes (5–240). The job must be quoted first (the quote sets the visit duration the matcher schedules). - [Job timeline](https://docs.crisphive.com/technical-reference/get-job-request-timeline): GET /v1/job-requests/{id}/timeline — Per-status progress of a job's lifecycle (e.g. booked → confirmed → on the way → arrived → completed, following the business's configured workflow) — render it as a job-tracking timeline. Each status carries its state (completed | current | upcoming), when the job entered it, and the actions fired within it. entered_at may be null for upcoming steps and for older jobs predating the backfill. - [Find nearby feasible technicians (job-less location query)](https://docs.crisphive.com/technical-reference/list-nearby-technicians): GET /v1/technicians/nearby — Ranks who could serve a hypothetical visit at (lat,lng) starting `at` for `duration_minutes` — the engine applies the REAL hard filters (weekly hours, existing schedule, approved time-off, geographic service areas, optional skill floor) and returns candidates nearest-arrival first. ETA origin is each technician's start location (no live GPS). Use before creating a booking to propose realistic arrivals. - [One technician's real schedule (sessions + time off)](https://docs.crisphive.com/technical-reference/get-technician-schedule): GET /v1/technicians/{id}/schedule — The technician's ACTUAL occupancy over a date range: every job session on their lane (solo/lead and crew) plus approved time-off blocks. Weekly recurring working hours come from the technician-availability endpoints — combine both for the full availability picture ("get crew availability"). from/to are business-local dates (YYYY-MM-DD, inclusive); omitted = today .. +7 days; range max 31 days. - [List job types](https://docs.crisphive.com/technical-reference/list-job-types): GET /v1/job-types — Returns the business's service catalog — the job/work-order types it offers (e.g. installation, repair, maintenance, inspection for trades like HVAC, plumbing, electrical, cleaning). Use it to discover the `job_type_id` accepted when booking a job request, or to render a services menu on your own site. - [Add a job type to the catalog](https://docs.crisphive.com/technical-reference/create-job-type): POST /v1/job-types — Creates a kind of work customers can book, such as "Annual boiler service" or "Drain unblocking". Job types classify bookings: createJobRequest takes an optional `job_type_id` from this catalog and the job keeps the type's name as it was at booking time. `name` is the only required field and must be unique in the business (JOB_TYPE_DUPLICATE). `status` defaults to active. An inactive type stays in the catalog but cannot be chosen for new job requests; use that rather than deleting a type you may revive. Send an Idempotency-Key header (the `idempotency_key` argument over MCP) so a retried call replays the original response instead of creating a duplicate type. Optional default_duration_minutes (+ default_mobilization_minutes / default_demobilization_minutes) set how long this kind of work usually takes: quoteJobRequest uses them when it is sent no job_duration_minutes, so an automation or voice agent can schedule the job without knowing the length. A buffer needs a duration (JOB_TYPE_INVALID_DEFAULT_DURATION). This defines the catalog, not a booking. To book actual work use createJobRequest and reference the job type there. - [Remove a job type from the catalog](https://docs.crisphive.com/technical-reference/delete-job-type): DELETE /v1/job-types/{id} — Soft-deletes the entry: it disappears from the catalog and can no longer be selected for new bookings. Jobs already booked against it are unaffected and keep showing the name they were booked with. Prefer updateJobType with `status=inactive` in almost every case: it has the same effect on the booking form and is trivially reversible. Delete is for a type created in error or one that will never return. Rows the platform ships with (`is_system=true`) cannot be deleted and are refused with JOB_TYPE_SYSTEM_READ_ONLY. - [Get a job type](https://docs.crisphive.com/technical-reference/get-job-type): GET /v1/job-types/{id} — Returns one entry of the business's service catalog (job/work-order type) with its localized display name — e.g. an HVAC tune-up, drain cleaning or electrical inspection offering. - [Rename a job type or change its availability](https://docs.crisphive.com/technical-reference/update-job-type): PUT /v1/job-types/{id} — Edits a catalog entry in place. A rename applies to NEW bookings only: every job stores the job-type name it was booked with, so existing and completed jobs keep their original label. Partial update: omit a field to keep it. `name` rejects "" because a type must stay identifiable, and must stay unique (JOB_TYPE_DUPLICATE). Setting `status` to inactive is the reversible way to take a type off the booking form; inactive types are refused for new job requests. Default quote bundle: default_duration_minutes / default_mobilization_minutes / default_demobilization_minutes follow the same partial rule; omit to keep, 0 to clear, a value to set. Clearing the duration while a buffer stays is refused (JOB_TYPE_INVALID_DEFAULT_DURATION). Rows the platform ships with (`is_system=true`, e.g. the default "General" type) keep their name and status read-only (JOB_TYPE_SYSTEM_READ_ONLY); their default quote bundle IS editable. Create your own type if you need different wording. - [List service areas](https://docs.crisphive.com/technical-reference/list-service-areas): GET /v1/service-areas — Returns the business's geographic coverage: paginated service areas (service territories / coverage zones) used for routing jobs to the right teams. Discover the `service_area_id` values accepted on customer create/update here. - [Define a territory the business serves](https://docs.crisphive.com/technical-reference/create-service-area): POST /v1/service-areas — Creates a service area: a named region used as a HARD filter when deciding who can take a job. A technician assigned to no area covering the job's address is never offered by listNearbyTechnicians, listMatchingSlots or listCrewCandidates, and never auto-assigned at confirm, whatever their skills or availability say. `name` is the only required field and must be unique (SERVICE_AREA_DUPLICATE_NAME). Coverage is matched two ways: with a `boundary` (GeoJSON polygon), a geocoded address must fall inside the polygon; without one, the area matches addresses by equality on its postal_code, city or district. A polygon is the precise option; the administrative fields are the fallback, and also what matches jobs whose address could not be geocoded. An invalid polygon is refused with SERVICE_AREA_INVALID_BOUNDARY. Creating the area does not staff it. Assign technicians with replaceTechnicianServiceAreas, or pass `service_area_ids` to createTechnician. Send an Idempotency-Key header (the `idempotency_key` argument over MCP) so a retry does not create a duplicate area. - [Stop serving a territory](https://docs.crisphive.com/technical-reference/delete-service-area): DELETE /v1/service-areas/{id} — Soft-deletes the service area: it disappears from listServiceAreas and stops counting for coverage immediately. Technician assignments to it are left in place but no longer grant coverage. The consequence is easy to underestimate: technicians whose only coverage was this area become unmatchable for addresses inside it. Jobs already assigned keep their technician, but any re-plan (reassign, board move, confirm of a pending job, the slot picker) can find no feasible crew there. Before deleting, re-check listCrewCandidates on upcoming jobs in that territory. If you are reshaping coverage rather than withdrawing from it, edit the polygon or postal/city fields with updateServiceArea instead; that keeps the area and its technician assignments working. - [Get a service area](https://docs.crisphive.com/technical-reference/get-service-area): GET /v1/service-areas/{id} — Returns one service area — a geographic coverage zone (service territory) the business operates in, with its name and geometry metadata. Reference its UUID as `service_area_id` on customer records for territory-aware dispatch. - [Adjust a territory's details or its boundary](https://docs.crisphive.com/technical-reference/update-service-area): PUT /v1/service-areas/{id} — Edits a service area in place, keeping its id and every technician already assigned to it. Partial update: omit a field to keep it, send "" to clear an optional text field; `name` rejects "". `boundary` is the one to watch: omitting it KEEPS the stored polygon, while sending one REPLACES it outright (no partial merge of geometry). This tool cannot remove a polygon once set. A boundary or postal/city change takes effect for every NEW matching decision (quote checks, confirm, reassign, board moves, slot pickers), including for jobs already on the calendar when they are next re-planned. Jobs already assigned are not re-evaluated automatically, so after moving an edge, re-check listCrewCandidates on upcoming jobs near it. - [List skill categories](https://docs.crisphive.com/technical-reference/list-skill-categories): GET /v1/skill-categories — Returns paginated skill categories — how the business groups technician qualifications by trade or specialty (e.g. HVAC, plumbing, electrical) — ordered alphabetically. - [Create a skill category](https://docs.crisphive.com/technical-reference/create-skill-category): POST /v1/skill-categories — Creates a skill category for the current business. Categories group skills (e.g. "Plumbing", "Electrical"). Names must be unique within a business. - [Delete a skill category](https://docs.crisphive.com/technical-reference/delete-skill-category): DELETE /v1/skill-categories/{id} — Permanently deletes a skill category. Returns SKILL_CATEGORY_NOT_EMPTY (409) if any skills still belong to the category — remove or move all skills first. - [List skills in a category](https://docs.crisphive.com/technical-reference/list-skills-by-category): GET /v1/skill-categories/{id}/skills — Returns paginated skills (technician qualifications/certifications) belonging to the given trade/specialty category, ordered alphabetically. The `members` field on each skill is the count of active technicians currently holding it — a quick capacity check per capability. - [Create a skill](https://docs.crisphive.com/technical-reference/create-skill): POST /v1/skill-categories/{id}/skills — Creates a skill under the given category. New skills are active by default. Skill names must be unique within their category. - [List all skills](https://docs.crisphive.com/technical-reference/list-skills): GET /v1/skills — Returns the flat list of all active technician skills / qualifications for the current business — the vocabulary the dispatch engine uses for skill-based matching when assigning technicians and crews. Use it to discover the skill UUIDs accepted in `skill_ids` when creating a job request. (For a category-grouped view, use GET /skill-categories and GET /skill-categories/{id}/skills.) - [Delete a skill](https://docs.crisphive.com/technical-reference/delete-skill): DELETE /v1/skills/{id} — Permanently deletes a skill. Returns SKILL_HAS_MEMBERS (409) if any active technicians are still assigned — unassign all technicians first. - [Update a skill](https://docs.crisphive.com/technical-reference/update-skill): PUT /v1/skills/{id} — Updates a skill's name, description, and/or active status. `is_active` is optional — omit the field entirely to keep the current value; send `false` to deactivate or `true` to reactivate. Deactivating a skill prevents new technician assignments but does not remove existing ones. - [List skills for a technician](https://docs.crisphive.com/technical-reference/list-technician-skills): GET /v1/technicians/{id}/skills — Returns paginated skills assigned to the technician. By default (`eligible_only` omitted or `true`) only active skills are returned — pass `eligible_only=false` to include inactive skills. - [Replace a technician's skills](https://docs.crisphive.com/technical-reference/replace-technician-skills): PATCH /v1/technicians/{id}/skills — Sets the technician's full skill set in one call (replace semantics): skills not in the list are removed, new ones added. Pass an empty list to clear all. All skills must be active and belong to the business — on SKILL_NOT_FOUND (404) the `data` field contains `{"missing_ids": ["uuid", ...]}`; on SKILL_INACTIVE (409) it contains `{"inactive_ids": ["uuid", ...]}`. - [List groups the caller may assign to a member](https://docs.crisphive.com/technical-reference/list-assignable-groups): GET /v1/permission/groups/assignable — Returns the role groups the CURRENT caller is allowed to hand out when creating or re-roling a team member, filtered server-side by the role-assignment ceiling (system groups: at or below the caller's own rank, inclusive; custom groups: only those whose permissions are a subset of the caller's). Build the member-create role dropdown from THIS list — the same rule is enforced on POST/PUT /business/technicians, so anything shown here is guaranteed to be accepted. - [Record a technician's time off (sick day, leave)](https://docs.crisphive.com/technical-reference/create-technician-time-off): POST /v1/technician-time-off — Records a time-off block for a technician. The record is created PENDING; a business staff member approves it on the dashboard — EXCEPT for the sick-call flow, where `commitAbsenceResolve` approves the pending record that covers the absence itself. `start_datetime`/`end_datetime` are RFC3339 INSTANTS with an offset (e.g. `2030-06-15T04:00:00Z` for midnight Toronto), unlike the business-local naive datetimes used by scheduling calls — a whole day off is that day's local midnight to the next in UTC. Overlapping an existing record for the same technician is refused (`TIME_OFF_OVERLAP`); list `GET /technicians/{id}/time-off` first. Creating a record immediately flags the technician's jobs in that window as needing attention on the dispatch board. Supports Idempotency-Key. - [List technicians](https://docs.crisphive.com/technical-reference/list-technicians): GET /v1/technicians — Returns the field workforce roster: paginated technicians (field workers / engineers) with status, assignment tier (lead, buddy, float), skills and crew relations — the people the dispatch engine schedules onto jobs. Discover the `preferred_technician_id` accepted on customer records here. Supports the `since` cursor for incremental workforce sync. - [Add a technician](https://docs.crisphive.com/technical-reference/create-technician): POST /v1/technicians — Creates a technician membership under the current business. If the phone/email matches an existing user, their account is linked. Otherwise a new user identity is created. Either way the membership starts active. The new member is notified (live mode only, best-effort): an email when `email` is supplied, an SMS when `phone` is supplied, both when both — informational only, no activation step (login stays passwordless: magic link / OTP). Re-adding someone: if the person is currently SUSPENDED on this business (a dashboard action — not reachable via this API), the create REACTIVATES that existing membership (same technician id, their existing group; also notified). A technician REMOVED with deleteTechnician is a closed membership: re-adding the same email/phone links the SAME underlying person (no duplicate identity) but creates a FRESH membership with a NEW id — history stays under the old one. Owner/Administrator groups cannot be assigned via API key, and not at all in sandbox mode. Optional relations (all validated; any missing id → 404 TECHNICIAN_NOT_FOUND with `missing_ids`): `buddy_ids` sets this technician's buddy list (use when creating a lead); `lead_ids` adds this technician as a buddy of each named lead (use when creating a buddy — the buddy-side way to attach the same lead↔buddy relation); `service_area_ids` assigns the technician to those service areas. `start_location_type=office` snapshots the business address + coordinates into the technician at create time; `address`, `start_location_lat`, `start_location_long` in the body are ignored. Requires the business to have coordinates set (else 400 BUSINESS_LOCATION_MISSING). `start_location_type=home` (or empty) uses the address + coordinates from the body. - [Remove a technician from the business](https://docs.crisphive.com/technical-reference/delete-technician): DELETE /v1/technicians/{id} — Closes the technician's membership: the profile is set to deactive and soft-deleted, and their access to this business ends on their next request. In the same operation they are removed from every other technician's buddy list, vehicles they own are released, and any personal-calendar connection they made for this business is revoked. Fires the technician.deleted webhook. Their underlying user identity is untouched, and so are memberships at other businesses. Removal does NOT move their work: jobs still assigned to them keep the assignment and must be re-staffed. Do that BEFORE removing: use listCrewCandidates on each upcoming job, or re-plan the whole day with previewAbsenceResolve / commitAbsenceResolve (which needs a time-off record covering those days; create one with createTechnicianTimeOff). This is a closed membership, not a pause. Re-adding the same email or phone later with createTechnician links the SAME person but opens a FRESH membership with a NEW technician id and none of the old buddy, vehicle or area links. If you expect the person back, suspend them from the dashboard instead; a suspended member is reactivated in place by createTechnician, keeping their id and group. The last active Owner cannot be removed (TECHNICIAN_LAST_OWNER). API keys the person created are NOT revoked (an HR action must not take an integration down); if any are still active, the business Owners and Administrators are emailed a list of them. - [Get a technician](https://docs.crisphive.com/technical-reference/get-technician): GET /v1/technicians/{id} — Returns one technician's full profile: contact info, employment status, assignment tier, skills/qualifications, buddy (crew) relations and assigned vehicles — the dispatch-ready view of a field worker. - [Update a technician](https://docs.crisphive.com/technical-reference/update-technician): PUT /v1/technicians/{id} — Updates mutable technician profile fields. `start_location_type=office` re-snapshots the current business address + coordinates (body `address`/`start_location_lat`/`start_location_long` ignored; requires business coordinates, else 400 BUSINESS_LOCATION_MISSING). `start_location_type=home` (or empty) uses the address + coordinates from the body. - [Replace a technician's buddies](https://docs.crisphive.com/technical-reference/replace-technician-buddies): PUT /v1/technicians/{id}/buddies — Overwrites the technician's buddy list with the provided set of technician IDs. Sending an empty list clears all buddies. Each buddy ID must be an active technician of the same business; a technician cannot be their own buddy. - [Replace a buddy's leaders (buddy side)](https://docs.crisphive.com/technical-reference/replace-technician-leads): PUT /v1/technicians/{id}/leads — Sets the full set of leads this buddy belongs to (many-to-many favorites). Replace-semantics — lead_ids is the complete new list; [] clears every leader. Managed by business staff. - [Replace a technician's service areas](https://docs.crisphive.com/technical-reference/replace-technician-service-areas): PUT /v1/technicians/{id}/service-areas — Overwrites the technician's service-area assignments with the provided set of service area IDs. Sending an empty list clears all. Each service area ID must belong to the same business — any missing id → 404 SERVICE_AREA_NOT_FOUND with `missing_ids` and no writes. The resolved set is returned and also embedded as `service_areas` in the technician GET/list response. Managed by business staff (Booking Coordinator), not tech self-service. - [List time off for a specific technician](https://docs.crisphive.com/technical-reference/list-technician-time-off): GET /v1/technicians/{id}/time-off — Returns the technician's time-off records (pending, approved, rejected, cancelled), paginated, optionally bounded by start_date/end_date (YYYY-MM-DD, inclusive overlap) and status. Read this before recording a new block (overlaps are refused) and to see whether an absence is already recorded before `commitAbsenceResolve`. - [Replace a technician's vehicles](https://docs.crisphive.com/technical-reference/replace-technician-vehicles): PUT /v1/technicians/{id}/vehicles — Overwrites the technician's vehicle list with the provided set of vehicle IDs (the vehicles this technician uses). Sending an empty list clears all. Each vehicle ID must belong to the same business. The list is also embedded as `vehicle_ids` in the technician GET/list response. - [List vehicles](https://docs.crisphive.com/technical-reference/list-vehicles): GET /v1/vehicles — Returns the business's fleet: paginated service vehicles (vans/trucks) with operational status (idle, on job, maintenance) — the fleet inventory behind crew carpooling and job mobilization. Supports the `since` cursor for incremental fleet sync. - [Add a vehicle to the fleet](https://docs.crisphive.com/technical-reference/create-vehicle): POST /v1/vehicles — Registers a van, truck or car in the business's own fleet. Vehicles are what a confirmed job's crew travels in: at confirm, Crisphive auto-selects one vehicle for the whole crew from the lead technician's vehicles, then from unowned fleet vehicles, and blocks a vehicle already booked for an overlapping job. `name` is the only required field, so a bulk fleet import needs nothing else; brand, model, year, plate_number, current_mileage and vehicle_type (van, truck or car) can be filled in later with updateVehicle. Names and plate numbers must be unique in the business (VEHICLE_DUPLICATE_NAME / VEHICLE_DUPLICATE_PLATE). `owner_id` records who has CLAIMED the vehicle as their primary one. The owner must be a lead technician or a management role; a buddy- or float-tier profile is refused with VEHICLE_OWNER_TIER_NOT_ALLOWED, an unknown profile with VEHICLE_INVALID_OWNER. Deciding which vehicles a technician may USE is a separate relation: use replaceTechnicianVehicles for that, not this tool. Send an Idempotency-Key header (the `idempotency_key` argument over MCP) so a retried call replays the original response instead of creating a duplicate vehicle. - [Retire a vehicle from the fleet](https://docs.crisphive.com/technical-reference/delete-vehicle): DELETE /v1/vehicles/{id} — Soft-deletes the vehicle and, in the same transaction, removes it from every technician's vehicle list. It no longer appears in listVehicles or getVehicle and can no longer be auto-selected for a crew. Jobs that referenced it keep the stored reference but no longer display an assigned vehicle, and upcoming jobs are NOT given a replacement automatically. Reach for this only when a vehicle leaves the fleet for good (sold, written off, off-lease). For a van that is merely in the workshop, set its `status` to maintenance with updateVehicle instead, so the record stays in the fleet and can be brought straight back. - [Get a vehicle](https://docs.crisphive.com/technical-reference/get-vehicle): GET /v1/vehicles/{id} — Returns one fleet vehicle (service van/truck): identity, plate, operational status (idle, on job, maintenance) and which technicians use it — the fleet-management view of a single asset. - [Change a vehicle's details](https://docs.crisphive.com/technical-reference/update-vehicle): PUT /v1/vehicles/{id} — Edits an existing fleet record in place; the vehicle id and the technicians who use it are untouched. Partial update: omit a field to KEEP its current value, send "" to CLEAR an optional text field (brand, model, plate_number). Exceptions: `name` rejects "" because a vehicle must stay identifiable, and `vehicle_type` (van, truck, car) and `status` (inactive, idle, on_job, maintenance) must be valid enum values when present; an empty string there is a 400. `owner_id`: omit to keep the current owner, "" to unclaim, or a UUID to reassign; the new owner must be a lead or management profile (VEHICLE_OWNER_TIER_NOT_ALLOWED otherwise). Use this for corrections and odometer updates, and set `status` to maintenance or inactive when a vehicle is temporarily out of service so it stays in the fleet. To change which technicians may use it, call replaceTechnicianVehicles; to take it out of the fleet for good, call deleteVehicle. - [Create a webhook endpoint](https://docs.crisphive.com/technical-reference/create-webhook-endpoint): POST /v1/webhooks — Registers an HTTPS URL that Crisphive will POST events to, and immediately sends a signed verification `ping`. If your receiver answers 2xx the endpoint is created `active`; otherwise it is created `pending_verification` (receives NO events) — fix the receiver, then re-verify it from the Crisphive dashboard, or (on the public API, where verify is not exposed) delete it and create it again. Automation platforms subscribing a REST hook should filter out the `ping` event on their side. The response includes the signing `secret` ONE TIME — store it; you verify the `Crisphive-Signature` header with it and it cannot be retrieved again. `event_types` empty = every event the calling credential may READ: `customer.*` needs customers_view, `technician.*` team_view, `job_request.*` job_view, because a subscription streams that data to your URL. Naming an event the credential cannot read is refused with WEBHOOK_EVENT_NOT_PERMITTED (403, `data.event_types` + `data.required_permissions`). The endpoint is bound to the caller's current environment (live/sandbox). - [List subscribable webhook event types](https://docs.crisphive.com/technical-reference/list-webhook-event-types): GET /v1/webhooks/event-types — Returns the catalog of event types you can subscribe a webhook to (resource.action, e.g. job_request.completed, customer.created). The catalog is extend-only: a published type is never renamed or removed. - [Delete a webhook endpoint](https://docs.crisphive.com/technical-reference/delete-webhook-endpoint): DELETE /v1/webhooks/{id} — Unsubscribes the endpoint: no further events are delivered to it, including retries still pending. This is the "unsubscribe" half of a REST-hook trigger; call it when the automation is turned off. Endpoints are scoped to the credential's environment (live/sandbox), so a sandbox key cannot delete a live endpoint. ## Optional - [OpenAPI spec](https://api.crisphive.com/developers/openapi.json): the machine-readable spec the SDKs and tools are generated from. - [Integration skill](https://api.crisphive.com/developers/SKILL.md): a build-a-client walkthrough for coding agents. - [Crisphive MCP for Claude](https://crisphive.com/claude): The official Crisphive MCP for Claude page — value prop, 3 example prompts, tool table, one-click connect via the Claude connectors directory. - [Crisphive for ChatGPT](https://crisphive.com/chatgpt): The official Crisphive app for ChatGPT — value prop, example prompts, tool table, one-click install from the ChatGPT apps directory.