Skip to main content
Glama

Crisphive MCP

glama score npm

The official MCP (Model Context Protocol) server for the Crisphive API — agentic AI scheduling infrastructure for field operations.

Lets AI agents — Claude, ChatGPT, Gemini, Cursor or any MCP client — match schedules between customers and businesses and route crews to jobs by location, skills, and real-time availability: job booking & appointment scheduling, work-order tracking, availability from a live dispatch & scheduling engine, customer (CRM) sync, service catalogs, technician & crew rosters, geographic service territories and fleet — for trades and home services such as HVAC, plumbing, electrical, cleaning, appliance repair and property maintenance. Scheduling is constraint-based on a deterministic solver: the agent handles the conversation, the solver makes the decision — same inputs, same plan, never an LLM guessing at a calendar. Hosted remote server; nothing to install or run (this repository holds the documentation and registry manifest).

https://api.crisphive.com/mcp

Try these first

Connect (a chsk_test_ sandbox key is enough), then paste any of these straight into your agent:

  1. Job creation — "Schedule a 2-hour HVAC job at 145 Laurier Ave W tomorrow for Marie Tremblay, 613-555-0142." (createCustomer → listJobRequestBookingWindows → createJobRequest → quoteJobRequest → confirmJobRequest)

  2. Emergency insertion — "Emergency plumbing job now at 99 Bank St for David Okafor (613-555-0198) — show me what gets rescheduled." (listEmergencyCandidates → previewEmergencyReschedule → commitEmergencyReschedule)

  3. Daily outline — "Outline my day tomorrow and flag anything at risk." (listJobRequests → getTechnicianSchedule) . Sick call — "Dmitri called in sick for tomorrow — re-staff his jobs without moving any customer's appointment." (previewAbsenceResolve → commitAbsenceResolve)

The same prompts appear on every Crisphive listing and docs page, so what you see here is exactly the first-run experience everywhere.

→ Full overview, tool table and one-click connect: https://crisphive.com/claude

Related MCP server: Servicialo

Requirements

Any MCP client that supports remote servers over Streamable HTTP — claude.ai, Claude Desktop, Claude Code, ChatGPT, Gemini CLI, Cursor, VS Code, Windsurf, Cline, Zed, LM Studio, ….

Installation

claude.ai / Claude Desktop (OAuth — no key needed)

Settings → Connectors → Add custom connector, paste https://api.crisphive.com/mcp. Sign in as the Crisphive business owner when the consent screen opens. (Custom connectors require a Claude plan that supports them.)

Claude Code

# OAuth (you'll be prompted to authorize in the browser)
claude mcp add --transport http crisphive https://api.crisphive.com/mcp

# or with an API key (sandbox key shown — safe to experiment)
claude mcp add --transport http crisphive https://api.crisphive.com/mcp \
  --header "Authorization: Bearer chsk_test_YOUR_KEY"

Cursor

Add to Cursor

Or add to .cursor/mcp.json:

{
  "mcpServers": {
    "crisphive": { "url": "https://api.crisphive.com/mcp" }
  }
}

VS Code

code --add-mcp '{"name":"crisphive","url":"https://api.crisphive.com/mcp"}'

ChatGPT

Settings → Connectors (developer mode) → add MCP server with URL https://api.crisphive.com/mcp (OAuth).

Gemini CLI

Add to ~/.gemini/settings.json (note: Gemini CLI uses httpUrl for Streamable HTTP servers):

{
  "mcpServers": {
    "crisphive": {
      "httpUrl": "https://api.crisphive.com/mcp",
      "headers": { "Authorization": "Bearer chsk_test_YOUR_KEY" }
    }
  }
}

Other MCP clients (Windsurf, Cline, Zed, LM Studio, …)

Most clients accept the standard remote-server shape:

{
  "mcpServers": {
    "crisphive": {
      "url": "https://api.crisphive.com/mcp",
      "headers": { "Authorization": "Bearer chsk_test_YOUR_KEY" }
    }
  }
}

Only the URL field name varies in a few clients:

Client

Config file

URL field

Cline / Roo Code

cline_mcp_settings.json

url

Windsurf

~/.codeium/windsurf/mcp_config.json

serverUrl

Gemini CLI

~/.gemini/settings.json

httpUrl

Zed

settings.json → context_servers

url

Clients that only speak stdio can bridge with mcp-remote:

{
  "mcpServers": {
    "crisphive": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://api.crisphive.com/mcp"]
    }
  }
}

Local server (npm — @crisphive/mcp)

This repository also ships a thin local stdio server: the same 67 tools (same names, same schemas — generated from the same /v1 OpenAPI spec as the hosted endpoint), where each call is an HTTPS request to the Crisphive API with your key. No business logic runs locally.

{
  "mcpServers": {
    "crisphive": {
      "command": "npx",
      "args": ["-y", "@crisphive/mcp"],
      "env": { "CRISPHIVE_API_KEY": "chsk_test_YOUR_KEY" }
    }
  }
}

Environment variables:

Variable

Required

Meaning

CRISPHIVE_API_KEY

for tool calls

chsk_live_… = production data, chsk_test_… = isolated sandbox. Create keys in the dashboard (Developers → API keys).

CRISPHIVE_BASE_URL

no

