{"components":{"schemas":{"Customer":{"properties":{"contact":{"allOf":[{"$ref":"#/components/schemas/CustomerContact"}],"description":"Contact details: address, phone, email, preferred technician, service area."},"profile":{"allOf":[{"$ref":"#/components/schemas/CustomerProfile"}],"description":"Core identity and lifecycle fields."},"spending":{"allOf":[{"$ref":"#/components/schemas/CustomerSpending"}],"description":"Spending aggregates over the trailing 12 months."}},"type":"object"},"CustomerAddress":{"properties":{"city":{"description":"City / locality.","type":"string"},"country":{"description":"Country (free-form or ISO code as supplied).","type":"string"},"formatted":{"description":"Human-readable single-line address, composed server-side.","type":"string"},"latitude":{"description":"Geographic latitude in decimal degrees; null if no coordinates are saved.","type":"number"},"line":{"description":"Street address, line 1 (e.g. \"123 Main St\").","type":"string"},"line2":{"description":"Street address, line 2 (apartment, suite, unit). Empty if unused.","type":"string"},"longitude":{"description":"Geographic longitude in decimal degrees; null if no coordinates are saved.","type":"number"},"postal_code":{"description":"Postal / ZIP code.","type":"string"},"state":{"description":"State / province / region.","type":"string"}},"type":"object"},"CustomerAddressRequest":{"properties":{"city":{"maxLength":100,"type":"string"},"country":{"maxLength":100,"type":"string"},"formatted":{"maxLength":500,"type":"string"},"latitude":{"maximum":90,"minimum":-90,"type":"number"},"line":{"maxLength":255,"type":"string"},"line2":{"maxLength":255,"type":"string"},"longitude":{"maximum":180,"minimum":-180,"type":"number"},"postal_code":{"maxLength":32,"type":"string"},"state":{"maxLength":100,"type":"string"}},"type":"object"},"CustomerContact":{"properties":{"address":{"allOf":[{"$ref":"#/components/schemas/CustomerAddress"}],"description":"Postal address and coordinates."},"email":{"description":"Email address.","type":"string"},"phone":{"description":"Phone number in the form it was supplied.","type":"string"},"preferred_technician":{"allOf":[{"$ref":"#/components/schemas/CustomerTechnicianRef"}],"description":"The technician this customer prefers, if one is set; otherwise null."},"service_area":{"allOf":[{"$ref":"#/components/schemas/CustomerServiceAreaRef"}],"description":"The service area this customer falls in, if resolved; otherwise null."}},"type":"object"},"CustomerCreateRequest":{"properties":{"address":{"allOf":[{"$ref":"#/components/schemas/CustomerAddressRequest"}],"description":"Postal address and coordinates."},"email":{"description":"Email address. Optional, but at least one of phone/email is required.","type":"string"},"full_name":{"description":"Customer's full name. Required; max 255 chars.","maxLength":255,"type":"string"},"phone":{"description":"Phone number. Optional, but at least one of phone/email is required; 10–20 chars.","maxLength":20,"type":"string"},"preferred_technician_id":{"description":"UUID of the technician this customer prefers. Must belong to this business.","format":"uuid","type":"string"},"service_area_id":{"description":"UUID of the service area for this customer. Must belong to this business.","format":"uuid","type":"string"},"tier":{"description":"Loyalty tier. Defaults to \"regular\" if omitted.","enum":["regular","vip"],"type":"string"},"uid":{"description":"Your external reference for this customer (your own system's ID). Optional; max 32 chars.","maxLength":32,"type":"string"}},"required":["full_name"],"type":"object"},"CustomerCreateResponse":{"properties":{"customer_id":{"description":"UUID of the newly created customer.","format":"uuid","type":"string"}},"type":"object"},"CustomerList":{"properties":{"customers":{"description":"The customers on this page.","items":{"$ref":"#/components/schemas/CustomerListItem"},"type":"array"},"has_more":{"description":"True if more rows exist beyond this page.","type":"boolean"},"meta":{"allOf":[{"$ref":"#/components/schemas/Pagination"}],"description":"Page/limit/total pagination metadata."},"next_since":{"description":"Cursor for short-polling: pass back as `since` to fetch only rows changed\nafter this point (RFC3339).","type":"string"}},"type":"object"},"CustomerListItem":{"properties":{"deleted_at":{"description":"Set (RFC3339) only when the record has been soft-deleted; omitted otherwise.","format":"date-time","type":"string"},"full_name":{"description":"Customer's full name.","type":"string"},"id":{"description":"Customer UUID.","format":"uuid","type":"string"},"last_request_at":{"description":"When the customer last booked, or null if never (RFC3339).","format":"date-time","type":"string"},"request_count":{"description":"Total number of job requests booked.","type":"integer"},"status":{"description":"Lifecycle status.","enum":["active","inactive"],"type":"string"},"tier":{"description":"Loyalty tier.","enum":["regular","vip"],"type":"string"},"uid":{"description":"Your external reference for this customer. Optional.","type":"string"},"updated_at":{"description":"When this record was last modified (RFC3339).","format":"date-time","type":"string"}},"type":"object"},"CustomerProfile":{"properties":{"created_at":{"description":"When the customer record was created (RFC3339).","format":"date-time","type":"string"},"full_name":{"description":"Customer's full name.","type":"string"},"id":{"description":"Customer UUID — the stable identifier used in every customer endpoint.","format":"uuid","type":"string"},"request_count":{"description":"Total number of job requests this customer has booked.","type":"integer"},"status":{"description":"Lifecycle status.","enum":["active","inactive"],"type":"string"},"tier":{"description":"Loyalty tier.","enum":["regular","vip"],"type":"string"},"uid":{"description":"Your external reference for this customer (your own system's ID). Optional.","type":"string"}},"type":"object"},"CustomerServiceAreaRef":{"properties":{"id":{"description":"Service-area UUID.","format":"uuid","type":"string"},"name":{"description":"Service-area display name.","type":"string"}},"type":"object"},"CustomerSpending":{"properties":{"avg_job_value":{"description":"Average value per completed job.","type":"number"},"currency":{"description":"ISO 4217 currency code for the monetary fields (e.g. \"USD\", \"CAD\").","type":"string"},"rank_position_label":{"description":"Human-readable rank label (e.g. \"Top 5%\").","type":"string"},"tier":{"description":"Loyalty tier the spend resolves to.","enum":["regular","vip"],"type":"string"},"tier_progress":{"allOf":[{"$ref":"#/components/schemas/CustomerTierProgress"}],"description":"Progress toward the next tier."},"total_spent_12m":{"description":"Total amount spent in the trailing 12 months.","type":"number"}},"type":"object"},"CustomerTechnicianRef":{"properties":{"id":{"description":"Technician UUID.","format":"uuid","type":"string"},"name":{"description":"Technician display name.","type":"string"}},"type":"object"},"CustomerTierProgress":{"properties":{"current":{"description":"Current accumulated value toward the next tier.","type":"number"},"next":{"description":"Name of the next tier, or empty if already at the top tier.","type":"string"},"remaining":{"description":"Remaining value needed to reach the next tier.","type":"number"}},"type":"object"},"CustomerUpdateRequest":{"properties":{"address":{"allOf":[{"$ref":"#/components/schemas/CustomerAddressRequest"}],"description":"Postal address and coordinates."},"email":{"description":"Email address.","type":"string"},"full_name":{"description":"Customer's full name. Required; max 255 chars.","maxLength":255,"type":"string"},"notes":{"description":"Free-form internal notes about the customer; max 4000 chars.","maxLength":4000,"type":"string"},"phone":{"description":"Phone number. 10–20 chars.","maxLength":20,"type":"string"},"preferred_technician_id":{"description":"UUID of the technician this customer prefers. Must belong to this business.","format":"uuid","type":"string"},"service_area_id":{"description":"UUID of the service area for this customer. Must belong to this business.","format":"uuid","type":"string"},"status":{"description":"Lifecycle status.","enum":["active","inactive"],"type":"string"},"tier":{"description":"Loyalty tier.","enum":["regular","vip"],"type":"string"},"uid":{"description":"Your external reference for this customer. Optional; max 32 chars.","maxLength":32,"type":"string"}},"required":["full_name"],"type":"object"},"JobDate":{"properties":{"date":{"format":"date-time","type":"string"},"periods":{"items":{"$ref":"#/components/schemas/JobDatePeriodEntry"},"type":"array"}},"type":"object"},"JobDateBusinessRange":{"properties":{"date":{"format":"date-time","type":"string"},"end_minute":{"type":"integer"},"period":{"$ref":"#/components/schemas/JobDatePeriod"},"start_minute":{"type":"integer"}},"type":"object"},"JobDatePeriod":{"enum":["morning","afternoon","evening"],"type":"string","x-enum-varnames":["PeriodMorning","PeriodAfternoon","PeriodEvening"]},"JobDatePeriodEntry":{"properties":{"business_view":{"items":{"$ref":"#/components/schemas/JobDateBusinessRange"},"type":"array"},"period":{"$ref":"#/components/schemas/JobDatePeriod"}},"type":"object"},"JobRequest":{"properties":{"action_audit":{"additionalProperties":{"$ref":"#/components/schemas/JobRequestActionAuditEntry"},"description":"ActionAudit is the per-action audit log keyed by action_key. `complete`\nfixed action không xuất hiện ở đây — actor lookup qua CompletedByUserID.","type":"object"},"address":{"allOf":[{"$ref":"#/components/schemas/JobRequestAddressSummary"}],"description":"The job's service address snapshot."},"archive":{"allOf":[{"$ref":"#/components/schemas/JobRequestArchiveSummary"}],"description":"The job's archive state."},"assigned_vehicle":{"allOf":[{"$ref":"#/components/schemas/JobRequestAssignedVehicle"}],"description":"AssignedVehicle is the carpool vehicle (the lead drives). Omitted when the\njob has no vehicle."},"assignment":{"allOf":[{"$ref":"#/components/schemas/JobRequestAssignmentSummary"}],"description":"Who/what is assigned to the job."},"business_id":{"description":"UUID of the owning business.","format":"uuid","type":"string"},"completed_at":{"description":"When the job was completed (UTC); null until completed.","format":"date-time","type":"string"},"completed_by_user_id":{"description":"UUID of the user who fired complete; null until completed.","format":"uuid","type":"string"},"created_at":{"description":"When the job was created (UTC).","format":"date-time","type":"string"},"crew":{"description":"Crew is the multi-person crew on the job (lead first). A single-person job\nhas one member. Omitted when no session plan exists.","items":{"$ref":"#/components/schemas/JobRequestCrewMember"},"type":"array"},"current_status":{"allOf":[{"$ref":"#/components/schemas/JobRequestStatusSummary"}],"description":"The job's current workflow status."},"customer":{"allOf":[{"$ref":"#/components/schemas/JobRequestCustomerSummary"}],"description":"The job's customer contact snapshot."},"customer_url":{"description":"Ready-to-share customer URL embedding the magic token. Omitted if not issued.","type":"string"},"deleted_at":{"description":"When the job was soft-deleted (UTC); omitted unless deleted.","format":"date-time","type":"string"},"description":{"description":"Free-text job description; null if none.","type":"string"},"id":{"description":"Job request UUID.","format":"uuid","type":"string"},"job_type_id":{"description":"UUID of the job type, or null if unclassified.","format":"uuid","type":"string"},"job_type_name":{"description":"Job type display name (resolved to locale). Omitted if unclassified.","type":"string"},"next_actions":{"description":"The action(s) the job is currently waiting on. Omitted in a terminal status.","items":{"$ref":"#/components/schemas/JobRequestActionSummary"},"type":"array"},"priority":{"description":"Job priority.","enum":["normal","emergency"],"type":"string"},"quote":{"allOf":[{"$ref":"#/components/schemas/JobRequestQuoteSummary"}],"description":"The time-bundle quoted for the job."},"rating":{"allOf":[{"$ref":"#/components/schemas/JobRequestRatingSummary"}],"description":"The customer's rating of the completed job."},"schedule":{"allOf":[{"$ref":"#/components/schemas/JobRequestSchedule"}],"description":"When/how the job is scheduled and its per-day plan."},"short_code":{"description":"Human-friendly short code. Omitted if not assigned.","type":"string"},"skills":{"description":"Desired skills for the job (resolved id+name).","items":{"$ref":"#/components/schemas/JobRequestSkillSummary"},"type":"array"},"status_version":{"description":"Optimistic-lock version; pass back as status_version on transitions to avoid races.","type":"integer"},"updated_at":{"description":"When the job was last modified (UTC).","format":"date-time","type":"string"},"workflow_id":{"description":"UUID of the workflow snapshot driving this job's state machine.","format":"uuid","type":"string"},"workflow_name":{"description":"Workflow display name.","type":"string"}},"type":"object"},"JobRequestActionAuditBy":{"properties":{"actor":{"description":"Role that fired the action (business_on_behalf = a business user acting for the customer/technician).","enum":["business","customer","technician","business_on_behalf"],"type":"string"},"user_id":{"description":"UUID of the firing user; null for an unauthenticated customer actor.","format":"uuid","type":"string"}},"type":"object"},"JobRequestActionAuditEntry":{"properties":{"at":{"description":"When the action was fired (UTC).","type":"string"},"by":{"allOf":[{"$ref":"#/components/schemas/JobRequestActionAuditBy"}],"description":"Who fired the action."},"payload":{"description":"Payload is the raw capability payload (e.g. {\"location\":{\"lat\":...,\n\"lng\":...}} for client_location). Forwarded as json.RawMessage so FE\nreceives a real JSON object, NOT a stringified blob. `swaggertype`\nhints the older swag CLI used in the Docker build pipeline (it does\nnot natively recognise json.RawMessage).","type":"object"}},"type":"object"},"JobRequestActionSummary":{"properties":{"actor":{"description":"Role expected to fire this action.","enum":["business","customer","technician","system"],"type":"string"},"capabilities":{"description":"Capability slugs declared on the action (e.g. client_location). Omitted when none.","items":{"type":"string"},"type":"array"},"key":{"description":"Action key (e.g. quote, confirm_booking, complete, or a custom DYNAMIC action).","type":"string"},"label":{"description":"Action label, resolved to the request locale.","type":"string"}},"type":"object"},"JobRequestAddressSummary":{"properties":{"city":{"description":"City / locality.","type":"string"},"country":{"description":"Country (free-form or ISO code as supplied).","type":"string"},"formatted":{"description":"Human-readable single-line address. Omitted if not composed.","type":"string"},"latitude":{"description":"Geographic latitude in decimal degrees; null if no coordinates are saved.","type":"number"},"line":{"description":"Street address, line 1.","type":"string"},"line2":{"description":"Street address, line 2 (apartment, suite, unit). Omitted if unused.","type":"string"},"longitude":{"description":"Geographic longitude in decimal degrees; null if no coordinates are saved.","type":"number"},"postal":{"description":"Postal / ZIP code.","type":"string"},"state":{"description":"State / province / region.","type":"string"}},"type":"object"},"JobRequestArchiveSummary":{"properties":{"archived_at":{"description":"When the job was archived (UTC); null if not archived.","format":"date-time","type":"string"},"archived_by_user_id":{"description":"UUID of the user who archived the job; null if not archived.","format":"uuid","type":"string"},"is_archived":{"description":"Whether the job is archived.","type":"boolean"},"note":{"description":"Optional free-text archive note; null if none.","type":"string"}},"type":"object"},"JobRequestArrivalWindow":{"properties":{"end":{"description":"Window end (UTC) = start + window_minutes.","type":"string"},"start":{"description":"Window start (UTC); render in business/customer timezone.","type":"string"},"window_minutes":{"description":"Width of the arrival window, in minutes.","type":"integer"}},"type":"object"},"JobRequestAssignedVehicle":{"properties":{"id":{"description":"Vehicle UUID.","format":"uuid","type":"string"},"name":{"description":"Vehicle display name.","type":"string"}},"type":"object"},"JobRequestAssignmentSummary":{"properties":{"assigned_at":{"description":"When the technician was assigned (UTC); null if unassigned.","format":"date-time","type":"string"},"service_area_id":{"description":"UUID of the service area the job falls in; null if not resolved.","format":"uuid","type":"string"},"technician":{"allOf":[{"$ref":"#/components/schemas/JobRequestTechnicianInfo"}],"description":"Full contact info of the assigned technician; null if unassigned."},"technician_id":{"description":"UUID of the assigned (lead) technician; null if unassigned.","format":"uuid","type":"string"},"vehicle_id":{"description":"UUID of the assigned carpool vehicle; null if none.","format":"uuid","type":"string"}},"type":"object"},"JobRequestBookingWindows":{"properties":{"alias":{"description":"Business alias the windows were computed for.","type":"string"},"business_timezone":{"description":"IANA timezone of the business (the business_view ranges).","type":"string"},"customer_timezone":{"description":"IANA timezone the customer-facing dates/periods are expressed in.","type":"string"},"days":{"description":"Per-day availability, ordered chronologically.","items":{"$ref":"#/components/schemas/JobRequestDayWindows"},"type":"array"}},"type":"object"},"JobRequestBusinessTimeRange":{"properties":{"date":{"description":"Calendar day (YYYY-MM-DD), business-local.","format":"date-time","type":"string"},"end_minute":{"description":"Range end as minutes since midnight (0–1440), business-local.","type":"integer"},"period":{"description":"Time-of-day period this range belongs to.","enum":["morning","afternoon","evening"],"type":"string"},"start_minute":{"description":"Range start as minutes since midnight (0–1440), business-local.","type":"integer"}},"type":"object"},"JobRequestChanges":{"properties":{"has_more":{"description":"True when this page hit `limit` (more changes already waiting) — poll again\nimmediately instead of waiting your normal interval. False ⇒ you are caught\nup; resume your normal polling interval.","type":"boolean"},"items":{"description":"The changed job requests, oldest-change first (ordered by updated_at ASC).\nEach item is the full job-request shape (same as GET /job-requests/{id}).\nSoft-deleted/archived jobs also surface here so you can prune your copy.","items":{"$ref":"#/components/schemas/JobRequest"},"type":"array"},"next_since":{"description":"The cursor to send as `since` on your next poll. Always store and echo it.\nNOTE: the server re-scans a small safety window (~5s) each poll, so a job\nMAY appear again across polls — upsert by `id`, never blindly append.","format":"date-time","type":"string"}},"type":"object"},"JobRequestCreateRequest":{"properties":{"customer_id":{"description":"UUID of an existing customer of this business to book the job for. Required.","format":"uuid","type":"string"},"description":{"description":"Free-text description of the work requested. Optional; max 2000 chars.","maxLength":2000,"type":"string"},"job_dates":{"description":"Requested date(s) + period(s) the customer wants the job. At least one, up to 12.","items":{"$ref":"#/components/schemas/JobRequestJobDateRequest"},"maxItems":12,"minItems":1,"type":"array"},"job_type_id":{"description":"UUID of the job type to classify this job. Optional; null leaves the job unclassified.","format":"uuid","type":"string"},"skill_ids":{"description":"UUIDs of the skills the customer desires for this job. Optional; up to 20.","items":{"format":"uuid","type":"string"},"maxItems":20,"type":"array"}},"required":["customer_id","job_dates"],"type":"object"},"JobRequestCreateResponse":{"properties":{"customer_url":{"description":"Ready-to-share customer URL embedding the magic token. Omitted if not issued.","type":"string"},"id":{"description":"UUID of the newly created job request.","format":"uuid","type":"string"},"magic_token":{"description":"Customer magic-link JWT granting access to this job's customer pages. Omitted if not issued.","type":"string"},"short_code":{"description":"Human-friendly short code for the job (e.g. for customer reference). Omitted if not assigned.","type":"string"}},"type":"object"},"JobRequestCrewMember":{"properties":{"email":{"type":"string"},"ends_at":{"description":"This member's last session end (UTC). Omitted when no session plan exists.","format":"date-time","type":"string"},"job_title":{"description":"JobTitle/Phone/Email — crew member's contact (resolved like the customer\nblock). Empty for a planned-but-unassigned slot.","type":"string"},"phone":{"type":"string"},"role":{"description":"Role of this member on the job.","enum":["lead","buddy"],"type":"string"},"sessions":{"description":"This member's per-day on-site work blocks (ordered).","items":{"$ref":"#/components/schemas/JobRequestSession"},"type":"array"},"skill_ids":{"description":"SkillIDs are the skills this crew slot was quoted to require (the skills\nthe assignment engine matched on); Skills resolves them to id+name for\ndisplay. Omitted when the slot carries no skill requirement.","items":{"format":"uuid","type":"string"},"type":"array"},"skills":{"description":"Resolved skills (id+name) for SkillIDs. Omitted when the slot carries no skill requirement.","items":{"$ref":"#/components/schemas/JobRequestSkillSummary"},"type":"array"},"technician_id":{"description":"TechnicianID/Name identify the assigned technician. Empty for a planned-\nbut-unassigned crew member (quoted, not yet confirmed/assigned).","format":"uuid","type":"string"},"technician_name":{"type":"string"},"wrench_percent":{"description":"Share of the job's man-hours this member works (1–100). Omitted for a single-person job.","type":"integer"}},"type":"object"},"JobRequestCustomerSummary":{"properties":{"email":{"description":"Customer email, or null if not captured.","type":"string"},"id":{"description":"Customer UUID, or null for an inline public-booking customer with no saved record.","format":"uuid","type":"string"},"name":{"description":"Customer's full name.","type":"string"},"phone":{"description":"Customer phone, or null if not captured.","type":"string"}},"type":"object"},"JobRequestDayWindows":{"properties":{"date":{"description":"Calendar day (YYYY-MM-DD), customer-local.","format":"date-time","type":"string"},"periods":{"description":"Available time-of-day periods on this day.","items":{"$ref":"#/components/schemas/JobRequestPeriod"},"type":"array"}},"type":"object"},"JobRequestJobDateBusinessRangeRequest":{"properties":{"date":{"description":"Calendar day (YYYY-MM-DD), business-local.","format":"date-time","type":"string"},"end_minute":{"description":"Range end as minutes since midnight (0–1440), business-local.","type":"integer"},"period":{"description":"Time-of-day period this range belongs to.","enum":["morning","afternoon","evening"],"type":"string"},"start_minute":{"description":"Range start as minutes since midnight (0–1440), business-local.","type":"integer"}},"type":"object"},"JobRequestJobDatePeriodRequest":{"properties":{"business_view":{"description":"Business-local time ranges the period maps to (echoed from booking-windows). Optional.","items":{"$ref":"#/components/schemas/JobRequestJobDateBusinessRangeRequest"},"type":"array"},"period":{"description":"Time-of-day period the customer requested. Required.","enum":["morning","afternoon","evening"],"type":"string"}},"required":["period"],"type":"object"},"JobRequestJobDateRequest":{"properties":{"date":{"description":"Requested calendar day (YYYY-MM-DD), customer-local. Required.","format":"date-time","type":"string"},"periods":{"description":"One or more time-of-day periods requested on this date. At least one, up to 3.","items":{"$ref":"#/components/schemas/JobRequestJobDatePeriodRequest"},"maxItems":3,"minItems":1,"type":"array"}},"required":["date","periods"],"type":"object"},"JobRequestList":{"properties":{"items":{"description":"The job requests on this page.","items":{"$ref":"#/components/schemas/JobRequest"},"type":"array"},"meta":{"allOf":[{"$ref":"#/components/schemas/Pagination"}],"description":"Page/limit/total pagination metadata."}},"type":"object"},"JobRequestPeriod":{"properties":{"business_view":{"description":"Business-local time ranges that make up this period's availability.","items":{"$ref":"#/components/schemas/JobRequestBusinessTimeRange"},"type":"array"},"period":{"description":"Time-of-day period.","enum":["morning","afternoon","evening"],"type":"string"}},"type":"object"},"JobRequestQuoteSummary":{"properties":{"demobilization_minutes":{"description":"Demobilization (teardown) minutes; null until quoted.","type":"integer"},"job_duration_minutes":{"description":"Hands-on work duration in minutes; null until quoted.","type":"integer"},"mobilization_minutes":{"description":"Mobilization (setup/travel-prep) minutes; null until quoted.","type":"integer"},"quoted_at":{"description":"When the quote was fired (UTC); null until quoted.","format":"date-time","type":"string"}},"type":"object"},"JobRequestRatingSummary":{"properties":{"comment":{"description":"Optional free-text rating comment; null if none.","type":"string"},"is_rated":{"description":"Whether the customer has rated the job.","type":"boolean"},"rated_at":{"description":"When the job was rated (UTC); null if not rated.","format":"date-time","type":"string"},"score":{"description":"Rating score (1–5); null if not rated.","type":"integer"}},"type":"object"},"JobRequestSchedule":{"properties":{"arrival_window":{"allOf":[{"$ref":"#/components/schemas/JobRequestArrivalWindow"}],"description":"ArrivalWindow is the [start, start+window_minutes] arrival window the customer\nsees after confirming. start/end UTC — render in business/customer timezone. Omitted until confirmed."},"business_timezone":{"description":"IANA timezone of the business. Omitted if unknown.","type":"string"},"customer_timezone":{"description":"IANA timezone of the customer. Omitted if unknown.","type":"string"},"ends_at":{"description":"EndsAt is the job's span END (last session end, UTC). For a multi-day job\nit falls on a LATER calendar day than scheduled_at — render \"starts D1 →\nends Dn\". Nil when no session plan exists.","format":"date-time","type":"string"},"job_dates":{"description":"Requested date(s)/period(s) captured at booking. Omitted once confirmed/scheduled.\nUse regional.JobDate directly (not the jobrequest.JobDate alias) so swag emits\na single canonical `JobDate` schema instead of an aliased duplicate.","items":{"$ref":"#/components/schemas/JobDate"},"type":"array"},"scheduled_at":{"description":"Confirmed start time (UTC); null until the customer confirms. Render in business/customer timezone.","format":"date-time","type":"string"},"scheduled_duration_minutes":{"description":"Total scheduled duration in minutes (job + mobilization + demobilization); null until quoted.","type":"integer"},"sessions":{"description":"Sessions is the per-day on-site breakdown (ordered): one for single-day,\nseveral for multi-day.","items":{"$ref":"#/components/schemas/JobRequestSession"},"type":"array"}},"type":"object"},"JobRequestSession":{"properties":{"date":{"description":"Calendar day (YYYY-MM-DD) this session falls on, business-local.","format":"date-time","type":"string"},"depart_at":{"description":"DepartAt / ReturnAt bracket the technician's whole day (leave home / arrive\nhome), derived from travel. Omitted together when travel is unknown.","format":"date-time","type":"string"},"end_at":{"description":"On-site end of this day's work block (UTC).","format":"date-time","type":"string"},"ordinal":{"description":"1-based day index within the job's span (1 for single-day jobs).","type":"integer"},"return_at":{"format":"date-time","type":"string"},"start_at":{"description":"On-site start of this day's work block (UTC).","format":"date-time","type":"string"},"travel_minutes":{"description":"TravelMinutes is the planned one-way commute for this day. Omitted when\nunknown (admin-fallback job without geocoded location, or legacy row).","type":"integer"}},"type":"object"},"JobRequestSkillSummary":{"properties":{"category_id":{"description":"UUID of the skill's category.","format":"uuid","type":"string"},"category_name":{"description":"Category display name.","type":"string"},"id":{"description":"Skill UUID.","format":"uuid","type":"string"},"is_active":{"description":"Whether the skill is currently active.","type":"boolean"},"name":{"description":"Skill display name (resolved to the request locale).","type":"string"}},"type":"object"},"JobRequestStatusSummary":{"properties":{"display_name":{"description":"Status display name, resolved to the request locale.","type":"string"},"key":{"description":"Workflow status key (the business's workflow defines the set; first is always \"booking\", last always \"completed\").","type":"string"}},"type":"object"},"JobRequestTechnicianInfo":{"properties":{"avatar_url":{"description":"Technician avatar URL (CDN); null if no avatar set.","type":"string"},"email":{"description":"Technician email; null if not available.","type":"string"},"full_name":{"description":"Technician's full name.","type":"string"},"id":{"description":"Technician UUID.","format":"uuid","type":"string"},"job_title":{"description":"Technician's job title. Omitted if unset.","type":"string"},"phone":{"description":"Technician phone; null if not available.","type":"string"}},"type":"object"},"JobRequestTimeline":{"properties":{"current_status_key":{"description":"Status key the job is currently in.","type":"string"},"events":{"description":"Status rows in workflow order (booking → … → completed).","items":{"$ref":"#/components/schemas/JobRequestTimelineEvent"},"type":"array"},"status_version":{"description":"Optimistic-lock version of the job (read-only here; timeline does not bump it).","type":"integer"},"workflow_id":{"description":"UUID of the workflow snapshot driving this job.","format":"uuid","type":"string"},"workflow_name":{"description":"Workflow display name.","type":"string"}},"type":"object"},"JobRequestTimelineAction":{"properties":{"actor":{"description":"Role expected to fire this action (badge hint, NOT who actually fired it).","enum":["business","customer","technician","system"],"type":"string"},"fired_at":{"description":"When the action was fired (UTC); null until fired.","format":"date-time","type":"string"},"key":{"description":"Action key (e.g. quote, confirm_booking, complete, or a DYNAMIC action).","type":"string"},"label":{"description":"Action label, resolved to the request locale.","type":"string"},"payload":{"description":"Payload mirrors action_data[key].payload — raw JSON; `swaggertype`\nhints the swag CLI which doesn't natively know json.RawMessage.","type":"object"},"state":{"description":"Where this action sits relative to the job's progress.","enum":["completed","pending","upcoming"],"type":"string"}},"type":"object"},"JobRequestTimelineEvent":{"properties":{"actions":{"description":"Actions declared for this status, in workflow order. Omitted when none.","items":{"$ref":"#/components/schemas/JobRequestTimelineAction"},"type":"array"},"display_name":{"description":"Status display name, resolved to the request locale.","type":"string"},"entered_at":{"description":"When the job entered this status (UTC); null for upcoming steps or jobs missing the backfill.","format":"date-time","type":"string"},"state":{"description":"Where this status sits relative to the job's progress.","enum":["completed","current","upcoming"],"type":"string"},"status_key":{"description":"Workflow status key for this row.","type":"string"}},"type":"object"},"JobType":{"properties":{"business_id":{"description":"UUID of the business that owns this job type.","example":"660e8400-e29b-41d4-a716-446655440000","format":"uuid","type":"string"},"created_at":{"description":"When the job type was created (RFC3339).","format":"date-time","type":"string"},"id":{"description":"Job type UUID — the stable identifier used in every job-type endpoint.","example":"550e8400-e29b-41d4-a716-446655440000","format":"uuid","type":"string"},"is_system":{"description":"True for platform-seeded system rows, which cannot be modified or deleted.","example":true,"type":"boolean"},"name":{"description":"Job type name, localized server-side to the request locale.","example":"General","type":"string"},"status":{"description":"Lifecycle status. Inactive types remain on historical jobs but cannot be selected for new requests.","enum":["active","inactive"],"example":"active","type":"string"},"updated_at":{"description":"When the job type was last modified (RFC3339).","format":"date-time","type":"string"}},"type":"object"},"Pagination":{"properties":{"count":{"type":"integer"},"current_page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"},"total_pages":{"type":"integer"}},"type":"object"},"ResponseEnvelope":{"properties":{"data":{},"error_code":{},"errors":{},"message":{"type":"string"}},"type":"object"},"ServiceArea":{"properties":{"boundary":{"description":"GeoJSON polygon boundary of the area; null if no boundary was set.","type":"object"},"business_id":{"description":"UUID of the business that owns this service area.","example":"660e8400-e29b-41d4-a716-446655440000","format":"uuid","type":"string"},"city":{"description":"City / locality for the area. Empty if unused.","example":"New York","type":"string"},"county":{"description":"County for the area. Empty if unused.","example":"New York County","type":"string"},"created_at":{"description":"When the service area was created (RFC3339).","format":"date-time","type":"string"},"description":{"description":"Free-form description of the area. Empty if none was set.","example":"Main service area","type":"string"},"district":{"description":"District for the area. Empty if unused.","example":"Manhattan","type":"string"},"id":{"description":"Service-area UUID — the stable identifier used in every service-area endpoint.","example":"550e8400-e29b-41d4-a716-446655440000","format":"uuid","type":"string"},"name":{"description":"Service-area display name.","example":"Downtown Area","type":"string"},"postal_code":{"description":"Postal / ZIP code for the area. Empty if unused.","example":"10001","type":"string"},"updated_at":{"description":"When the service area was last modified (RFC3339).","format":"date-time","type":"string"}},"type":"object"},"ServiceAreaList":{"properties":{"meta":{"allOf":[{"$ref":"#/components/schemas/Pagination"}],"description":"Page/limit/total pagination metadata."},"service_areas":{"description":"The service areas on this page.","items":{"$ref":"#/components/schemas/ServiceArea"},"type":"array"}},"type":"object"},"Skill":{"properties":{"category_id":{"example":"550e8400-e29b-41d4-a716-446655440001","format":"uuid","type":"string"},"created_at":{"example":"2024-01-15T08:30:00.000000000Z","format":"date-time","type":"string"},"description":{"example":"Repair of household water pipes","type":"string"},"id":{"example":"550e8400-e29b-41d4-a716-446655440000","format":"uuid","type":"string"},"is_active":{"example":true,"type":"boolean"},"members":{"description":"Members is the count of active (non-deleted) technicians currently assigned to this skill.","example":3,"type":"integer"},"name":{"example":"Pipe Repair","type":"string"},"updated_at":{"example":"2024-01-15T08:30:00.000000000Z","format":"date-time","type":"string"}},"type":"object"},"SkillByCategoryList":{"properties":{"meta":{"$ref":"#/components/schemas/Pagination"},"skills":{"items":{"$ref":"#/components/schemas/Skill"},"type":"array"}},"type":"object"},"SkillCategory":{"properties":{"created_at":{"example":"2024-01-15T08:30:00.000000000Z","format":"date-time","type":"string"},"icon":{"example":"wrench","type":"string"},"id":{"example":"550e8400-e29b-41d4-a716-446655440000","format":"uuid","type":"string"},"name":{"example":"Plumbing","type":"string"},"updated_at":{"example":"2024-01-15T08:30:00.000000000Z","format":"date-time","type":"string"}},"type":"object"},"SkillCategoryList":{"properties":{"categories":{"items":{"$ref":"#/components/schemas/SkillCategory"},"type":"array"},"meta":{"$ref":"#/components/schemas/Pagination"}},"type":"object"},"SkillList":{"properties":{"skills":{"description":"All active skills across every category.","items":{"$ref":"#/components/schemas/SkillSummary"},"type":"array"}},"type":"object"},"SkillSummary":{"properties":{"category_id":{"description":"UUID of the category this skill belongs to.","example":"550e8400-e29b-41d4-a716-446655440001","format":"uuid","type":"string"},"category_name":{"description":"Display name of the skill's category.","example":"Plumbing","type":"string"},"id":{"description":"Skill UUID — pass this in `skill_ids` when creating a job request.","example":"550e8400-e29b-41d4-a716-446655440000","format":"uuid","type":"string"},"is_active":{"description":"Whether the skill is active and bookable.","example":true,"type":"boolean"},"name":{"description":"Skill name.","example":"Pipe Repair","type":"string"}},"type":"object"},"Technician":{"properties":{"address":{"allOf":[{"$ref":"#/components/schemas/TechnicianAddress"}],"description":"Technician's start-location postal address."},"assignment_tier":{"description":"Smart-assignment tier, or null if unset.","enum":["lead","buddy","float"],"type":"string"},"buddy_ids":{"description":"UUIDs of this technician's buddies (the techs they prefer to work alongside).","items":{"format":"uuid","type":"string"},"type":"array"},"business_group_id":{"description":"UUID of the permission group (role) this technician is assigned to.","format":"uuid","type":"string"},"business_id":{"description":"UUID of the business this technician belongs to.","format":"uuid","type":"string"},"created_at":{"description":"When the technician record was created (RFC3339).","format":"date-time","type":"string"},"deleted_at":{"description":"Set (RFC3339) only when the technician has been soft-removed; omitted otherwise.","format":"date-time","type":"string"},"email":{"description":"Email address; null if not set.","type":"string"},"full_name":{"description":"Technician's full name.","type":"string"},"id":{"description":"Technician membership UUID — the stable identifier used in every technician endpoint.","format":"uuid","type":"string"},"job_title":{"description":"Free-form job title (e.g. \"Senior HVAC Technician\"). Empty if unset.","type":"string"},"join_date":{"description":"Date the technician joined (YYYY-MM-DD); null if unset.","format":"date","type":"string"},"last_login_at":{"description":"When the technician last logged in (RFC3339); null if never.","format":"date-time","type":"string"},"leads":{"description":"Leads this technician is a buddy of (id + name).","items":{"$ref":"#/components/schemas/TechnicianLeadRef"},"type":"array"},"phone":{"description":"Phone number in the form it was supplied; null if not set.","type":"string"},"role":{"description":"Resolved role/group name (e.g. \"Technician\", \"Owner\").","type":"string"},"service_areas":{"description":"Service areas this technician is assigned to (id + name).","items":{"$ref":"#/components/schemas/TechnicianServiceAreaRef"},"type":"array"},"start_location_lat":{"description":"Start-location latitude in decimal degrees; null if no coordinates are saved.","type":"number"},"start_location_long":{"description":"Start-location longitude in decimal degrees; null if no coordinates are saved.","type":"number"},"start_location_type":{"description":"Where the technician's day starts: \"home\" (their address) or \"office\" (the business address). Null if unset.","enum":["home","office"],"type":"string"},"status":{"allOf":[{"$ref":"#/components/schemas/TechnicianStatus"}],"description":"Membership lifecycle status.","enum":["active","onboarding","deactive"]},"updated_at":{"description":"When the technician record was last modified (RFC3339).","format":"date-time","type":"string"},"user_id":{"description":"UUID of the underlying user account.","format":"uuid","type":"string"},"vehicle_ids":{"description":"UUIDs of the vehicles this technician uses.","items":{"format":"uuid","type":"string"},"type":"array"}},"type":"object"},"TechnicianAddress":{"properties":{"city":{"description":"City / locality. Empty if unset.","type":"string"},"country":{"description":"Country (free-form or ISO code as supplied). Empty if unset.","type":"string"},"formatted":{"description":"Human-readable single-line address. Empty if unset.","type":"string"},"line":{"description":"Street address, line 1 (e.g. \"123 Main St\"). Empty if unset.","type":"string"},"line2":{"description":"Street address, line 2 (apartment, suite, unit). Empty if unused.","type":"string"},"postal_code":{"description":"Postal / ZIP code. Empty if unset.","type":"string"},"state":{"description":"State / province / region. Empty if unset.","type":"string"}},"type":"object"},"TechnicianLeadRef":{"properties":{"full_name":{"description":"Lead technician display name.","type":"string"},"id":{"description":"Lead technician UUID.","format":"uuid","type":"string"}},"type":"object"},"TechnicianList":{"properties":{"has_more":{"description":"True if more rows exist beyond this page.","type":"boolean"},"meta":{"allOf":[{"$ref":"#/components/schemas/Pagination"}],"description":"Page/limit/total pagination metadata."},"next_since":{"description":"Cursor for short-polling: pass back as `since` to fetch only rows changed after this point (RFC3339).","type":"string"},"technicians":{"description":"The technicians on this page.","items":{"$ref":"#/components/schemas/Technician"},"type":"array"}},"type":"object"},"TechnicianServiceAreaRef":{"properties":{"id":{"description":"Service-area UUID.","format":"uuid","type":"string"},"name":{"description":"Service-area display name.","type":"string"}},"type":"object"},"TechnicianStatus":{"enum":["active","onboarding","deactive"],"type":"string","x-enum-varnames":["StatusActive","StatusOnboarding","StatusDeactive"]},"Vehicle":{"properties":{"brand":{"description":"Manufacturer / make (e.g. \"Ford\"). Empty if not set.","example":"Ford","type":"string"},"business_id":{"description":"UUID of the business that owns this vehicle.","example":"660e8400-e29b-41d4-a716-446655440000","format":"uuid","type":"string"},"created_at":{"description":"When the vehicle record was created (RFC3339).","format":"date-time","type":"string"},"current_mileage":{"description":"Current odometer reading; null if not tracked.","example":50000,"type":"integer"},"deleted_at":{"description":"Set (RFC3339) only when the record has been soft-deleted; omitted otherwise.","format":"date-time","type":"string"},"id":{"description":"Vehicle UUID — the stable identifier used in every vehicle endpoint.","example":"550e8400-e29b-41d4-a716-446655440000","format":"uuid","type":"string"},"model":{"description":"Model name (e.g. \"Transit\"). Empty if not set.","example":"Transit","type":"string"},"name":{"description":"Vehicle display name.","example":"North Van 01","type":"string"},"operational_status":{"description":"Live operational state derived from the vehicle's jobs: on_job = a job's scheduled window contains now (the vehicle is out working); assigned = attached to an upcoming/open job that is not in progress; other values mirror status.","enum":["inactive","idle","assigned","on_job","maintenance"],"example":"assigned","type":"string"},"owner":{"allOf":[{"$ref":"#/components/schemas/VehicleOwner"}],"description":"The technician who currently owns (claimed) this vehicle; null if unowned."},"plate_number":{"description":"License plate number. Empty if not set.","example":"ABC-1234","type":"string"},"status":{"description":"Stored status. on_job here means \"attached to an open job\" (set at assignment, cleared at complete/archive/unassign) — see operational_status for the live state.","enum":["inactive","idle","on_job","maintenance"],"example":"idle","type":"string"},"technician_ids":{"description":"UUIDs of technicians who use this vehicle (derived from each technician's vehicle list). Empty array if none.","items":{"format":"uuid","type":"string"},"type":"array"},"updated_at":{"description":"When the vehicle record was last modified (RFC3339).","format":"date-time","type":"string"},"utilization_percent":{"description":"Percentage of capacity the vehicle is utilized (0–100).","example":25,"type":"integer"},"vehicle_type":{"description":"Vehicle category.","enum":["van","truck","car"],"example":"van","type":"string"},"year":{"description":"Model year. Zero if not set.","example":2023,"type":"integer"}},"type":"object"},"VehicleList":{"properties":{"has_more":{"description":"True if more rows exist beyond this page.","type":"boolean"},"meta":{"allOf":[{"$ref":"#/components/schemas/Pagination"}],"description":"Page/limit/total pagination metadata."},"next_since":{"description":"Cursor for short-polling: pass back as `since` to fetch only rows changed after this point (RFC3339).","format":"date-time","type":"string"},"vehicles":{"description":"The vehicles on this page.","items":{"$ref":"#/components/schemas/Vehicle"},"type":"array"}},"type":"object"},"VehicleOwner":{"properties":{"full_name":{"description":"Owning technician's display name.","example":"John Doe","type":"string"},"id":{"description":"UUID of the owning technician's business user profile.","example":"770e8400-e29b-41d4-a716-446655440000","format":"uuid","type":"string"}},"type":"object"}},"securitySchemes":{"ApiKeyAuth":{"description":"Secret API key as a Bearer token, e.g. `Authorization: Bearer chsk_live_xxx`. Use a `chsk_test_` key for sandbox data.","in":"header","name":"Authorization","type":"apiKey"}}},"info":{"description":"Public REST API for integrating CrispHive from your own backend. Authenticate every request with a secret API key as a Bearer token (`Authorization: Bearer chsk_live_…`). The key prefix selects the data environment: `chsk_live_…` → production (live), `chsk_test_…` → sandbox (isolated test).\n\n**Key scopes (restricted keys).** A key is either *full-access* (can call every endpoint below) or *restricted* to a set of permission codes chosen at creation — the same codes as the dashboard permission grid (e.g. `customers_view`, `job_create`, `team_manage`). A restricted key calling an endpoint outside its scope gets `403`. The full code list is the permission catalog (`GET /permission/modules` on the dashboard API). Create, scope, and revoke keys from the business dashboard.\n\nEvery response is wrapped in the envelope `{ \"error_code\": 0, \"message\": \"Success\", \"data\": \u003cpayload\u003e }`.","title":"CrispHive Developer API","version":"1.0"},"openapi":"3.0.3","paths":{"/customers":{"get":{"description":"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.","operationId":"listCustomers","parameters":[{"description":"Search name, UID, phone, email","in":"query","name":"q","schema":{"type":"string"}},{"description":"Filter by tier: regular|vip (repeatable)","in":"query","name":"tier","schema":{"items":{"type":"string"},"type":"array"}},{"description":"Filter by status: active|inactive","in":"query","name":"status","schema":{"type":"string"}},{"description":"Filter by preferred technician UUID","in":"query","name":"preferred_technician_id","schema":{"type":"string"}},{"description":"Sort: created_at_desc|created_at_asc|name_asc|name_desc|uid_asc|uid_desc (default: created_at_desc)","in":"query","name":"sort","schema":{"type":"string"}},{"description":"Page number (default 1)","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Page size (default 15, max 1000)","in":"query","name":"limit","schema":{"type":"integer"}},{"description":"RFC3339 cursor for short-polling; echo back next_since from prior response","in":"query","name":"since","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/CustomerList"}},"type":"object"}]}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"WRONG_QUERY"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"}},"security":[{"ApiKeyAuth":[]}],"summary":"List customers","tags":["customer"]},"post":{"description":"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.","operationId":"createCustomer","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerCreateRequest"}}},"description":"Customer details","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/CustomerCreateResponse"}},"type":"object"}]}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"INVALID_REQUEST_BODY | PHONE_OR_EMAIL_REQUIRED"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"BUSINESS_NOT_FOUND"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"CUSTOMER_DUPLICATE_UID | CUSTOMER_DUPLICATE_PHONE | CUSTOMER_DUPLICATE_EMAIL"}},"security":[{"ApiKeyAuth":[]}],"summary":"Create a customer","tags":["customer"]}},"/customers/{id}":{"delete":{"description":"Soft-deletes a customer record, removing it from the active customer directory; existing bookings keep their customer snapshot.","operationId":"deleteCustomer","parameters":[{"description":"Customer ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"CUSTOMER_NOT_FOUND"}},"security":[{"ApiKeyAuth":[]}],"summary":"Delete a customer","tags":["customer"]},"get":{"description":"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.","operationId":"getCustomer","parameters":[{"description":"Customer ID (UUID)","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/Customer"}},"type":"object"}]}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"CUSTOMER_NOT_FOUND"}},"security":[{"ApiKeyAuth":[]}],"summary":"Get a customer","tags":["customer"]},"put":{"description":"Replaces mutable fields on a customer record — two-way CRM sync friendly (push changes from your system of record). Pass service_area_id=\"\" to clear the service area. Address fields (including latitude/longitude) live under the nested `address` object.","operationId":"updateCustomer","parameters":[{"description":"Customer ID (UUID)","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerUpdateRequest"}}},"description":"Fields to update","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"INVALID_REQUEST_BODY"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"CUSTOMER_NOT_FOUND"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"CUSTOMER_DUPLICATE_PHONE | CUSTOMER_DUPLICATE_EMAIL"}},"security":[{"ApiKeyAuth":[]}],"summary":"Update a customer","tags":["customer"]}},"/job-requests":{"get":{"description":"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.","operationId":"listJobRequests","parameters":[{"description":"Comma-separated status slugs","in":"query","name":"status_keys","schema":{"type":"string"}},{"description":"active (default) | archived | all","in":"query","name":"status","schema":{"type":"string"}},{"description":"Customer UUID","in":"query","name":"customer_id","schema":{"type":"string"}},{"description":"Technician UUID","in":"query","name":"technician_id","schema":{"type":"string"}},{"description":"Filter from (YYYY-MM-DD = start of that day in the business timezone, or RFC3339); range is [from, to)","in":"query","name":"scheduled_from","schema":{"type":"string"}},{"description":"Filter to (YYYY-MM-DD = end of that day in the business timezone, or RFC3339), exclusive","in":"query","name":"scheduled_to","schema":{"type":"string"}},{"description":"Search short_code or description (case-insensitive, partial match)","in":"query","name":"q","schema":{"type":"string"}},{"description":"Sort key","in":"query","name":"sort","schema":{"type":"string"}},{"description":"Page number","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Page size","in":"query","name":"limit","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/JobRequestList"}},"type":"object"}]}}},"description":"OK"}},"security":[{"ApiKeyAuth":[]}],"summary":"List job requests","tags":["job-request-business"]},"post":{"description":"Books a field-service job — the work order that enters the dispatch \u0026 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.","operationId":"createJobRequest","parameters":[{"description":"Customer IANA timezone","in":"header","name":"X-Timezone","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobRequestCreateRequest"}}},"description":"Booking payload","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/JobRequestCreateResponse"}},"type":"object"}]}}},"description":"OK"}},"security":[{"ApiKeyAuth":[]}],"summary":"Create a job request (business actor)","tags":["job-request-business"]}},"/job-requests/booking-windows":{"get":{"description":"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.","operationId":"listJobRequestBookingWindows","parameters":[{"description":"Customer IANA timezone","in":"header","name":"X-Timezone","required":true,"schema":{"type":"string"}},{"description":"Start YYYY-MM-DD","in":"query","name":"from","schema":{"type":"string"}},{"description":"End YYYY-MM-DD","in":"query","name":"to","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/JobRequestBookingWindows"}},"type":"object"}]}}},"description":"OK"}},"security":[{"ApiKeyAuth":[]}],"summary":"Booking availability (business actor)","tags":["job-request-business"]}},"/job-requests/changes":{"get":{"description":"Keep an external system (your CRM, ERP or field-service 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).\n\nHow 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).\n\nThis 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.","operationId":"listJobRequestChanges","parameters":[{"description":"Comma-separated status slugs — only surface changes to jobs in these statuses","in":"query","name":"status_keys","schema":{"type":"string"}},{"description":"Priority filter (normal|emergency)","in":"query","name":"priority","schema":{"type":"string"}},{"description":"Only changes to this customer's jobs (UUID)","in":"query","name":"customer_id","schema":{"type":"string"}},{"description":"Only changes to jobs assigned to this technician (UUID)","in":"query","name":"technician_id","schema":{"type":"string"}},{"description":"Filter from (YYYY-MM-DD = start of that day in the business timezone, or RFC3339); range is [from, to)","in":"query","name":"scheduled_from","schema":{"type":"string"}},{"description":"Filter to (YYYY-MM-DD = end of that day in the business timezone, or RFC3339), exclusive","in":"query","name":"scheduled_to","schema":{"type":"string"}},{"description":"RFC3339 cursor from the prior response's next_since. OMIT on the first poll to prime the cursor at server-now.","in":"query","name":"since","schema":{"type":"string"}},{"description":"Max changes per poll (default 15, max 1000). If the page fills, has_more=true.","in":"query","name":"limit","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/JobRequestChanges"}},"type":"object"}]}}},"description":"OK"}},"security":[{"ApiKeyAuth":[]}],"summary":"Poll for new \u0026 changed job requests (sync feed)","tags":["job-request-business"]}},"/job-requests/{id}":{"get":{"description":"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-service system needs to track one job.","operationId":"getJobRequest","parameters":[{"description":"Job request ID (UUID or short_code)","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/JobRequest"}},"type":"object"}]}}},"description":"OK"}},"security":[{"ApiKeyAuth":[]}],"summary":"Get a job request","tags":["job-request-business"]}},"/job-requests/{id}/timeline":{"get":{"description":"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.","operationId":"getJobRequestTimeline","parameters":[{"description":"Job request ID (UUID or short_code)","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/JobRequestTimeline"}},"type":"object"}]}}},"description":"OK"}},"security":[{"ApiKeyAuth":[]}],"summary":"Job timeline (business surface — also serves tech via BusinessAuth)","tags":["job-request-business"]}},"/job-types":{"get":{"description":"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.","operationId":"listJobTypes","parameters":[{"description":"Filter by status (active|inactive)","in":"query","name":"status","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"items":{"$ref":"#/components/schemas/JobType"},"type":"array"}},"type":"object"}]}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"}},"security":[{"ApiKeyAuth":[]}],"summary":"List job types","tags":["job-types"]}},"/job-types/{id}":{"get":{"description":"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.","operationId":"getJobType","parameters":[{"description":"Job Type ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/JobType"}},"type":"object"}]}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"JOB_TYPE_NOT_FOUND"}},"security":[{"ApiKeyAuth":[]}],"summary":"Get a job type","tags":["job-types"]}},"/service-areas":{"get":{"description":"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.","operationId":"listServiceAreas","parameters":[{"description":"Page number (default 1)","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Items per page (default 15, max 1000)","in":"query","name":"limit","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/ServiceAreaList"}},"type":"object"}]}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"}},"security":[{"ApiKeyAuth":[]}],"summary":"List service areas","tags":["service-area"]}},"/service-areas/{id}":{"get":{"description":"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.","operationId":"getServiceArea","parameters":[{"description":"Service Area ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/ServiceArea"}},"type":"object"}]}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"SERVICE_AREA_NOT_FOUND"}},"security":[{"ApiKeyAuth":[]}],"summary":"Get a service area","tags":["service-area"]}},"/skill-categories":{"get":{"description":"Returns paginated skill categories — how the business groups technician qualifications by trade or specialty (e.g. HVAC, plumbing, electrical) — ordered alphabetically.","operationId":"listSkillCategories","parameters":[{"description":"Page number (default: 1)","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Page size (default: 15, max: 1000)","in":"query","name":"limit","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/SkillCategoryList"}},"type":"object"}]}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"WRONG_QUERY"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"}},"security":[{"ApiKeyAuth":[]}],"summary":"List skill categories","tags":["business-skill"]}},"/skill-categories/{id}/skills":{"get":{"description":"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.","operationId":"listSkillsByCategory","parameters":[{"description":"Skill category ID (UUID)","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Page number (default: 1)","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Page size (default: 15, max: 1000)","in":"query","name":"limit","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/SkillByCategoryList"}},"type":"object"}]}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"INVALID_REQUEST_BODY | WRONG_QUERY"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"SKILL_CATEGORY_NOT_FOUND"}},"security":[{"ApiKeyAuth":[]}],"summary":"List skills in a category","tags":["business-skill"]}},"/skills":{"get":{"description":"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.)","operationId":"listSkills","responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/SkillList"}},"type":"object"}]}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"}},"security":[{"ApiKeyAuth":[]}],"summary":"List all skills","tags":["business-skill"]}},"/technicians":{"get":{"description":"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.","operationId":"listTechnicians","parameters":[{"description":"Page number (default 1)","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Items per page (default 15, max 1000)","in":"query","name":"limit","schema":{"type":"integer"}},{"description":"Filter by status (active, onboarding, deactive)","in":"query","name":"status","schema":{"type":"string"}},{"description":"Filter by assignment tier (lead, buddy, float)","in":"query","name":"assignment_tier","schema":{"type":"string"}},{"description":"Search by full name, email, phone, or address","in":"query","name":"keyword","schema":{"type":"string"}},{"description":"Sort: created_at_desc|created_at_asc|name_asc|name_desc (default: created_at_desc)","in":"query","name":"sort","schema":{"type":"string"}},{"description":"RFC3339 cursor for short-polling; echo back next_since from prior response","in":"query","name":"since","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/TechnicianList"}},"type":"object"}]}}},"description":"OK"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"INVALID_REQUEST_BODY"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"}},"security":[{"ApiKeyAuth":[]}],"summary":"List technicians","tags":["technician"]}},"/technicians/{id}":{"get":{"description":"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.","operationId":"getTechnician","parameters":[{"description":"Technician ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/Technician"}},"type":"object"}]}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"TECHNICIAN_NOT_FOUND"}},"security":[{"ApiKeyAuth":[]}],"summary":"Get a technician","tags":["technician"]}},"/vehicles":{"get":{"description":"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.","operationId":"listVehicles","parameters":[{"description":"Page number (default 1)","in":"query","name":"page","schema":{"type":"integer"}},{"description":"Items per page (default 15, max 1000)","in":"query","name":"limit","schema":{"type":"integer"}},{"description":"Filter by status (inactive, idle, on_job, maintenance)","in":"query","name":"status","schema":{"type":"string"}},{"description":"Search by name, brand, model, or plate number","in":"query","name":"keyword","schema":{"type":"string"}},{"description":"RFC3339 cursor for short-polling; echo back next_since from prior response","in":"query","name":"since","schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/VehicleList"}},"type":"object"}]}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"}},"security":[{"ApiKeyAuth":[]}],"summary":"List vehicles","tags":["vehicle"]}},"/vehicles/{id}":{"get":{"description":"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.","operationId":"getVehicle","parameters":[{"description":"Vehicle ID","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ResponseEnvelope"},{"properties":{"data":{"$ref":"#/components/schemas/Vehicle"}},"type":"object"}]}}},"description":"OK"},"401":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"UNAUTHORIZED"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResponseEnvelope"}}},"description":"VEHICLE_NOT_FOUND"}},"security":[{"ApiKeyAuth":[]}],"summary":"Get a vehicle","tags":["vehicle"]}}},"servers":[{"url":"/v1"}],"x-tagGroups":[{"name":"Customers","tags":["customer"]},{"name":"Bookings","tags":["job-request-business"]},{"name":"Catalog","tags":["job-types","business-skill","service-area"]},{"name":"Team \u0026 Fleet","tags":["technician","vehicle"]}]}