API origin override (default https://api.crisphive.com).

Prefer the hosted remote server (https://api.crisphive.com/mcp) when your client supports it — OAuth, no key handling, always current. The local package exists for stdio-only clients and self-hosted setups.

Developing in this repo: npm ci && npm test. The tool registry (src/tools.generated.json) is generated — npm run generate refreshes it from the live spec; CI fails if it drifts from /v1.

Authentication

Every request is authenticated with a secret API key sent as a bearer token. Create keys from your Crisphive business dashboard. The key prefix selects the data environment:

  • chsk_live_… → live (production) data

  • chsk_test_… → sandbox (isolated test) data

Load keys from the environment — never commit them.

Keys expire. The lifetime is chosen when the key is created — 30 days by default, up to 365 — and is fixed for that key's life; it cannot be extended later. To renew, create a second key, point your agent at it, then revoke the first: a business can hold several active keys at once, so the changeover has no downtime and needs no special endpoint (the same procedure AWS documents for access keys). Read expires_at from the dashboard or the key API and schedule the swap. An aged-out key fails with API_KEY_EXPIRED, distinct from API_KEY_INVALID, so you can alert on a missed renewal separately from a revocation.

Crisphive emails the business's owners 7 days before a key expires (14 days for an OAuth connection), so an expiry should not be a surprise — but the mail goes to the business, not necessarily to you, so track expires_at yourself. A key deliberately created for less than 7 days gets no advance notice; it would have arrived at creation.

The MCP endpoint additionally supports OAuth 2.1 for end-user connectors (claude.ai, ChatGPT, …): the business owner authorizes your agent on a consent screen and no key is ever handled. A compliant MCP client runs the whole flow automatically — discovery, dynamic client registration, authorization code + PKCE. Full flow, scopes and token lifetimes: docs/integration.md.

Tools

67 tools, one per operation of the public /v1 API — same names as the SDK methods (listCustomers, createJobRequest, …), derived from the same OpenAPI spec so REST and MCP never drift. Full reference: docs/tools.md.

Group

Tools

Customers (CRM sync, full CRUD)

listCustomers · createCustomer · getCustomer · updateCustomer · deleteCustomer

Bookings (create & track)

createJobRequest · listJobRequests · getJobRequest · getJobRequestTimeline · listJobRequestBookingWindows · listJobRequestChanges

Catalog (reads)

listJobTypes · getJobType · listSkills · listSkillCategories · listSkillsByCategory · listServiceAreas · getServiceArea

Catalog management (import sync)

createJobType · updateJobType · deleteJobType · createSkillCategory · deleteSkillCategory · createSkill · updateSkill · deleteSkill · createServiceArea · updateServiceArea · deleteServiceArea · createVehicle · updateVehicle · deleteVehicle

Team & fleet (reads)

listTechnicians · getTechnician · listVehicles · getVehicle

Team roster management (HR-system sync)

listAssignableGroups · createTechnician · updateTechnician · deleteTechnician · replaceTechnicianBuddies · replaceTechnicianLeads · replaceTechnicianVehicles · replaceTechnicianServiceAreas · replaceTechnicianSkills · listTechnicianSkills

Matching & scheduling (read-only, engine-computed)

listMatchingSlots · listCrewCandidates · getTechnicianSchedule · listNearbyTechnicians

Scheduling actions (drive the schedule)

quoteJobRequest · confirmJobRequest · bookAndConfirmJobRequest · previewJobRequestMove · commitJobRequestMove

Priority & emergency dispatch (P0–P3, SLA, cascade)

updateJobPriority · listEmergencyCandidates · previewEmergencyReschedule · commitEmergencyReschedule

Absence resolve (sick call — re-staff a technician's whole day)

previewAbsenceResolve · commitAbsenceResolve

Technician time off (record a sick day / leave, read existing leave, delete it when they come back)

createTechnicianTimeOff · listTechnicianTimeOff · deleteTechnicianTimeOff

Webhooks (REST-hook triggers: subscribe / unsubscribe)

listWebhookEventTypes · createWebhookEndpoint · deleteWebhookEndpoint

Typical agent flow:

listSkills / listJobTypes                → discover reference IDs
createCustomer                           → { customer_id }
listJobRequestBookingWindows             → offer only the returned windows
createJobRequest                         → booking created
quoteJobRequest → confirmJobRequest      → scheduled (auto or forced technician)
   (quote answers 409 JOB_REQUEST_QUOTE_NOT_SCHEDULABLE when the customer
    would see no slot — agree a new time, or resend with force: true)
getJobRequest / listJobRequestChanges    → track status

One-call flow for a phone call or an automation (voice agents: connect to https://api.crisphive.com/mcp?profile=voice for a small tool set):

listCustomers (phone: "+16135550188")    → is the caller already a customer?
bookAndConfirmJobRequest                 → booked + scheduled, or confirmed:false + refusal
                                           (the job then waits in the coordinator's queue)

Emergency (P0) flow:

createJobRequest (priority: "p0") → quoteJobRequest
listEmergencyCandidates                  → ranked techs + crew_recommendation
previewEmergencyReschedule               → what moves (or reassigns)
commitEmergencyReschedule                → inserted + auto-confirmed
   (or preview with an inline `job` probe, then bookAndConfirmJobRequest
    with priority p0 + emergency{dispatch: "now"} books and commits in one call)

Sick-call flow (a technician is out — re-staff every job on their board at its UNCHANGED time):

previewAbsenceResolve                    → who takes each job; nothing written
createTechnicianTimeOff                  → record the sick day (lands pending — enough for the commit)
commitAbsenceResolve                     → apply exactly the previewed plan, all-or-nothing

Pagination

List tools accept page / limit and return a meta object (total, count, per_page, current_page, total_pages).

Idempotency

Create/commit tools (createCustomer, createTechnician, createJobRequest, confirmJobRequest, commitJobRequestMove, commitEmergencyReschedule, commitAbsenceResolve) accept an idempotency_key argument so retries never create a duplicate — pass the same value when retrying.

Errors

Every tool returns the Crisphive response envelope (as text and as structuredContent): error_code is 0 on success, a stable string on failure (CUSTOMER_NOT_FOUND, API_KEY_INVALID, …). Match codes, never message strings.

Documentation

Privacy & support

  • Privacy policy: https://crisphive.com/privacy-policy — Crisphive processes the business data reachable through the API (customers, bookings, technicians, fleet) solely to operate the Service; it does not sell personal information. Data is retained while the account is active and shared only with service providers/sub-processors as necessary. An agent connected over MCP acts on behalf of the authorizing business and is scoped to that business's data, environment (live vs sandbox) and granted permissions.

  • Support: support@crisphive.com

License

MIT

Available Tools

53 tools
commitEmergencyRescheduleCommit emergency insert + cascade rescheduleAInspect

Commit emergency insert + cascade reschedule

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. 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): call /preview again, show the fresh plan, then commit. EMERGENCY_RESCHEDULE_SLOT_OCCUPIED — landing window blocked by an immovable anchor (P0/crew/multi-day): another tech or time. Other codes — same remedies as /candidates.

NameRequiredDescriptionDefault
modeYesCascade mode: overtime | next_day (must match the preview).
start_atYesDesired start — business-local naive datetime, no offset. Must be in the future and match the preview.
technician_idYesTarget technician (must belong to the business).
idempotency_keyNoUnique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header)
emergency_job_idYesID of the P0 job to insert.
displacement_modeNoFate of displaced jobs: reschedule (default) or reassign — must match the preview.
expected_move_idsNoExpectedMoveIDs — the displaced job IDs the preview returned (echo BOTH data.days[].moves[].job_id AND data.reassignments[].job_id). RECOMMENDED: if the recomputed cascade would touch a different set (a job booked onto the lane since the preview), the commit is rejected with EMERGENCY_RESCHEDULE_PLAN_DRIFTED instead of displacing a job the coordinator never saw. Omit to opt out.
emergency_expected_versionNoOptimistic-lock fence from the previewed job (0 = fence on the server-read version).
commitJobRequestMoveCommit a schedule-board job moveAInspect

Commit a schedule-board job move

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 no longer match): re-preview, show the fresh plan, commit again. All other codes — same remedies as /move/preview.

NameRequiredDescriptionDefault
idYesJob request ID (UUID or short_code)
modeYesCascade mode: overtime | next_day (must match the preview).
start_atYesNew start — business-local naive datetime, no offset. Must match the preview and be in the future.
technician_idYesTarget technician (must match the preview).
idempotency_keyNoUnique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header)
expected_versionNoExpectedVersion — the moved job's status_version from the preview (0 = fence on the fresh plan-read value; negatives rejected).
expected_move_idsNoExpectedMoveIDs — the displaced job IDs the preview returned (echo data.days[].moves[].job_id). Optional but RECOMMENDED: if the schedule changed so the commit would push a different set (e.g. a job booked onto the target tech since the preview), the commit is rejected with SCHEDULE_MOVE_PLAN_DRIFTED instead of silently pushing an unseen job.
expected_member_idsNoExpectedMemberIDs — crew moves only: the FULL member set the preview staffed (echo data.members[].technician_id). If the commit's re-plan would staff a DIFFERENT set (a previewed replacement got booked in the meantime), the commit is rejected with SCHEDULE_MOVE_PLAN_DRIFTED — crew swaps are never approved unseen. Omit to opt out.
confirmJobRequestConfirm a booking on behalf of the customerAInspect

Confirm a booking on behalf of the customer

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). 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: cannot_arrive_in_time (commute/shift-start — data.earliest_feasible_at (RFC3339 UTC) is the first same-day time they CAN be on site → offer it) | missing_required_skills | not_available_today | not_lead_tier. 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.

NameRequiredDescriptionDefault
idYesJob request ID
scheduled_atNoChosen start time — business-local naive datetime, no offset (the business_time.datetime value from the time-segments picker). The server converts to UTC using the job's business timezone.
technician_idNoTechnicianID (BUSINESS confirm only — ignored on the customer surface): force-assign the job to this technician instead of the ranked auto-pick. Ranking is bypassed; feasibility (hours/time-off/geo/skills), the TierLead rule and the double-booking guard still apply — an infeasible forced tech rejects the confirm (P0 gets the displacement hint).
status_versionNoOptimistic-lock fence: the status_version from your last read. Omitted/0 = fence on the row's current version (no race protection).
idempotency_keyNoUnique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header)
arrival_window_minutesNoArrivalWindowMinutes = width (phút) của arrival-window ô khách bấm ở slot-picker (chính là time_slot_step_minutes, mặc định 30). Persist để post-confirm detail render lại đúng window. Optional; bounds ([5, 240], khớp slot-picker step) validate ở usecase — single authority, một error code (JOB_REQUEST_INVALID_INPUT).
createCustomerCreate a customerAInspect

Create a customer

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.

NameRequiredDescriptionDefault
uidNoYour external reference for this customer (your own system's ID). Optional; max 32 chars.
tierNoLoyalty tier. Defaults to "regular" if omitted.
emailNoEmail address. Optional, but at least one of phone/email is required.
phoneNoPhone number in E.164 international format: a leading `+` and the country code, e.g. `+16135550188`. A bare national number (`6135550188`) is REJECTED with PHONE_INVALID — there is no default region to guess the country from. Separators (spaces, dashes, parentheses) are stripped before validation. Optional, but at least one of phone/email is required; 10–20 chars.
addressNoPostal address and coordinates.
full_nameYesCustomer's full name. Required; max 255 chars.
sms_opt_inNoSMS consent: set true ONLY when the customer explicitly agreed to receive SMS (e.g. ticked a non-pre-checked consent box, or gave verbal/written consent you keep a record of). SMS notifications are suppressed while false.
idempotency_keyNoUnique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header)
service_area_idNoUUID of the service area for this customer. Must belong to this business.
preferred_technician_idNoUUID of the technician this customer prefers. Must belong to this business.
createJobRequestCreate a job requestAInspect

Create a job request

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.

NameRequiredDescriptionDefault
priorityNoScheduling priority. Optional; omitted bookings receive the business's default_priority setting (assignment settings, default p2). p0=emergency (interrupt-driven insert), p1=top (displaced only by p0), p2=standard, p3=deferrable (first candidate for displacement).
job_datesYesRequested date(s) + period(s) the customer wants the job. At least one, up to 12.
skill_idsNoUUIDs of the skills the customer desires for this job. Optional; up to 20.
x_timezoneNoCustomer IANA timezone (sent as the X-Timezone header)
customer_idYesUUID of an existing customer of this business to book the job for. Required.
descriptionNoFree-text description of the work requested. Optional; max 2000 chars.
job_type_idNoUUID of the job type to classify this job. Optional; null leaves the job unclassified.
sla_deadlineNoSLA deadline (business-local naive datetime, e.g. "2030-06-14T17:00:00"). Optional; ONLY valid together with priority=p1 — arms the auto-escalation clock (the job escalates to p0 as breach risk crosses the business's safety buffer). Must be in the future.
idempotency_keyNoUnique key making retries safe: a repeat send with the same key returns the original booking instead of creating a duplicate (sent as the Idempotency-Key header)
createJobTypeCreate a job typeAInspect

Create a job type

Adds a new job type to the business catalog

NameRequiredDescriptionDefault
nameYesJob-type name (required) — what the booking form shows as the kind of work. Stored as the canonical English value; per-locale names are a dashboard authoring feature.
statusNoLifecycle status. Defaults to active; inactive types cannot be selected for new job requests.
idempotency_keyNoUnique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header)
createServiceAreaCreate a new service areaAInspect

Create a new service area

Creates a service area with optional polygon boundary for the business

NameRequiredDescriptionDefault
cityNoCity name
nameYesArea name (required)
countyNoCounty name
countryNoCountry name
boundaryNoGeoJSON polygon boundary (optional)
districtNoDistrict name
descriptionNoOptional description
postal_codeNoPostal code
state_provinceNoState / province name
idempotency_keyNoUnique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header)
createSkillCreate a skillAInspect

Create a skill

Creates a skill under the given category. New skills are active by default. Skill names must be unique within their category.

NameRequiredDescriptionDefault
idYesSkill category ID (UUID)
nameYesSkill name (required). The matching engine hard-filters technicians on skills when a booking carries skill_ids.
descriptionNoWhat the skill covers. Optional.
idempotency_keyNoUnique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header)
createSkillCategoryCreate a skill categoryAInspect

Create a skill category

Creates a skill category for the current business. Categories group skills (e.g. "Plumbing", "Electrical"). Names must be unique within a business.

NameRequiredDescriptionDefault
iconNoIcon slug for the dashboard's catalog tree. Optional.
nameYesCategory name (required), e.g. a trade: Plumbing, Electrical, HVAC.
idempotency_keyNoUnique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header)
createTechnicianAdd a technicianAInspect

Add a technician

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.

NameRequiredDescriptionDefault
emailNoEmail address. At least one of phone/email is required (identity resolution key).
phoneNoPhone number in E.164 international format: a leading `+` and the country code, e.g. `+16135550188`. A bare national number (`6135550188`) is REJECTED with PHONE_INVALID — there is no default region to guess the country from. At least one of phone/email is required (identity resolution key).
addressNoHome address (the day-start point when start_location_type=home).
lead_idsNoLeads this technician is a buddy of (set when creating a buddy; the technician is appended to each lead's buddy list). Optional; max 50 technician ids.
buddy_idsNoBuddies of this technician (set when creating a lead). Optional; max 50 technician ids.
full_nameYesThe person's full display name. Required; max 255 chars.
job_titleNoDisplay title (e.g. "Senior HVAC Technician").
join_dateNoFirst working day (YYYY-MM-DD).
assignment_tierNoCrew tier the matching engine assigns by: lead (can head a job), buddy (crew helper), float (excluded from auto crew-assign).
idempotency_keyNoUnique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header)
service_area_idsNoService areas to assign the technician to. Optional; max 50. Discover via GET /service-areas.
business_group_idYesRole group for the new member. Discover IDs via GET /permission/groups (Owner/Administrator groups are rejected in sandbox mode).
start_location_latNoExplicit day-start latitude in decimal degrees (-90..90); when set it wins over the address geocode.
start_location_longNoExplicit day-start longitude in decimal degrees (-180..180); when set it wins over the address geocode.
start_location_typeNoWhere the technician starts their day: home (their address) or office (the business location). Drives the engine's travel estimates.
createVehicleCreate a new vehicleCInspect

Create a new vehicle

Creates a vehicle for the business fleet

NameRequiredDescriptionDefault
nameYesVehicle display name (required). The only mandatory field — a fleet import needs nothing but names to get vehicles into the system.
yearNoModel year. Optional.
brandNoManufacturer / make. Optional.
modelNoModel name. Optional.
owner_idNoUUID of the technician who owns (has claimed) this vehicle. Optional. 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. Assigning which vehicles a technician USES is a different relation: PUT /technicians/{id}/vehicles.
plate_numberNoLicense plate. Optional.
vehicle_typeNo
current_mileageNoOdometer reading. Optional; must be >= 0 when sent.
idempotency_keyNoUnique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE. (sent as the Idempotency-Key header)
deleteCustomerDelete a customerA
DestructiveIdempotent
Inspect

Delete a customer

Soft-deletes a customer record, removing it from the active customer directory; existing bookings keep their customer snapshot.

NameRequiredDescriptionDefault
idYesCustomer ID
deleteJobTypeDelete a job typeA
DestructiveIdempotent
Inspect

Delete a job type

Soft-deletes a job type. System rows cannot be deleted.

NameRequiredDescriptionDefault
idYesJob Type ID
deleteServiceAreaDelete a service areaB
DestructiveIdempotent
Inspect

Delete a service area

Soft deletes a service area

NameRequiredDescriptionDefault
idYesService Area ID
deleteSkillDelete a skillA
DestructiveIdempotent
Inspect

Delete a skill

Permanently deletes a skill. Returns SKILL_HAS_MEMBERS (409) if any active technicians are still assigned — unassign all technicians first.

NameRequiredDescriptionDefault
idYesSkill ID (UUID)
deleteSkillCategoryDelete a skill categoryA
DestructiveIdempotent
Inspect

Delete a skill category

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.

NameRequiredDescriptionDefault
idYesSkill category ID (UUID)
deleteTechnicianRemove a technicianB
DestructiveIdempotent
Inspect

Remove a technician

Soft-removes a technician from the business by setting status to deactive

NameRequiredDescriptionDefault
idYesTechnician ID
deleteVehicleDelete a vehicleA
DestructiveIdempotent
Inspect

Delete a vehicle

Soft deletes a vehicle

NameRequiredDescriptionDefault
idYesVehicle ID
getCustomerGet a customerA
Read-only
Inspect

Get a customer

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.

NameRequiredDescriptionDefault
idYesCustomer ID (UUID)
getJobRequestGet a job requestA
Read-only
Inspect

Get a job request

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.

NameRequiredDescriptionDefault
idYesJob request ID (UUID or short_code)
getJobRequestTimelineJob timelineA
Read-only
Inspect

Job 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.

NameRequiredDescriptionDefault
idYesJob request ID (UUID or short_code)
getJobTypeGet a job typeA
Read-only
Inspect

Get a job type

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.

NameRequiredDescriptionDefault
idYesJob Type ID
getServiceAreaGet a service areaA
Read-only
Inspect

Get a service area

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.

NameRequiredDescriptionDefault
idYesService Area ID
getTechnicianGet a technicianA
Read-only
Inspect

Get a technician

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.

NameRequiredDescriptionDefault
idYesTechnician ID
getTechnicianScheduleOne technician's real schedule (sessions + time off)A
Read-only
Inspect

One technician's real schedule (sessions + time off)

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.

NameRequiredDescriptionDefault
idYesTechnician ID
toNoEnd date (YYYY-MM-DD, inclusive; default from+7d; max range 31 days)
fromNoStart date (YYYY-MM-DD, business-local; default today)
getVehicleGet a vehicleA
Read-only
Inspect

Get a vehicle

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.

NameRequiredDescriptionDefault
idYesVehicle ID
listCrewCandidatesMatching crew candidates for a jobA
Read-only
Inspect

Matching crew candidates for a job

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.

NameRequiredDescriptionDefault
idYesJob request ID or short_code
force_lead_idNoCheck a specific technician as lead — returns only that lead if feasible, else 409
include_buddiesNoAlso return buddy candidate pools
include_vehicleNoAlso return the available-vehicle list
listCustomersList customersA
Read-only
Inspect

List 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.

NameRequiredDescriptionDefault
qNoSearch name, UID, phone, email
pageNoPage number (default 1)
sortNoSort: created_at_desc|created_at_asc|name_asc|name_desc|uid_asc|uid_desc (default: created_at_desc)
tierNoFilter by tier: regular|vip (repeatable)
limitNoPage size (default 15, max 1000)
sinceNoRFC3339 cursor for short-polling; echo back next_since from prior response
statusNoFilter by status: active|inactive
preferred_technician_idNoFilter by preferred technician UUID
listEmergencyCandidatesRank technicians for a P0 emergency insertAInspect

Rank technicians for a P0 emergency insert

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). Feed the chosen technician_id into emergency/preview + emergency/commit. 409 NEXT STEPS: EMERGENCY_RESCHEDULE_NOT_ELIGIBLE — the job cannot be emergency-inserted (not P0, already started/completed/archived, or not quoted): fix the job state or use a normal confirm. EMERGENCY_RESCHEDULE_CREW_UNSUPPORTED — crew jobs cannot use the emergency flow (v1): staff via confirm/reassign instead. EMERGENCY_RESCHEDULE_MULTIDAY_UNSUPPORTED — a confirmed multi-day job cannot be re-inserted (v1): use the normal reassign flow. EMERGENCY_RESCHEDULE_NO_WORKING_DAY — the chosen date has no working hours: pick a working day. EMERGENCY_RESCHEDULE_IN_PAST — start time already passed: pick a future time.

NameRequiredDescriptionDefault
modeYesCascade mode each candidate's displacement preview assumes: overtime = displaced jobs stay same-day; next_day = overflow rolls to the next working day.
limitNoMax candidates to return (default 5, max 10).
start_atYesDesired start — business-local naive datetime, no offset. Must be in the future.
idempotency_keyNoOptional idempotency key (forwarded as the Idempotency-Key header). Reuse the same value when retrying so the operation runs at most once.
emergency_job_idYesID of the P0 job to place.
listJobRequestBookingWindowsBooking availabilityA
Read-only
Inspect

Booking availability

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.

NameRequiredDescriptionDefault
toNoEnd YYYY-MM-DD
fromNoStart YYYY-MM-DD
x_timezoneYesCustomer IANA timezone (sent as the X-Timezone header)
listJobRequestChangesPoll for new & changed job requests (sync feed)A
Read-only
Inspect

Poll for new & changed job requests (sync feed)

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.

NameRequiredDescriptionDefault
limitNoMax changes per poll (default 15, max 1000). If the page fills, has_more=true.
sinceNoRFC3339 cursor from the prior response's next_since. OMIT on the first poll to prime the cursor at server-now.
priorityNoPriority filter (p0|p1|p2|p3)
customer_idNoOnly changes to this customer's jobs (UUID)
status_keysNoComma-separated status slugs — only surface changes to jobs in these statuses
scheduled_toNoFilter to (YYYY-MM-DD = end of that day in the business timezone, or RFC3339), exclusive
technician_idNoOnly changes to jobs assigned to this technician (UUID)
scheduled_fromNoFilter from (YYYY-MM-DD = start of that day in the business timezone, or RFC3339); range is [from, to)
listJobRequestsList job requestsA
Read-only
Inspect

List 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.

NameRequiredDescriptionDefault
qNoSearch short_code or description (case-insensitive, partial match)
pageNoPage number
sortNoSort key: created_at:desc (default) | created_at:asc | scheduled_at:asc | scheduled_at:desc | priority:asc (P0 first) | priority:desc
limitNoPage size
statusNoactive (default) | archived | all
priorityNoPriority filter (p0|p1|p2|p3)
customer_idNoCustomer UUID
status_keysNoComma-separated status slugs
completed_toNoFilter by completion time to (YYYY-MM-DD = end of that day in the business timezone, or RFC3339), exclusive
scheduled_toNoFilter to (YYYY-MM-DD = end of that day in the business timezone, or RFC3339), exclusive
technician_idNoTechnician UUID
completed_fromNoFilter by completion time from (YYYY-MM-DD = start of that day in the business timezone, or RFC3339); range is [from, to). Counts jobs by when they were completed regardless of reschedules.
scheduled_fromNoFilter from (YYYY-MM-DD = start of that day in the business timezone, or RFC3339); range is [from, to)
service_area_idNoService-area UUID (board zone filter)
listJobTypesList job typesA
Read-only
Inspect

List 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.

NameRequiredDescriptionDefault
statusNoFilter by status (active|inactive)
listMatchingSlotsMatching time slots for a quoted jobA
Read-only
Inspect

Matching time slots for a quoted job

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).

NameRequiredDescriptionDefault
idYesJob request ID (UUID or short_code)
step_minutesNoSlot step in minutes (default: business arrival window, 5–240)
listNearbyTechniciansFind nearby feasible technicians (job-less location query)A
Read-only
Inspect

Find nearby feasible technicians (job-less location query)

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.

NameRequiredDescriptionDefault
atNoVisit start (RFC3339, e.g. 2026-07-20T14:00:00Z; default now)
latYesLatitude of the service location
lngYesLongitude of the service location
limitNoMax candidates (default 10, max 20)
skill_idsNoComma-separated skill UUIDs to require/match
duration_minutesNoVisit length in minutes (default 60; 15–480)
listServiceAreasList service areasA
Read-only
Inspect

List 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.

NameRequiredDescriptionDefault
pageNoPage number (default 1)
limitNoItems per page (default 15, max 1000)
listSkillCategoriesList skill categoriesA
Read-only
Inspect

List skill categories

Returns paginated skill categories — how the business groups technician qualifications by trade or specialty (e.g. HVAC, plumbing, electrical) — ordered alphabetically.

NameRequiredDescriptionDefault
pageNoPage number (default: 1)
limitNoPage size (default: 15, max: 1000)
listSkillsList all skillsA
Read-only
Inspect

List all 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.)

NameRequiredDescriptionDefault

No parameters

listSkillsByCategoryList skills in a categoryA
Read-only
Inspect

List skills in a category

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.

NameRequiredDescriptionDefault
idYesSkill category ID (UUID)
pageNoPage number (default: 1)
limitNoPage size (default: 15, max: 1000)
listTechniciansList techniciansA
Read-only
Inspect

List 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.

NameRequiredDescriptionDefault
pageNoPage number (default 1)
sortNoSort: created_at_desc|created_at_asc|name_asc|name_desc (default: created_at_desc)
limitNoItems per page (default 15, max 1000)
sinceNoRFC3339 cursor for short-polling; echo back next_since from prior response
statusNoFilter by status (active, onboarding, deactive)
keywordNoSearch by full name, email, phone, or address
assignment_tierNoFilter by assignment tier (lead, buddy, float)
listTechnicianSkillsList skills for a technicianA
Read-only
Inspect

List skills for a technician

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.

NameRequiredDescriptionDefault
idYesTechnician ID — business_user_profiles.id (UUID)
pageNoPage number (default: 1)
limitNoPage size (default: 15, max: 1000)
eligible_onlyNotrue (default) = active skills only; false = all assigned skills including inactive
listVehiclesList vehiclesA
Read-only
Inspect

List 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.

NameRequiredDescriptionDefault
pageNoPage number (default 1)
limitNoItems per page (default 15, max 1000)
sinceNoRFC3339 cursor for short-polling; echo back next_since from prior response
statusNoFilter by status (inactive, idle, on_job, maintenance)
keywordNoSearch by name, brand, model, or plate number
previewEmergencyReschedulePreview emergency insert + cascade rescheduleBInspect

Preview emergency insert + cascade reschedule

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). 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): 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 / NO_WORKING_DAY / IN_PAST — same remedies as /candidates.

NameRequiredDescriptionDefault
modeYesCascade mode: overtime = displaced jobs stay same-day (tech works late); next_day = overflow rolls to the next working day.
start_atYesDesired start — business-local naive datetime, no offset. Must be in the future.
technician_idYesTarget technician (must belong to the business).
idempotency_keyNoOptional idempotency key (forwarded as the Idempotency-Key header). Reuse the same value when retrying so the operation runs at most once.
emergency_job_idYesID of the P0 job to insert.
displacement_modeNoFate of displaced jobs: reschedule (default — pushed to later windows) or reassign (handed to another feasible technician at their ORIGINAL time; no-capacity jobs fall back to reschedule).
previewJobRequestMovePreview a schedule-board job moveAInspect

Preview a schedule-board job move

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. See SCHEDULE_BOARD_DESIGN.md. Warning detail: a TECH_NOT_FEASIBLE warning carries reason = 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) | missing_required_skills | not_available_today (no working hours, approved time off, or outside the service area) | not_lead_tier. For a P0 move this warning is advisory (coordinator may commit anyway); for p1/p2/p3 the same condition is the hard 409 SCHEDULE_MOVE_TECH_INFEASIBLE. 409 NEXT STEPS: SCHEDULE_MOVE_NOT_ELIGIBLE (job unconfirmed/unquoted/archived/completed — not movable) · SCHEDULE_MOVE_IN_PROGRESS (tech already executing — do not move) · SCHEDULE_MOVE_IN_PAST (pick a future time) · SCHEDULE_MOVE_SLOT_OCCUPIED (landing window blocked by an immovable anchor — another tech/time) · SCHEDULE_MOVE_TECH_INFEASIBLE (non-P0 hard block: target tech not qualified/available — its data carries technician_id, reason (same catalog as the TECH_NOT_FEASIBLE warning) 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) · SCHEDULE_MOVE_NO_WORKING_DAY (pick a working day) · SCHEDULE_MOVE_REQUIRES_FREE_SLOT (non-P0 moves may not displace — free capacity only, unless the owner enables allow_non_p0_displacement) · 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.

NameRequiredDescriptionDefault
idYesJob request ID (UUID or short_code)
modeYesCascade mode for displaced jobs: overtime = stay same-day (tech works late); next_day = overflow rolls to the next working day.
start_atYesNew start — business-local naive datetime, no offset. Must be in the future.
technician_idYesTarget technician — may equal the current tech (pure time move) or differ (manual reassign).
idempotency_keyNoOptional idempotency key (forwarded as the Idempotency-Key header). Reuse the same value when retrying so the operation runs at most once.
quoteJobRequestFire quote (FIXED action — business)AInspect

Fire quote (FIXED action — business)

Sends the quote: sets quoted_at + duration cols, advances pending_action to confirm_booking. Status stays booking.

NameRequiredDescriptionDefault
idYesJob request ID
crewNoCrew — multi-person plan (tech lead + buddies). Omit / empty = single person (lead 100%). When present must have exactly one is_lead and wrench_percent summing to 100. See MULTIPERSON_CREW_DESIGN.md.
status_versionNoOptimistic-lock fence: the status_version from your last read. Omitted/0 = fence on the row's current version (no race protection).
idempotency_keyNoOptional idempotency key (forwarded as the Idempotency-Key header). Reuse the same value when retrying so the operation runs at most once.
job_duration_minutesYesHands-on work duration in minutes (man-minutes for a crew job). Required, min 1.
mobilization_minutesNoMobilization (setup/travel-prep) minutes added before the work. Optional, min 0.
demobilization_minutesNoDemobilization (teardown) minutes added after the work. Optional, min 0.
replaceTechnicianBuddiesReplace a technician's buddiesA
Idempotent
Inspect

Replace a technician's 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.

NameRequiredDescriptionDefault
idYesTechnician ID
buddy_idsNoTechnician ids to set as this lead's buddies (max 50; self-buddy rejected).
replaceTechnicianLeadsReplace a buddy's leaders (buddy side)A
Idempotent
Inspect

Replace a buddy's leaders (buddy side)

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.

NameRequiredDescriptionDefault
idYesBuddy technician ID
lead_idsNoLead technician ids this buddy assists (max 50).
replaceTechnicianServiceAreasReplace a technician's service areasA
Idempotent
Inspect

Replace a technician's 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.

NameRequiredDescriptionDefault
idYesTechnician ID
service_area_idsNoService-area ids (max 50). Discover via GET /service-areas.
replaceTechnicianSkillsReplace a technician's skillsAInspect

Replace a technician's 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", ...]}.

NameRequiredDescriptionDefault
idYesTechnician ID — business_user_profiles.id (UUID)
skill_idsNoSkill ids (max 100). Discover via GET /skills.
replaceTechnicianVehiclesReplace a technician's vehiclesA
Idempotent
Inspect

Replace a technician's 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.

NameRequiredDescriptionDefault
idYesTechnician ID
vehicle_idsNoVehicle ids (max 50). Discover via GET /vehicles.
updateCustomerUpdate a customerA
Idempotent
Inspect

Update a customer

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.

NameRequiredDescriptionDefault
idYesCustomer ID (UUID)
uidNoYour external reference for this customer. Omit to leave unchanged, "" to clear. Max 32 chars.
tierNoLoyalty tier. Omit to leave unchanged; "" is ignored (an enum has no empty member).
emailNoEmail address. Omit to leave unchanged, "" to clear.
notesNoFree-form internal notes about the customer. Omit to leave unchanged, "" to clear; max 4000 chars.
phoneNoPhone number in E.164 international format (`+16135550188`); a bare national number is rejected with PHONE_INVALID — see createCustomerReq.Phone. Omit to leave unchanged, "" to clear. 10–20 chars.
statusNoLifecycle status. Omit to leave unchanged; "" is ignored.
addressNoPostal address and coordinates. Omit the whole object to leave the stored address untouched; when present it REPLACES the address block.
full_nameNoCustomer's full name. Omit to leave unchanged; an empty or blank value is ignored (a customer cannot be left nameless). Max 255 chars.
sms_opt_inNoSMS consent, tri-state: omit/null = leave unchanged; true = grant (only when the customer explicitly consented — the original consent timestamp is preserved); false = revoke (opt-out, stops SMS immediately).
service_area_idNoUUID of the service area for this customer. Omit to leave unchanged, "" to clear. Must belong to this business.
preferred_technician_idNoUUID of the technician this customer prefers. Omit to leave unchanged, "" to clear. Must belong to this business.
updateJobPrioritySet job priority (scheduling staff)AInspect

Set job priority (scheduling staff)

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.

NameRequiredDescriptionDefault
idYesJob request ID or short_code
noteNoOptional justification (max 500 chars) — recorded on the activity trail and the priority_changed webhook. Recommended when de-escalating an SLA-escalated job (audit provenance).
priorityYesNew P0–P3 priority level. p0=emergency, p1=top, p2=standard, p3=deferrable.
sla_deadlineNoSLA deadline (business-local naive datetime) — only with priority=p1; (re)arms the auto-escalation clock. Omitted on a p1 change keeps any stored deadline; changing away from p1 disarms SLA entirely.
updateTechnicianUpdate a technicianA
Idempotent
Inspect

Update a technician

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.

NameRequiredDescriptionDefault
idYesTechnician ID
emailNoEmail address. At least one of phone/email is required.
phoneNoPhone number in E.164 international format (`+16135550188`); a bare national number is rejected with PHONE_INVALID — see AddTechnicianReq.Phone. At least one of phone/email is required — enforced in the usecase; see the note on AddTechnicianReq.Phone for why no binding rule may decide it.
addressNoHome address (the day-start point when start_location_type=home).
full_nameYesThe person's full display name. Required; max 255 chars.
job_titleNoDisplay title.
join_dateNoFirst working day (YYYY-MM-DD).
assignment_tierNoCrew tier: lead / buddy / float (float = excluded from auto crew-assign).
business_group_idYesRole group. Discover IDs via GET /permission/groups.
start_location_latNoExplicit day-start latitude in decimal degrees (-90..90); when set it wins over the address geocode.
start_location_longNoExplicit day-start longitude in decimal degrees (-180..180); when set it wins over the address geocode.
start_location_typeNoDay-start point: home or office; explicit coordinates win when set.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 14 tool updatesv0.1.1
    • ChangedcreateCustomer1 field changed
      • changedInput schema / properties / phone / description
        Previous value: -"Phone number. Optional, but at least one of phone/email is required; 10–20 chars."New value: +"Phone number in E.164 international format: a leading `+` and the country\ncode, e.g. `+16135550188`. A bare national number (`6135550188`) is\nREJECTED with PHONE_INVALID — there is no default region to guess the\ncountry from. Separators (spaces, dashes, parentheses) are stripped before\nvalidation. Optional, but at least one of phone/email is required; 10–20 chars."
    • AddedcreateJobType
    • AddedcreateServiceArea
    • AddedcreateSkill
    • AddedcreateSkillCategory
    • ChangedcreateTechnician1 field changed
      • changedInput schema / properties / phone / description
        Previous value: -"At least one of phone/email is required (identity resolution key)."New value: +"Phone number in E.164 international format: a leading `+` and the country\ncode, e.g. `+16135550188`. A bare national number (`6135550188`) is\nREJECTED with PHONE_INVALID — there is no default region to guess the\ncountry from. At least one of phone/email is required (identity resolution key)."
    • AddedcreateVehicle
    • AddeddeleteJobType
    • AddeddeleteServiceArea
    • AddeddeleteSkill
    • AddeddeleteSkillCategory
    • AddeddeleteVehicle
    • ChangedupdateCustomer11 fields changed
      • changedInput schema / properties / address / description
        Previous value: -"Postal address and coordinates."New value: +"Postal address and coordinates. Omit the whole object to leave the stored address untouched; when present it REPLACES the address block."
      • changedInput schema / properties / email / description
        Previous value: -"Email address."New value: +"Email address. Omit to leave unchanged, \"\" to clear."
      • changedInput schema / properties / full_name / description
        Previous value: -"Customer's full name. Required; max 255 chars."New value: +"Customer's full name. Omit to leave unchanged; an empty or blank value is ignored (a customer cannot be left nameless). Max 255 chars."
      • changedInput schema / properties / notes / description
        Previous value: -"Free-form internal notes about the customer; max 4000 chars."New value: +"Free-form internal notes about the customer. Omit to leave unchanged, \"\" to clear; max 4000 chars."
      • changedInput schema / properties / phone / description
        Previous value: -"Phone number. 10–20 chars."New value: +"Phone number in E.164 international format (`+16135550188`); a bare\nnational number is rejected with PHONE_INVALID — see createCustomerReq.Phone.\nOmit to leave unchanged, \"\" to clear. 10–20 chars."
      • changedInput schema / properties / preferred_technician_id / description
        Previous value: -"UUID of the technician this customer prefers. Must belong to this business."New value: +"UUID of the technician this customer prefers. Omit to leave unchanged, \"\" to clear. Must belong to this business."
      • changedInput schema / properties / service_area_id / description
        Previous value: -"UUID of the service area for this customer. Must belong to this business."New value: +"UUID of the service area for this customer. Omit to leave unchanged, \"\" to clear. Must belong to this business."
      • changedInput schema / properties / status / description
        Previous value: -"Lifecycle status."New value: +"Lifecycle status. Omit to leave unchanged; \"\" is ignored."
      • changedInput schema / properties / tier / description
        Previous value: -"Loyalty tier."New value: +"Loyalty tier. Omit to leave unchanged; \"\" is ignored (an enum has no empty member)."
      • changedInput schema / properties / uid / description
        Previous value: -"Your external reference for this customer. Optional; max 32 chars."New value: +"Your external reference for this customer. Omit to leave unchanged, \"\" to clear. Max 32 chars."
      • changedInput schema / required
        Previous value: -[
        -  "id",
        -  "full_name"
        -]New value: +[
        +  "id"
        +]
    • ChangedupdateTechnician1 field changed
      • changedInput schema / properties / phone / description
        Previous value: -"At least one of phone/email is required."New value: +"Phone number in E.164 international format (`+16135550188`); a bare\nnational number is rejected with PHONE_INVALID — see AddTechnicianReq.Phone.\nAt least one of phone/email is required — enforced in the usecase; see\nthe note on AddTechnicianReq.Phone for why no binding rule may decide it."
  2. 43 tool updatesv0.1.0
    • First observedcommitEmergencyReschedule
    • First observedcommitJobRequestMove
    • First observedconfirmJobRequest
    • First observedcreateCustomer
    • First observedcreateJobRequest
    • First observedcreateTechnician
    • First observeddeleteCustomer
    • First observeddeleteTechnician
    • First observedgetCustomer
    • First observedgetJobRequest
    • First observedgetJobRequestTimeline
    • First observedgetJobType
    • First observedgetServiceArea
    • First observedgetTechnician
    • First observedgetTechnicianSchedule
    • First observedgetVehicle
    • First observedlistCrewCandidates
    • First observedlistCustomers
    • First observedlistEmergencyCandidates
    • First observedlistJobRequestBookingWindows
    • First observedlistJobRequestChanges
    • First observedlistJobRequests
    • First observedlistJobTypes
    • First observedlistMatchingSlots
    • First observedlistNearbyTechnicians
    • First observedlistServiceAreas
    • First observedlistSkillCategories
    • First observedlistSkills
    • First observedlistSkillsByCategory
    • First observedlistTechnicians
    • First observedlistTechnicianSkills
    • First observedlistVehicles
    • First observedpreviewEmergencyReschedule
    • First observedpreviewJobRequestMove
    • First observedquoteJobRequest
    • First observedreplaceTechnicianBuddies
    • First observedreplaceTechnicianLeads
    • First observedreplaceTechnicianServiceAreas
    • First observedreplaceTechnicianSkills
    • First observedreplaceTechnicianVehicles
    • First observedupdateCustomer
    • First observedupdateJobPriority
    • First observedupdateTechnician

TDQS

B3.4/5.0

Scored across 53 tools

Disambiguation3/5

Most tools pair a unique action with a unique resource, but the scheduling/candidate cluster—listJobRequestBookingWindows, listMatchingSlots, listNearbyTechnicians, listCrewCandidates, and listEmergencyCandidates—has overlapping concepts that could cause misselection. The preview/commit and quote/confirm pairs are distinguishable but demand close reading.

Naming Consistency5/5

All 53 tools use the same verb-first camelCase pattern (create/get/list/update/delete plus preview/commit/replace/quote), with no snake_case or inconsistent verb styles. Minor semantic quirks like listTechnicianSkills versus listSkillsByCategory do not break the predictable pattern.

Tool Count1/5

With 53 tools, the surface is in the extreme range and creates a heavy tool-selection and context burden for agents. Even though the domain is broad, this set should be split into focused servers (customers, scheduling, technicians, fleet, catalog) rather than exposed as one monolithic tool set.

Completeness2/5

Scheduling and emergency-rescheduling flows are richly covered, but several resources are missing standard updates (no updateVehicle, updateJobType, updateServiceArea, updateSkill, or updateSkillCategory), and job requests lack explicit cancel/archive/advance-status endpoints despite a lifecycle-driven design. These are significant gaps an agent will hit when trying to manage work orders end to end.

Maintenance

ActivityMaintained
ResponsivenessLimited data

Related MCP Connectors

Related MCP Servers