{"openapi":"3.0.3","info":{"title":"CrewLink API","version":"0.1.0","description":"Schedule crews around the drive.\n\nField-service scheduling. Availability that knows every tech's jobs and the drive at that hour, short holds, bookings, and the office, field and partner work around them.\n\n**Auth.** Public booking-page endpoints need no key. Everything else takes `Authorization: Bearer <id>.<secret>`: the company API key (acts as the owner), a user token, or a session from email sign-in. Roles are owner, dispatcher and tech.\n\n**Conventions.** JSON in and out. Times come back as `start` (an ISO instant in UTC) and `local` (the company's wall clock, `YYYY-MM-DDTHH:mm`). Money is integer minor units plus a currency code. Errors are `{\"error\": \"<code>\", \"message\": \"<text>\"}`. Requests are rate limited per IP (429 with Retry-After).\n\n**Webhooks** deliver signed events for bookings, jobs, payments, messages and CrewConnect.\n\n**Guides** walk through each area with examples: [/docs/guides](/docs/guides). What changed, newest first: [the changelog](/docs/guides/changelog)."},"components":{"securitySchemes":{"officeKey":{"type":"http","scheme":"bearer","description":"<company id>.<secret> (the company API key, acts as owner), <user id>.<secret> (a user token: owner, field manager, dispatcher or tech), <session id>.<secret> (from email sign-in, 30 days) or pl_<platform id>.<secret> (a platform key: send X-CrewLink-Company with the id or slug of a company that granted it; acts as that owner within the grant's scopes, see [Platforms guide](/docs/guides/platforms))"},"integrationKey":{"type":"apiKey","in":"header","name":"X-CrewLink-Key","description":"Optional public integration key (pk_...), safe to embed in a website: grants no data access, only counts the holds, bookings, requests and address-search limits per key (300 a minute, 50 live holds) instead of per visitor IP. POST /v1/office/integration-key/rotate"}},"schemas":{}},"paths":{"/v1/public/{slug}":{"get":{"summary":"Company and its bookable services","tags":["Booking page"],"parameters":[{"schema":{"type":"string","minLength":1},"in":"path","name":"slug","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"company":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"timezone":{"type":"string","description":"The company's default zone; each time slot answer carries the zone of the branch serving that address"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 of the head office"},"units":{"type":"string","enum":["mi","km"],"description":"How to show distances (and per-distance fees) to this company's customers"}},"required":["name","slug","timezone","country","units"],"additionalProperties":false},"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"minutes":{"type":"number"},"peopleNeeded":{"type":"number"},"days":{"type":"number"},"priceCents":{"type":"number","nullable":true},"depositCents":{"type":"number"},"currency":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true,"description":"residential, commercial or other; null when the company has not set it"},"daylightOnly":{"type":"boolean","description":"true: only times when the whole visit is in daylight at the address are offered (start at or after sunrise, end at or before sunset)"}},"required":["id","name","minutes","peopleNeeded","days","priceCents","depositCents","currency","category","daylightOnly"],"additionalProperties":false}},"texts":{"type":"boolean","description":"Customer texts really go out on this server (an SMS provider is set up). When false, don't tell the homeowner \"we texted you\"."}},"required":["company","services","texts"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/{slug}/availability":{"post":{"summary":"Open start times at an address","tags":["Booking page"],"description":"Every start time a qualified tech can actually make, counting their other jobs, calendar and the drive at that hour. Nothing sooner than an hour from now.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"First local date, YYYY-MM-DD (default today)"},"days":{"type":"integer","minimum":1,"maximum":28,"default":14}},"required":["serviceId","lat","lng"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string","description":"IANA zone of `local`: the zone of the address (a branch whose area holds it gives its own; CrewLink's own notes)"},"slots":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string","format":"date-time","description":"The real instant (UTC)"},"local":{"type":"string","description":"Wall clock where the job is (the serving branch's time zone, `timezone`), e.g. '2026-10-12T14:30'"}},"required":["start","local"],"additionalProperties":false}},"outsideArea":{"type":"boolean","description":"The address is outside every service area: offer POST /v1/public/{slug}/requests instead"},"nearestKm":{"type":"number","nullable":true,"description":"When outsideArea: straight-line km to the nearest branch (show it in the company's `units`)"}},"required":["timezone","slots","outsideArea","nearestKm"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/{slug}/holds":{"post":{"summary":"Hold a start time while the customer finishes","tags":["Booking page"],"description":"Rechecks the time against the live schedule under a lock, assigns the best tech(s), and holds it for 5 minutes. 409 slot_taken if it's gone, or with allowQueue (and the customer) 202: the request goes to the office's intake queue as unscheduled and the customer is texted that a time is coming. Returns a holdSecret that booking the hold requires. One IP can have at most 3 live holds (429 too_many_holds) and make 30 hold requests a minute (429 rate_limited); with the company's integration key (X-CrewLink-Key) the key can have 50 live holds and make 300 a minute instead.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"start":{"type":"string","format":"date-time"},"allowQueue":{"type":"boolean","default":false,"description":"true: when the time is taken (or too soon), file the request for the office instead of answering 409. Needs `customer`. Answers 202 with status unscheduled; the customer is texted that the office will confirm a time"},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40},"email":{"type":"string","format":"email"}},"required":["name","phone"],"additionalProperties":false,"description":"With allowQueue: who to text"},"address":{"type":"string","maxLength":300,"description":"With allowQueue"},"notes":{"type":"string","maxLength":2000,"description":"With allowQueue"}},"required":["serviceId","lat","lng","start"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"security":[{},{"integrationKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"holdId":{"type":"number"},"holdSecret":{"type":"string","description":"Send it with the booking; only whoever made the hold can book it"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local"},"expiresAt":{"type":"string"}},"required":["holdId","holdSecret","start","local","timezone","expiresAt"],"additionalProperties":false}}}},"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"status":{"type":"string","enum":["unscheduled"]},"queued":{"type":"boolean","enum":[true]},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"description":"The time that was asked for"},"timezone":{"type":"string","description":"IANA zone of requested.local (the zone of the address, or of the techs sent)"},"reason":{"type":"string","description":"Why it couldn't be booked then, in words"},"siteId":{"type":"number","nullable":true,"description":"The site (property, per client company) it is at"}},"required":["jobId","status","queued","requested","timezone","reason"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/{slug}/bookings":{"post":{"summary":"Turn a hold into a booking","tags":["Booking page"],"description":"Needs the holdSecret returned with the hold (422 without it, 404 when it is wrong, so nobody else can take over a hold). Send geoPrecision (the address search's `precision`): a street- or area-level address is 422 approximate_address unless allowApproximate is true. Returns a manage token once. The customer uses it to view or cancel. 410 hold_expired if the hold ran out. 30 a minute per IP, or 300 per integration key (X-CrewLink-Key).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"holdId":{"type":"integer"},"holdSecret":{"type":"string","minLength":1,"maxLength":200,"description":"From the hold response"},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40},"email":{"type":"string","format":"email"}},"required":["name","phone"],"additionalProperties":false},"address":{"type":"string","maxLength":300},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"description":"The `precision` of the GET /v1/public/geocode match the address came from. street and locality (no house found) are refused with 422 approximate_address unless allowApproximate is true. Left out: not checked"},"allowApproximate":{"type":"boolean","default":false,"description":"true: the customer confirmed an approximate place (the street, not the house). Stored with the job"},"notes":{"type":"string","maxLength":2000},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200},"description":"Your ids for this job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" } (up to 10). Each (source, id) is unique per company among jobs that aren't cancelled: one already on another live job is refused with 409 duplicate_external_ref (jobId: that job); one whose every job is cancelled is free to reuse (the cancelled job keeps it). Find a job by any of them: GET /v1/office/jobs?externalSource=&externalRef= (the live job first, then the newest)"},"externalRef":{"type":"string","minLength":1,"maxLength":200,"description":"Shorthand for one externalRefs entry: { [externalSource]: externalRef }. Also the job's primary id (externalRef / externalSource on reads)"},"externalSource":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,99}$","description":"Whose id externalRef is. Default: the platform's slug for a platform key, `api` for other office keys, `booking_page` on the public endpoints (which may not use a granted platform's slug)"}},"required":["holdId","holdSecret","customer"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"security":[{},{"integrationKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"bookingId":{"type":"number"},"status":{"type":"string","enum":["booked"]},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local"},"techs":{"type":"array","items":{"type":"string"},"description":"First names only"},"manageToken":{"type":"string"},"deposit":{"type":"object","properties":{"amountCents":{"type":"number"},"currency":{"type":"string"},"provider":{"type":"string","description":"stripe, or fake (development and test servers: no money moves)"},"clientSecret":{"type":"string"},"publishableKey":{"type":"string","nullable":true,"description":"provider stripe: the Stripe publishable key to load Stripe.js with (null when the server has none: the office collects the deposit)"},"testPay":{"type":"boolean","description":"provider fake on a server with test payments on: POST /v1/bookings/{id}/deposit/test-pay marks it paid"}},"required":["amountCents","currency","provider","clientSecret","publishableKey","testPay"],"additionalProperties":false,"nullable":true,"description":"When the service takes a deposit: pay it with the provider's client-side SDK (see Deposits)"},"texted":{"type":"boolean","description":"A confirmation text was sent (an SMS provider is set up and the number is one we text). Only then tell the homeowner \"we texted you\"."}},"required":["bookingId","status","start","local","timezone","techs","manageToken","deposit","texted"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"jobId":{"type":"number","description":"duplicate_external_ref: the job that already has this externalRef"}},"required":["error","message"],"additionalProperties":false}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/bookings/{id}":{"get":{"summary":"A customer's booking, by manage token","tags":["Customer"],"description":"company is the name the customer knows the business by (for work shared through CrewConnect, the name the sending company chose). techs are first names, and empty when the business doesn't share who goes.","parameters":[{"schema":{"type":"string","minLength":10},"in":"query","name":"token","required":true},{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"bookingId":{"type":"number"},"status":{"type":"string"},"company":{"type":"string"},"slug":{"type":"string"},"service":{"type":"string"},"serviceId":{"type":"number"},"lat":{"type":"number"},"lng":{"type":"number"},"cancelCutoffHours":{"type":"number"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local (where the job is)"},"address":{"type":"string","nullable":true},"techs":{"type":"array","items":{"type":"string"}},"deposit":{"type":"object","properties":{"amountCents":{"type":"number"},"currency":{"type":"string"},"status":{"type":"string","enum":["due","paid","refunded"]},"provider":{"type":"string","nullable":true,"description":"stripe or fake; null when no online payment was started (the office collects the deposit)"},"clientSecret":{"type":"string","nullable":true,"description":"While due and payable online: confirm it with Stripe.js (provider stripe)"},"publishableKey":{"type":"string","nullable":true,"description":"provider stripe: the Stripe publishable key for Stripe.js; null when the server has none (the office collects the deposit)"},"testPay":{"type":"boolean","description":"provider fake on a server with test payments on: POST /v1/bookings/{id}/deposit/test-pay marks it paid"}},"required":["amountCents","currency","status","provider","clientSecret","publishableKey","testPay"],"additionalProperties":false,"nullable":true,"description":"The deposit the booking page took for this booking, or null when there isn't one"},"texts":{"type":"boolean","description":"Customer texts really go out on this server: only then say \"we texted you\" after a move or cancel"}},"required":["bookingId","status","company","slug","service","serviceId","lat","lng","cancelCutoffHours","start","local","timezone","address","techs","deposit","texts"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/bookings/{id}/deposit":{"get":{"summary":"The booking's deposit, checked with the payment provider","tags":["Customer"],"description":"The same as `deposit` on GET /v1/bookings/{id}, but while it's due and the provider is Stripe it first asks Stripe, so a payment the browser just confirmed shows as paid without waiting for the webhook (the homeowner gets the receipt text and you get deposit.paid either way, once). 404 when the link is wrong or there's no deposit. 30 a minute per IP.","parameters":[{"schema":{"type":"string","minLength":10},"in":"query","name":"token","required":true},{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"amountCents":{"type":"number"},"currency":{"type":"string"},"status":{"type":"string","enum":["due","paid","refunded"]},"provider":{"type":"string","nullable":true,"description":"stripe or fake; null when no online payment was started (the office collects the deposit)"},"clientSecret":{"type":"string","nullable":true,"description":"While due and payable online: confirm it with Stripe.js (provider stripe)"},"publishableKey":{"type":"string","nullable":true,"description":"provider stripe: the Stripe publishable key for Stripe.js; null when the server has none (the office collects the deposit)"},"testPay":{"type":"boolean","description":"provider fake on a server with test payments on: POST /v1/bookings/{id}/deposit/test-pay marks it paid"}},"required":["amountCents","currency","status","provider","clientSecret","publishableKey","testPay"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/bookings/{id}/cancel":{"post":{"summary":"Cancel a booking (company cutoff applies)","tags":["Customer"],"description":"Refused with 409 too_late if it's within the company's cancellation cutoff.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":10}},"required":["token"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"bookingId":{"type":"number"},"status":{"type":"string","enum":["cancelled"]}},"required":["bookingId","status"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/bookings/{id}/reschedule":{"post":{"summary":"Move a booking to another time","tags":["Customer"],"description":"Same rules as booking: rechecked under a lock against the live schedule. The tech(s) already on the booking stay if they still fit at the new time, else the best tech(s) then go. 409 slot_taken if it's not free, too_late inside the company's cutoff, too_soon under an hour from now.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":10},"start":{"type":"string","format":"date-time"}},"required":["token","start"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"bookingId":{"type":"number"},"status":{"type":"string"},"company":{"type":"string"},"slug":{"type":"string"},"service":{"type":"string"},"serviceId":{"type":"number"},"lat":{"type":"number"},"lng":{"type":"number"},"cancelCutoffHours":{"type":"number"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local (where the job is)"},"address":{"type":"string","nullable":true},"techs":{"type":"array","items":{"type":"string"}},"deposit":{"type":"object","properties":{"amountCents":{"type":"number"},"currency":{"type":"string"},"status":{"type":"string","enum":["due","paid","refunded"]},"provider":{"type":"string","nullable":true,"description":"stripe or fake; null when no online payment was started (the office collects the deposit)"},"clientSecret":{"type":"string","nullable":true,"description":"While due and payable online: confirm it with Stripe.js (provider stripe)"},"publishableKey":{"type":"string","nullable":true,"description":"provider stripe: the Stripe publishable key for Stripe.js; null when the server has none (the office collects the deposit)"},"testPay":{"type":"boolean","description":"provider fake on a server with test payments on: POST /v1/bookings/{id}/deposit/test-pay marks it paid"}},"required":["amountCents","currency","status","provider","clientSecret","publishableKey","testPay"],"additionalProperties":false,"nullable":true,"description":"The deposit the booking page took for this booking, or null when there isn't one"},"texts":{"type":"boolean","description":"Customer texts really go out on this server: only then say \"we texted you\" after a move or cancel"}},"required":["bookingId","status","company","slug","service","serviceId","lat","lng","cancelCutoffHours","start","local","timezone","address","techs","deposit","texts"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/schedule":{"get":{"summary":"Everyone's day","tags":["Office"],"description":"Each tech's own calendar day `date`, in their branch's time zone (`timezone`): with branches in several zones, a Denver tech's Oct 12 and a Chicago tech's Oct 12 are each their local day. Times are local wall clock in that zone. `client` filters the visits to one client company's jobs (its id), or to jobs with no client (`none`). Each visit says which client company it's for and who filed it. For people with a team or own scope, other people's jobs show as busy (`hidden: true`, service 'Busy', no customer, address or client).","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":true},{"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","enum":["none"]}]},"in":"query","name":"client","required":false,"description":"A client company id, or none"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of this tech's times (their branch's)"},"test":{"type":"boolean","description":"true: a test person (only takes jobs for test clients); shown with a Test badge"},"visits":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"end":{"type":"string"},"status":{"type":"string"},"service":{"type":"string"},"customer":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"sentBy":{"type":"string","nullable":true,"description":"CrewConnect: the company that sent this job"},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"unscheduled (no time yet: in the intake queue), scheduled, en_route, on_site, done or cancelled"},"overdue":{"type":"boolean","description":"The visit has ended and the job is not marked done"},"needsAttention":{"type":"boolean"},"attentionReason":{"type":"string","nullable":true},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"filedBy":{"type":"string","nullable":true,"description":"Who filed or booked it, e.g. \"Filed by Ann Lee · Globex Solar\""},"hidden":{"type":"boolean","description":"Outside your scope: shown as busy time only"},"homeownerTexts":{"type":"boolean","description":"false: this job sends the homeowner no texts or emails (its notify flag, the company's homeownerMessaging setting or the booking platform's grant)"},"outsideHours":{"type":"boolean","description":"true: booked outside the tech's working hours (an import honouring a time the old system confirmed)"},"test":{"type":"boolean","description":"true: the job is for a test client (false for hidden visits)"},"state":{"type":"string","enum":["held","scheduled","in_progress","overdue","completed"],"description":"held: a booking-page hold; scheduled; in_progress: the tech is on the way or on site; overdue: the job's last visit has ended and it isn't marked done; completed: marked done"}},"required":["jobId","start","end","status","service","customer","address","sentBy","jobStatus","overdue","needsAttention","attentionReason","client","filedBy","hidden","homeownerTexts","outsideHours","test","state"],"additionalProperties":false}},"blocks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"start":{"type":"string"},"end":{"type":"string"},"reason":{"type":"string","nullable":true},"source":{"type":"string"}},"required":["id","start","end","reason","source"],"additionalProperties":false}}},"required":["id","name","timezone","test","visits","blocks"],"additionalProperties":false}}},"required":["date","techs"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/me":{"get":{"summary":"Who I am and which company","tags":["Office"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"timezone":{"type":"string","description":"Default zone (a branch may have its own: GET /v1/office/branches)"},"country":{"type":"string","description":"ISO 3166-1 alpha-2"},"units":{"type":"string","enum":["mi","km"],"description":"Distance units to show (the company's choice, else its country's)"},"kind":{"type":"string"},"cancelCutoffHours":{"type":"number"},"onCall":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"}},"required":["start","end"],"additionalProperties":false,"nullable":true},"weeklyReportTo":{"type":"string","nullable":true},"allowedOrigins":{"type":"array","items":{"type":"string"},"description":"Websites that may call the public booking endpoints from the browser"},"integrationKey":{"type":"boolean","description":"Whether an integration key exists (the key itself is shown only when created)"},"homeownerMessaging":{"type":"string","enum":["on","off"],"description":"off: no job of this company texts or emails its homeowner (PATCH /v1/office/settings)"},"defaultTechRadiusMiles":{"type":"number","nullable":true,"description":"How far from home (miles) people without a branch are offered work when they have no maxTravelMiles (PATCH /v1/office/settings); null: 40 km"},"bufferMinutes":{"type":"integer","description":"Buffer between jobs: minutes kept clear before every visit, after the drive (parking, getting to the door, slack), between every two visits and from the day's start point; every way a job is placed plans with it. 0-60, default 10 (PATCH /v1/office/settings)"},"metricsFrom":{"type":"string","nullable":true,"description":"\"Count KPIs from\": a local date (YYYY-MM-DD); no KPI or metric counts anything before it (PATCH /v1/office/settings). null: no limit"},"productAnalytics":{"type":"string","enum":["on","off"],"description":"off: CrewLink's own product analytics records nothing about this company (PATCH /v1/office/settings)"},"availabilityManagedBy":{"type":"string","nullable":true,"description":"The integration that owns technician availability (e.g. acme), or null: CrewLink (GET/PUT /v1/office/availability-management)"},"availabilityManagedByName":{"type":"string","nullable":true,"description":"Its name, e.g. \"Acme\""},"locationRetentionDays":{"type":"integer","description":"Days techs' location trails are kept (7-365, default 90; PATCH /v1/office/settings)"},"shareLinkHost":{"type":"string","nullable":true,"description":"Where client share links are served (PATCH /v1/office/settings); null: the default"},"shareLinkMailFrom":{"type":"string","nullable":true,"description":"From address of share-link emails; null: the default"}},"required":["id","name","slug","timezone","country","units","kind","cancelCutoffHours","onCall","weeklyReportTo","allowedOrigins","integrationKey","homeownerMessaging","defaultTechRadiusMiles","bufferMinutes","metricsFrom","productAnalytics","availabilityManagedBy","availabilityManagedByName","locationRetentionDays","shareLinkHost","shareLinkMailFrom"],"additionalProperties":false},"role":{"type":"string","description":"owner, field_manager, dispatcher (techs use /v1/tech)"},"scope":{"type":"string","enum":["company","team","own"],"description":"Which jobs you see"},"teamLeadId":{"type":"string","nullable":true},"userId":{"type":"string","nullable":true},"now":{"type":"string","description":"Company-local wall clock, e.g. '2026-10-12T10:30'"},"superAdmin":{"type":"boolean","description":"CrewLink staff signed in as themselves: the Admin area (/v1/admin) is open to you"},"actingAs":{"type":"object","properties":{"adminUserId":{"type":"string"},"adminName":{"type":"string"},"expiresAt":{"type":"string","description":"When this act-as session ends (ISO instant)"}},"required":["adminUserId","adminName","expiresAt"],"additionalProperties":false,"nullable":true,"description":"Set when a super admin is acting as this company's owner (POST /v1/admin/companies/{id}/act-as)"}},"required":["company","role","scope","teamLeadId","userId","now","superAdmin","actingAs"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/calendar":{"get":{"summary":"Day-by-day totals for a range (month and week views)","tags":["Office"],"description":"Per day: jobs, booked vs working minutes, revenue at list price, the service mix, and jobs starting in each hour (7am to 6pm). Up to 62 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"jobs":{"type":"number"},"bookedMinutes":{"type":"number"},"workMinutes":{"type":"number"},"revenueCents":{"type":"number"},"mix":{"type":"object","additionalProperties":{"type":"number"}},"hours":{"type":"array","items":{"type":"number"}}},"required":["date","jobs","bookedMinutes","workMinutes","revenueCents","mix","hours"],"additionalProperties":false}}},"required":["days"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team":{"get":{"summary":"Everyone on the team","tags":["Office"],"description":"email finds the person with that email (people.email) or whose sign-in user has it, ignoring case; externalSource / externalRef find people by an integrator's id (either alone works too). Platform keys see names as first name and last initial.","parameters":[{"schema":{"type":"string","format":"email"},"in":"query","name":"email","required":false,"description":"Their email (POST/PATCH /v1/office/team), or their user's (POST /v1/office/users with personId)"},{"schema":{"type":"string","maxLength":100},"in":"query","name":"externalSource","required":false},{"schema":{"type":"string","maxLength":200},"in":"query","name":"externalRef","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"people":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"kind":{"type":"string"},"designation":{"type":"string","enum":["w2","1099"],"description":"Tax designation: w2 (W-2 employee, kind employee) or 1099 (1099 contractor, kind contractor). Scheduling treats both the same"},"branch":{"type":"string","nullable":true},"branchId":{"type":"string","nullable":true},"home":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true,"description":"Home address (where `home` is)"},"skills":{"type":"number"},"skillsAll":{"type":"boolean","description":"true: qualified for every active service, now and later (skills \"all\"); `skills` follows the services"},"hours":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}],"nullable":true},"minItems":7,"maxItems":7,"description":"Monday first: [start, end] minutes after midnight, or null when off. With several windows a day, each day's first start to last end"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"minItems":7,"maxItems":7,"description":"Monday first: every window worked that day, [start, end] minutes after midnight; [] when off"},"pendingChange":{"type":"object","properties":{"from":{"type":"string","description":"Local date it takes effect"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"nullable":true},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true},"home":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false,"nullable":true}},"required":["from","availability","address","home"],"additionalProperties":false,"nullable":true,"description":"A change the tech made in the field app, in force from `from`"},"maxTravelMiles":{"type":"number","nullable":true,"description":"Never offered work farther than this from home (straight line); null: no personal limit"},"verified":{"type":"boolean"},"taxId":{"type":"string","nullable":true,"description":"Masked: the last 4 only"},"phone":{"type":"string","nullable":true},"email":{"type":"string","nullable":true,"description":"Their email (lower case), for a field-app invite (POST /v1/office/team/invite-all). Office and platform callers only; never on customer pages"},"hourlyCostCents":{"type":"number","nullable":true},"active":{"type":"boolean"},"timezone":{"type":"string","description":"IANA zone of their hours, day and jobs: their own (ownTimezone) when set, else their branch's, else (no branch) the zone of their home, else the company's"},"ownTimezone":{"type":"string","nullable":true,"description":"The zone set on them (POST/PATCH timezone); null: it follows their branch or home"},"hoursSource":{"type":"string","enum":["default_recent_activity"],"nullable":true,"description":"default_recent_activity: CrewLink gave them the default week (Monday to Friday 8:00 to 17:00 on their clock) because they had no hours and completed a job lately; any hours set later replace it. null: set by the office, the API or the tech"},"serviceRadiusMiles":{"type":"number","nullable":true,"description":"Without a branch: how far from home (or that day's base) they are offered work, miles (maxTravelMiles, else the company's defaultTechRadiusMiles, else 40 km); null with a branch (the branch's area)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Integrators' ids for this person, by source"},"test":{"type":"boolean","description":"true: a test person (PATCH /v1/office/team/{id} { test }): only offered or booked for jobs of a test client, never anyone else's, and left out of KPIs, the Wins leaderboard and pay reports by default"},"availabilityManagedBy":{"type":"string","nullable":true,"description":"The slug of the integration that owns this person's weekly hours, time off, home, travel radius and bases (e.g. acme), or null: managed in CrewLink. While set, only that integration's key changes them; everyone else gets 409 managed_by_integration ([Platforms guide](/docs/guides/platforms#owning-technician-availability-from-your-system))"},"availabilityManagedByName":{"type":"string","nullable":true,"description":"Its name, e.g. \"Acme\"; null when managed in CrewLink"},"ownAvailabilityManagedBy":{"type":"string","nullable":true,"description":"This person's own setting (PATCH /v1/office/team/{id} availabilityManagedBy): null follows the company; crewlink: managed in CrewLink even when the company's is an integration; a platform slug"},"startsFrom":{"type":"object","properties":{"kind":{"type":"string","enum":["home","branch","none"],"description":"home: their home; branch: their branch's location (they were added without a home of their own, or their home isn't a real place); none: neither, so they are never offered or auto-booked until a home or a branch location is set"},"lat":{"type":"number","nullable":true},"lng":{"type":"number","nullable":true},"address":{"type":"string","nullable":true,"description":"The address on file for that place, one line"},"branchId":{"type":"string","nullable":true,"description":"kind branch: which branch"},"branchName":{"type":"string","nullable":true},"text":{"type":"string","description":"In words, e.g. \"Home (12 Elm St, Chicago)\" or \"Branch North (4544 N Lincoln Ave, Chicago, IL)\""}},"required":["kind","lat","lng","address","branchId","branchName","text"],"additionalProperties":false,"description":"Where their working day starts and ends on a normal day (a trip or base day starts at its base). The first job is offered only when shift start + the drive from here at that time of day + the buffer fit before it"}},"required":["id","name","kind","designation","branch","branchId","home","address","skills","skillsAll","hours","availability","pendingChange","maxTravelMiles","verified","taxId","phone","email","hourlyCostCents","active","timezone","ownTimezone","hoursSource","serviceRadiusMiles","externalRefs","test","availabilityManagedBy","availabilityManagedByName","ownAvailabilityManagedBy","startsFrom"],"additionalProperties":false}}},"required":["people"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"post":{"summary":"Add a person to the team","tags":["Office"],"description":"skills \"all\" (the default for platform keys when skills is left out; 1 otherwise): qualified for every active service, kept in step when services are added or changed later (skillsAll on reads). homeLat/homeLng may be left out with a branchId: home is then the branch's location (422 with neither). They're offered for work from now on: within the branch's service area (without a branch: within maxTravelMiles of home, else the company's defaultTechRadiusMiles, else 40 km), during their hours (on their zone's clock: `timezone` if sent, else the branch's, else their home's), for the services their skills cover. hours and availability are optional: someone added without either has no working hours (every day off), and a tech with no hours is never offered until hours are set (PATCH /v1/office/team/{id} or PUT /v1/office/team/{id}/availability). There is no default schedule. To let them sign in to the field app, add a tech user with their id (POST /v1/office/users).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"branchId":{"type":"string","format":"uuid","description":"Their branch: sets the service area they cover (and their home when homeLat/homeLng are left out)"},"homeLat":{"type":"number","minimum":-90,"maximum":90,"description":"Required unless branchId is sent (then the branch location)"},"homeLng":{"type":"number","minimum":-180,"maximum":180,"description":"With homeLat"},"address":{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":200},"line2":{"type":"string","maxLength":200,"nullable":true},"city":{"type":"string","minLength":1,"maxLength":120},"region":{"type":"string","maxLength":120,"nullable":true,"description":"State, province or region"},"postalCode":{"type":"string","maxLength":20,"nullable":true},"country":{"type":"string","maxLength":60,"nullable":true}},"required":["line1","city"],"additionalProperties":false,"description":"Their home address (homeLat/homeLng is where it is)"},"skills":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","enum":["all"]}],"description":"Bit mask matched against services.skills, or \"all\": every active service, now and later. Default: \"all\" for platform keys, 1 otherwise"},"hours":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"integer","minimum":0,"maximum":1440},{"type":"integer","minimum":0,"maximum":1440}],"nullable":true},"minItems":7,"maxItems":7,"description":"One window a day (or availability). Neither: no working hours, never offered until set"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"integer","minimum":0,"maximum":1440},{"type":"integer","minimum":0,"maximum":1440}]},"maxItems":8},"minItems":7,"maxItems":7,"description":"Several windows a day (or hours). Neither: no working hours, never offered until set"},"kind":{"type":"string","enum":["employee","contractor"],"default":"employee"},"designation":{"type":"string","enum":["w2","1099"],"description":"Same as kind: w2 = employee, 1099 = contractor"},"phone":{"type":"string","minLength":7,"maxLength":40,"description":"Mobile, for masked calls"},"email":{"type":"string","format":"email","maxLength":200,"description":"For a field-app invite (POST /v1/office/team/invite-all)"},"hourlyCostCents":{"type":"integer","minimum":0,"maximum":1000000,"description":"Loaded labor cost per hour, for job P&L"},"maxTravelMiles":{"type":"number","minimum":1,"maximum":3000,"description":"Never offered work farther than this from home. Without a branch it is also their service radius"},"timezone":{"type":"string","maxLength":64,"description":"IANA zone of their hours (e.g. 'America/New_York'). Default: their branch's, else (no branch) their home's, else the company's. 422 for an unknown zone"},"verified":{"type":"boolean"},"taxId":{"type":"string","maxLength":40,"description":"SSN or EIN; stored encrypted, shown masked"},"test":{"type":"boolean","description":"true: a test person from the start (only offered or booked for jobs of a test client; as PATCH /v1/office/team/{id} { test }). Default false"},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200,"nullable":true},"description":"Your ids by source, e.g. { \"acme\": \"tech_42\" }; each (source, id) is unique in the company (409 duplicate_external_ref). On PATCH a null id removes that source"},"externalRef":{"type":"string","minLength":1,"maxLength":200,"description":"Shorthand for one externalRefs entry"},"externalSource":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,99}$","description":"Default: the platform's slug for a platform key, else `api`"}},"required":["name"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"kind":{"type":"string"},"designation":{"type":"string","enum":["w2","1099"],"description":"Tax designation: w2 (W-2 employee, kind employee) or 1099 (1099 contractor, kind contractor). Scheduling treats both the same"},"branch":{"type":"string","nullable":true},"branchId":{"type":"string","nullable":true},"home":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true,"description":"Home address (where `home` is)"},"skills":{"type":"number"},"skillsAll":{"type":"boolean","description":"true: qualified for every active service, now and later (skills \"all\"); `skills` follows the services"},"hours":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}],"nullable":true},"minItems":7,"maxItems":7,"description":"Monday first: [start, end] minutes after midnight, or null when off. With several windows a day, each day's first start to last end"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"minItems":7,"maxItems":7,"description":"Monday first: every window worked that day, [start, end] minutes after midnight; [] when off"},"pendingChange":{"type":"object","properties":{"from":{"type":"string","description":"Local date it takes effect"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"nullable":true},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true},"home":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false,"nullable":true}},"required":["from","availability","address","home"],"additionalProperties":false,"nullable":true,"description":"A change the tech made in the field app, in force from `from`"},"maxTravelMiles":{"type":"number","nullable":true,"description":"Never offered work farther than this from home (straight line); null: no personal limit"},"verified":{"type":"boolean"},"taxId":{"type":"string","nullable":true,"description":"Masked: the last 4 only"},"phone":{"type":"string","nullable":true},"email":{"type":"string","nullable":true,"description":"Their email (lower case), for a field-app invite (POST /v1/office/team/invite-all). Office and platform callers only; never on customer pages"},"hourlyCostCents":{"type":"number","nullable":true},"active":{"type":"boolean"},"timezone":{"type":"string","description":"IANA zone of their hours, day and jobs: their own (ownTimezone) when set, else their branch's, else (no branch) the zone of their home, else the company's"},"ownTimezone":{"type":"string","nullable":true,"description":"The zone set on them (POST/PATCH timezone); null: it follows their branch or home"},"hoursSource":{"type":"string","enum":["default_recent_activity"],"nullable":true,"description":"default_recent_activity: CrewLink gave them the default week (Monday to Friday 8:00 to 17:00 on their clock) because they had no hours and completed a job lately; any hours set later replace it. null: set by the office, the API or the tech"},"serviceRadiusMiles":{"type":"number","nullable":true,"description":"Without a branch: how far from home (or that day's base) they are offered work, miles (maxTravelMiles, else the company's defaultTechRadiusMiles, else 40 km); null with a branch (the branch's area)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Integrators' ids for this person, by source"},"test":{"type":"boolean","description":"true: a test person (PATCH /v1/office/team/{id} { test }): only offered or booked for jobs of a test client, never anyone else's, and left out of KPIs, the Wins leaderboard and pay reports by default"},"availabilityManagedBy":{"type":"string","nullable":true,"description":"The slug of the integration that owns this person's weekly hours, time off, home, travel radius and bases (e.g. acme), or null: managed in CrewLink. While set, only that integration's key changes them; everyone else gets 409 managed_by_integration ([Platforms guide](/docs/guides/platforms#owning-technician-availability-from-your-system))"},"availabilityManagedByName":{"type":"string","nullable":true,"description":"Its name, e.g. \"Acme\"; null when managed in CrewLink"},"ownAvailabilityManagedBy":{"type":"string","nullable":true,"description":"This person's own setting (PATCH /v1/office/team/{id} availabilityManagedBy): null follows the company; crewlink: managed in CrewLink even when the company's is an integration; a platform slug"},"startsFrom":{"type":"object","properties":{"kind":{"type":"string","enum":["home","branch","none"],"description":"home: their home; branch: their branch's location (they were added without a home of their own, or their home isn't a real place); none: neither, so they are never offered or auto-booked until a home or a branch location is set"},"lat":{"type":"number","nullable":true},"lng":{"type":"number","nullable":true},"address":{"type":"string","nullable":true,"description":"The address on file for that place, one line"},"branchId":{"type":"string","nullable":true,"description":"kind branch: which branch"},"branchName":{"type":"string","nullable":true},"text":{"type":"string","description":"In words, e.g. \"Home (12 Elm St, Chicago)\" or \"Branch North (4544 N Lincoln Ave, Chicago, IL)\""}},"required":["kind","lat","lng","address","branchId","branchName","text"],"additionalProperties":false,"description":"Where their working day starts and ends on a normal day (a trip or base day starts at its base). The first job is offered only when shift start + the drive from here at that time of day + the buffer fit before it"}},"required":["id","name","kind","designation","branch","branchId","home","address","skills","skillsAll","hours","availability","pendingChange","maxTravelMiles","verified","taxId","phone","email","hourlyCostCents","active","timezone","ownTimezone","hoursSource","serviceRadiusMiles","externalRefs","test","availabilityManagedBy","availabilityManagedByName","ownAvailabilityManagedBy","startsFrom"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"personId":{"type":"number"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}":{"get":{"summary":"One person on the team","tags":["Office"],"description":"The same object as an item of GET /v1/office/team, with the same rules: your company's people only (404 for anyone else, including another company's), and platform keys see the name as first name and last initial unless their grant here says full names. Changes the tech made in the field app that are due are applied first, as on the list.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"kind":{"type":"string"},"designation":{"type":"string","enum":["w2","1099"],"description":"Tax designation: w2 (W-2 employee, kind employee) or 1099 (1099 contractor, kind contractor). Scheduling treats both the same"},"branch":{"type":"string","nullable":true},"branchId":{"type":"string","nullable":true},"home":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true,"description":"Home address (where `home` is)"},"skills":{"type":"number"},"skillsAll":{"type":"boolean","description":"true: qualified for every active service, now and later (skills \"all\"); `skills` follows the services"},"hours":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}],"nullable":true},"minItems":7,"maxItems":7,"description":"Monday first: [start, end] minutes after midnight, or null when off. With several windows a day, each day's first start to last end"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"minItems":7,"maxItems":7,"description":"Monday first: every window worked that day, [start, end] minutes after midnight; [] when off"},"pendingChange":{"type":"object","properties":{"from":{"type":"string","description":"Local date it takes effect"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"nullable":true},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true},"home":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false,"nullable":true}},"required":["from","availability","address","home"],"additionalProperties":false,"nullable":true,"description":"A change the tech made in the field app, in force from `from`"},"maxTravelMiles":{"type":"number","nullable":true,"description":"Never offered work farther than this from home (straight line); null: no personal limit"},"verified":{"type":"boolean"},"taxId":{"type":"string","nullable":true,"description":"Masked: the last 4 only"},"phone":{"type":"string","nullable":true},"email":{"type":"string","nullable":true,"description":"Their email (lower case), for a field-app invite (POST /v1/office/team/invite-all). Office and platform callers only; never on customer pages"},"hourlyCostCents":{"type":"number","nullable":true},"active":{"type":"boolean"},"timezone":{"type":"string","description":"IANA zone of their hours, day and jobs: their own (ownTimezone) when set, else their branch's, else (no branch) the zone of their home, else the company's"},"ownTimezone":{"type":"string","nullable":true,"description":"The zone set on them (POST/PATCH timezone); null: it follows their branch or home"},"hoursSource":{"type":"string","enum":["default_recent_activity"],"nullable":true,"description":"default_recent_activity: CrewLink gave them the default week (Monday to Friday 8:00 to 17:00 on their clock) because they had no hours and completed a job lately; any hours set later replace it. null: set by the office, the API or the tech"},"serviceRadiusMiles":{"type":"number","nullable":true,"description":"Without a branch: how far from home (or that day's base) they are offered work, miles (maxTravelMiles, else the company's defaultTechRadiusMiles, else 40 km); null with a branch (the branch's area)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Integrators' ids for this person, by source"},"test":{"type":"boolean","description":"true: a test person (PATCH /v1/office/team/{id} { test }): only offered or booked for jobs of a test client, never anyone else's, and left out of KPIs, the Wins leaderboard and pay reports by default"},"availabilityManagedBy":{"type":"string","nullable":true,"description":"The slug of the integration that owns this person's weekly hours, time off, home, travel radius and bases (e.g. acme), or null: managed in CrewLink. While set, only that integration's key changes them; everyone else gets 409 managed_by_integration ([Platforms guide](/docs/guides/platforms#owning-technician-availability-from-your-system))"},"availabilityManagedByName":{"type":"string","nullable":true,"description":"Its name, e.g. \"Acme\"; null when managed in CrewLink"},"ownAvailabilityManagedBy":{"type":"string","nullable":true,"description":"This person's own setting (PATCH /v1/office/team/{id} availabilityManagedBy): null follows the company; crewlink: managed in CrewLink even when the company's is an integration; a platform slug"},"startsFrom":{"type":"object","properties":{"kind":{"type":"string","enum":["home","branch","none"],"description":"home: their home; branch: their branch's location (they were added without a home of their own, or their home isn't a real place); none: neither, so they are never offered or auto-booked until a home or a branch location is set"},"lat":{"type":"number","nullable":true},"lng":{"type":"number","nullable":true},"address":{"type":"string","nullable":true,"description":"The address on file for that place, one line"},"branchId":{"type":"string","nullable":true,"description":"kind branch: which branch"},"branchName":{"type":"string","nullable":true},"text":{"type":"string","description":"In words, e.g. \"Home (12 Elm St, Chicago)\" or \"Branch North (4544 N Lincoln Ave, Chicago, IL)\""}},"required":["kind","lat","lng","address","branchId","branchName","text"],"additionalProperties":false,"description":"Where their working day starts and ends on a normal day (a trip or base day starts at its base). The first job is offered only when shift start + the drive from here at that time of day + the buffer fit before it"}},"required":["id","name","kind","designation","branch","branchId","home","address","skills","skillsAll","hours","availability","pendingChange","maxTravelMiles","verified","taxId","phone","email","hourlyCostCents","active","timezone","ownTimezone","hoursSource","serviceRadiusMiles","externalRefs","test","availabilityManagedBy","availabilityManagedByName","ownAvailabilityManagedBy","startsFrom"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"patch":{"summary":"Change someone's details, hours, skills or status","tags":["Office"],"description":"New hours apply to times offered from now on. Jobs already booked stay put; the response lists the ones now outside the person's hours so the office can move them. availability sets several windows a day (hours then reads each day's first start to last end); hours sets one window a day. A home move is homeLat/homeLng with the new address. designation is the tax designation (w2 or 1099; the same as kind). taxId is stored encrypted and only ever shown masked; null removes it. maxTravelMiles: the person is never offered work farther than this from home; null removes the limit. Changing hours or home here replaces a change the tech made in the field app that hasn't taken effect yet.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"branchId":{"type":"string","format":"uuid","nullable":true,"description":"null: no branch (they cover maxTravelMiles, else the company's defaultTechRadiusMiles, else 40 km, around home, on their home's clock)"},"homeLat":{"type":"number","minimum":-90,"maximum":90,"description":"With homeLng"},"homeLng":{"type":"number","minimum":-180,"maximum":180,"description":"With homeLat"},"address":{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":200},"line2":{"type":"string","maxLength":200,"nullable":true},"city":{"type":"string","minLength":1,"maxLength":120},"region":{"type":"string","maxLength":120,"nullable":true,"description":"State, province or region"},"postalCode":{"type":"string","maxLength":20,"nullable":true},"country":{"type":"string","maxLength":60,"nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true,"description":"Home address; send homeLat/homeLng with it when they moved"},"kind":{"type":"string","enum":["employee","contractor"]},"designation":{"type":"string","enum":["w2","1099"],"description":"w2 (employee) or 1099 (contractor): the same as kind"},"hours":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"integer","minimum":0,"maximum":1440},{"type":"integer","minimum":0,"maximum":1440}],"nullable":true},"minItems":7,"maxItems":7,"description":"Monday first: [start, end] minutes after midnight (0 to 1440, start before end), or null when off"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"integer","minimum":0,"maximum":1440},{"type":"integer","minimum":0,"maximum":1440}]},"maxItems":8},"minItems":7,"maxItems":7,"description":"Monday first: each day a list of [start, end] windows, minutes after midnight (at most 8, not overlapping); [] when off"},"skills":{"anyOf":[{"type":"integer","minimum":0},{"type":"string","enum":["all"]}],"description":"A bit mask (turns skillsAll off), or \"all\": every active service, now and later"},"active":{"type":"boolean"},"phone":{"type":"string","minLength":7,"maxLength":40,"description":"Mobile, for masked calls"},"email":{"type":"string","format":"email","maxLength":200,"nullable":true,"description":"For a field-app invite (POST /v1/office/team/invite-all); null removes it. Their sign-in email is on their user"},"hourlyCostCents":{"type":"integer","minimum":0,"maximum":1000000,"nullable":true,"description":"Loaded labor cost per hour, for job P&L; null: the default"},"maxTravelMiles":{"type":"number","minimum":1,"maximum":3000,"nullable":true},"timezone":{"type":"string","maxLength":64,"nullable":true,"description":"IANA zone of their hours; null: follow their branch, else their home. When their zone changes (this, a home move or a branch change), their booked jobs and time off are re-expressed on the new clock keeping the same instants (rezoned)"},"verified":{"type":"boolean"},"taxId":{"type":"string","maxLength":40,"nullable":true,"description":"SSN or EIN; stored encrypted, shown masked; null removes it"},"test":{"type":"boolean","description":"true: a test person: only offered or booked for jobs of a test client (PATCH /v1/office/clients/{id} { test }), never anyone else's, and left out of KPIs, the Wins leaderboard and pay reports by default. false: a real tech again. Jobs already booked stay put. Platform keys may set it"},"availabilityManagedBy":{"type":"string","maxLength":64,"nullable":true,"description":"Who owns this person's availability, overriding the company's (GET /v1/office/availability-management): null follows the company; crewlink: managed in CrewLink; a platform slug with a live grant here. Owners may set any; a platform key only its own slug or null"},"force":{"type":"boolean","description":"Owners only: change hours, home, timezone or maxTravelMiles although an integration owns this person's availability (recorded). Ignored when nobody else owns it"},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200,"nullable":true},"description":"Your ids by source, e.g. { \"acme\": \"tech_42\" }; each (source, id) is unique in the company (409 duplicate_external_ref). On PATCH a null id removes that source"},"externalRef":{"type":"string","minLength":1,"maxLength":200,"description":"Shorthand for one externalRefs entry"},"externalSource":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,99}$","description":"Default: the platform's slug for a platform key, else `api`"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"outsideHours":{"type":"array","items":{"type":"number"},"description":"Job ids"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"}},"timezone":{"type":"string","description":"Their zone now"},"rezoned":{"type":"object","properties":{"jobs":{"type":"number"},"blocks":{"type":"number"}},"required":["jobs","blocks"],"additionalProperties":false,"description":"Booked jobs and time-off blocks moved onto their new clock (zero when the zone did not change)"}},"required":["id","outsideHours","externalRefs","timezone","rezoned"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"managedBy":{"type":"string","description":"managed_by_integration: the slug of the platform that owns it, e.g. acme"},"managedByName":{"type":"string","description":"Its name, e.g. Your platform"},"personId":{"type":"number"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/availability-management":{"get":{"summary":"Who owns this company's technician availability","tags":["Office"],"description":"null: CrewLink (the office and techs edit hours here). A platform slug: that integration owns weekly hours, time off, home, travel radius and bases ([Platforms guide](/docs/guides/platforms#owning-technician-availability-from-your-system)). Owners also get the audit of overrides.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"availabilityManagedBy":{"type":"string","nullable":true,"description":"The slug of the integration that owns this company's technician availability (e.g. acme), or null: managed in CrewLink"},"availabilityManagedByName":{"type":"string","nullable":true},"people":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"availabilityManagedBy":{"type":"string","nullable":true,"description":"Their own setting: crewlink or a platform slug"}},"required":["personId","availabilityManagedBy"],"additionalProperties":false},"description":"People whose own setting differs from following the company"},"overrides":{"type":"array","items":{"type":"object","properties":{"at":{"type":"string"},"action":{"type":"string","description":"availability_force (an owner overrode the integration) or availability_managed_by (the owner changed)"},"actor":{"type":"string","description":"user:<id>, api_key, admin:<id> or platform:<slug>"},"personId":{"type":"number","nullable":true},"method":{"type":"string","nullable":true},"path":{"type":"string","nullable":true},"detail":{"type":"object","additionalProperties":{}}},"required":["at","action","actor","personId","method","path","detail"],"additionalProperties":false},"description":"The latest 50 audit rows, newest first (owners only; [] for others)"}},"required":["availabilityManagedBy","availabilityManagedByName","people","overrides"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"put":{"summary":"Set who owns this company's technician availability","tags":["Office"],"description":"managedBy: a platform slug with a live grant on this company (e.g. acme), or null for CrewLink. Owners (not dispatchers) may set any granted platform or null. A platform key may claim it for itself (its own slug) or release it (null) when it is the owner; it can't hand it to another platform. While set, PUT /v1/office/team/{id}/availability, PATCH /v1/office/team/{id} (hours, availability, home, address, timezone, maxTravelMiles), POST/DELETE /v1/office/blocks, POST/DELETE /v1/office/team/{id}/bases and the field app's PATCH /v1/tech/profile are refused with 409 managed_by_integration for every caller except that platform (an owner may send force: true for one request; recorded). Blocks synced from linked Google or Outlook calendars keep working. Booking is unchanged. Recorded in the audit (GET).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"managedBy":{"type":"string","maxLength":64,"nullable":true}},"required":["managedBy"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"availabilityManagedBy":{"type":"string","nullable":true,"description":"The slug of the integration that owns this company's technician availability (e.g. acme), or null: managed in CrewLink"},"availabilityManagedByName":{"type":"string","nullable":true},"people":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"availabilityManagedBy":{"type":"string","nullable":true,"description":"Their own setting: crewlink or a platform slug"}},"required":["personId","availabilityManagedBy"],"additionalProperties":false},"description":"People whose own setting differs from following the company"},"overrides":{"type":"array","items":{"type":"object","properties":{"at":{"type":"string"},"action":{"type":"string","description":"availability_force (an owner overrode the integration) or availability_managed_by (the owner changed)"},"actor":{"type":"string","description":"user:<id>, api_key, admin:<id> or platform:<slug>"},"personId":{"type":"number","nullable":true},"method":{"type":"string","nullable":true},"path":{"type":"string","nullable":true},"detail":{"type":"object","additionalProperties":{}}},"required":["at","action","actor","personId","method","path","detail"],"additionalProperties":false},"description":"The latest 50 audit rows, newest first (owners only; [] for others)"}},"required":["availabilityManagedBy","availabilityManagedByName","people","overrides"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}/credentials":{"post":{"summary":"Add a license, insurance or certification for someone","tags":["Office"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["license","insurance","certification","background_check"]},"name":{"type":"string","minLength":1,"maxLength":200},"number":{"type":"string","maxLength":100},"issuer":{"type":"string","maxLength":200},"expiresOn":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"documentId":{"type":"string","format":"uuid","description":"One of this person's documents: the scanned card or certificate"}},"required":["kind","name","expiresOn"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"personId":{"type":"number"},"person":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"number":{"type":"string","nullable":true},"issuer":{"type":"string","nullable":true},"expiresOn":{"type":"string"},"daysLeft":{"type":"number"},"documentId":{"type":"string","nullable":true,"description":"The uploaded copy (GET /v1/office/team/{id}/documents)"}},"required":["id","personId","person","kind","name","number","issuer","expiresOn","daysLeft","documentId"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/credentials":{"get":{"summary":"Credentials, soonest to expire first","tags":["Office"],"description":"expiringWithin=30 lists only those expiring in the next 30 days (or already expired).","parameters":[{"schema":{"type":"integer","minimum":0,"maximum":3650},"in":"query","name":"expiringWithin","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"credentials":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"personId":{"type":"number"},"person":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"number":{"type":"string","nullable":true},"issuer":{"type":"string","nullable":true},"expiresOn":{"type":"string"},"daysLeft":{"type":"number"},"documentId":{"type":"string","nullable":true,"description":"The uploaded copy (GET /v1/office/team/{id}/documents)"}},"required":["id","personId","person","kind","name","number","issuer","expiresOn","daysLeft","documentId"],"additionalProperties":false}}},"required":["credentials"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/credentials/{id}":{"delete":{"summary":"Remove a credential","tags":["Office"],"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}},"required":["deleted"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/blocks":{"post":{"summary":"Block time on someone’s calendar (time off, training, a personal appointment)","tags":["Office"],"description":"Nothing new is offered in the block. Jobs already booked inside it are returned as conflicts and left in place for the office to move. start and end in the response are the person's wall clock (their zone, `timezone`). Up to 60 days per block. externalRefs (or externalRef): sending the same id again is 409 duplicate_external_ref with the existing blockId, never a second block. 409 managed_by_integration when an integration owns this person's availability and you aren't it (owners: force: true, recorded). Sends block.created (with source).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"personId":{"type":"integer"},"start":{"type":"string","format":"date-time"},"end":{"type":"string","format":"date-time"},"reason":{"type":"string","maxLength":200},"force":{"type":"boolean","description":"Owners only: add it although an integration owns this person's availability (recorded)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200,"nullable":true},"description":"Your ids by source, e.g. { \"acme\": \"tech_42\" }; each (source, id) is unique in the company (409 duplicate_external_ref). On PATCH a null id removes that source"},"externalRef":{"type":"string","minLength":1,"maxLength":200,"description":"Shorthand for one externalRefs entry"},"externalSource":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,99}$","description":"Default: the platform's slug for a platform key, else `api`"}},"required":["personId","start","end"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"personId":{"type":"number"},"start":{"type":"string"},"end":{"type":"string"},"timezone":{"type":"string","description":"The person's zone"},"reason":{"type":"string","nullable":true},"conflicts":{"type":"array","items":{"type":"number"},"description":"Booked job ids that overlap; move or reassign them"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Integrators' ids for this block, by source"}},"required":["id","personId","start","end","timezone","reason","conflicts","externalRefs"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"managedBy":{"type":"string","description":"managed_by_integration: the slug of the platform that owns it, e.g. acme"},"managedByName":{"type":"string","description":"Its name, e.g. Your platform"},"blockId":{"type":"number","description":"duplicate_external_ref: the block that already has that id"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"Blocked time in a date range (time off, and busy time from linked calendars)","tags":["Office"],"description":"Every block that overlaps from..to (local dates, inclusive; default today and the 30 days after), earliest first (then by id), with the person's name. source is office for blocks made here (by the office, an API key or a platform), or google / outlook for busy time imported from a linked calendar. Remove one with DELETE /v1/office/blocks/{id}; an imported block comes back at the next calendar sync while the event is still in that calendar. At most 92 days per call (from..to inclusive): a longer range, or to before from, is 422 invalid_request (\"Up to 92 days, from on or before to.\"). There is no cursor: each call returns every block in its range. To read further, page by date: call again with the next window (from = the day after the last to); a block that spans two windows comes back in both, so de-duplicate by id. externalSource/externalRef finds blocks by an integrator's id on any date (no range limit). personId narrows to one person.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false},{"schema":{"type":"integer"},"in":"query","name":"personId","required":false,"description":"Only this person"},{"schema":{"type":"string","maxLength":100},"in":"query","name":"externalSource","required":false,"description":"With externalRef (or alone): blocks with that integrator id, whatever their dates"},{"schema":{"type":"string","maxLength":200},"in":"query","name":"externalRef","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"blocks":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"personId":{"type":"number"},"person":{"type":"string"},"start":{"type":"string"},"end":{"type":"string"},"timezone":{"type":"string","description":"The person's zone (start and end are their wall clock)"},"reason":{"type":"string","nullable":true},"source":{"type":"string","description":"office, google or outlook"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"}}},"required":["id","personId","person","start","end","timezone","reason","source","externalRefs"],"additionalProperties":false}}},"required":["blocks"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/blocks/{id}":{"delete":{"summary":"Remove a block","tags":["Office"],"description":"409 managed_by_integration when an integration owns this person's availability and you aren't it (owners: ?force=true, recorded); busy time imported from a linked Google or Outlook calendar (source google / outlook) can always be removed (it comes back at the next sync while the event is still there). Sends block.deleted (with source and the block's externalRefs).","parameters":[{"schema":{"type":"string","enum":["true","false"]},"in":"query","name":"force","required":false,"description":"Owners only: true removes it although an integration owns availability (recorded)"},{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"deleted":{"type":"boolean","enum":[true]}},"required":["id","deleted"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"managedBy":{"type":"string","description":"managed_by_integration: the slug of the platform that owns it, e.g. acme"},"managedByName":{"type":"string","description":"Its name, e.g. Your platform"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/first-available":{"post":{"summary":"Every start time someone qualified can make, with who would go (first available first)","tags":["Office"],"description":"For the office's schedule screen. Unlike the booking page's availability (and POST /v1/office/availability, the same answer with a key), every feasible start is listed (no efficiency filter that hides times with a bigger detour) and there's no one-hour lead: anything from now on. `firstAvailable` is the earliest of them. stepMinutes (1, 5, 15, 30 or 60; default 15) is the grid of local minutes starts lie on: 60 gives whole hours only, 1 any minute. Times are on the clock of the address (`timezone`: a branch whose area holds it gives its own, else the zone of the address itself). Book one with POST /v1/office/jobs.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"First local date, YYYY-MM-DD (default today)"},"days":{"type":"integer","minimum":1,"maximum":14,"default":1},"clientCompanyId":{"type":"string","format":"uuid","description":"The job is for this client company (one of yours, active). A test client: only test people are offered; otherwise test people never are"},"stepMinutes":{"anyOf":[{"type":"number","enum":[1]},{"type":"number","enum":[5]},{"type":"number","enum":[15]},{"type":"number","enum":[30]},{"type":"number","enum":[60]}],"default":15,"description":"Start times on this grid of local minutes: 60 = on the hour only, 15 = :00/:15/:30/:45 (the default), 1 = any minute. Every one is still travel- and job-feasible. One of 1, 5, 15, 30, 60 (422 otherwise)"}},"required":["serviceId","lat","lng"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string"},"slots":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string","description":"The real instant (UTC)"},"local":{"type":"string","description":"Wall clock where the job is ('YYYY-MM-DDTHH:MM', `timezone`)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"Who CrewLink would send, best first (least added driving); platform keys see first name and last initial"}},"required":["start","local","techs"],"additionalProperties":false}},"firstAvailable":{"type":"object","properties":{"start":{"type":"string","description":"The real instant (UTC)"},"local":{"type":"string","description":"Wall clock where the job is ('YYYY-MM-DDTHH:MM', `timezone`)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"Who CrewLink would send, best first (least added driving); platform keys see first name and last initial"}},"required":["start","local","techs"],"additionalProperties":false,"nullable":true},"outsideArea":{"type":"boolean","description":"No one serves the address (outside every service area)"}},"required":["timezone","slots","firstAvailable","outsideArea"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs":{"post":{"summary":"Book a job from the office (phone-in, walk-in)","tags":["Office"],"description":"Same lock-and-recheck as the booking page, without the one-hour lead time or the hold step. The engine picks the best tech(s) serving the address (a branch whose area holds it, or people without a branch whose radius does); the job is on the address's clock (`timezone` in the answer), or with techIds on theirs. Send the time as `start` (an instant) or `local` (the wall clock there). 409 slot_taken if no one qualified can make it, with a `reason` (on_call_reserve, outside_hours, time_off, no_skills or doesnt_fit) and a message that says why. With allowQueue: true that refusal (or a time already past) becomes 202: the job is filed in the intake queue as unscheduled, with the time asked for and the reason, for the office to place (GET /v1/office/intake). 422 without start or local. Or book a time you held (POST /v1/office/holds, or the booking page's holds): send holdId and holdSecret instead of serviceId, the time and the location (404 when the hold is unknown or the secret is wrong, 410 hold_expired). `address` is one line, or its parts ({ line1, line2, city, region, postcode, country, lat, lng }); with parts, lat and lng may come from it. externalRefs (or the externalRef shorthand): your own ids for the job (409 duplicate_external_ref when one is taken). geoPrecision: how exact the place is (the address search's `precision`); street- or area-level places are refused with 422 approximate_address unless allowApproximate is true (the default for imports and platform keys).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer","description":"Required unless booking a hold"},"lat":{"type":"number","minimum":-90,"maximum":90,"description":"Required unless booking a hold or address has lat"},"lng":{"type":"number","minimum":-180,"maximum":180,"description":"Required unless booking a hold or address has lng"},"start":{"type":"string","format":"date-time","description":"The instant (ISO 8601 with offset). Send this or local"},"local":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$","description":"Wall clock where the job is, 'YYYY-MM-DDTHH:MM', read in the zone of the branch serving it (no need to know that zone). Send this or start"},"holdId":{"type":"integer","description":"Book this held time (with holdSecret): its service, start, place and crew"},"holdSecret":{"type":"string","minLength":1,"maxLength":200},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40,"description":"Required, except for imports (source import) and contacts with contact.kind: then no texts go out"},"email":{"type":"string","format":"email"}},"required":["name"],"additionalProperties":false},"contact":{"type":"object","properties":{"kind":{"type":"string","enum":["commercial","office"]}},"required":["kind"],"additionalProperties":false,"description":"The customer is a business site (commercial) or reached through an office (office), not a homeowner on a mobile: customer.phone may be left out, and then no texts go out"},"address":{"anyOf":[{"type":"string","maxLength":300},{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":200},"line2":{"type":"string","maxLength":200},"city":{"type":"string","maxLength":100},"region":{"type":"string","maxLength":100,"description":"State or province"},"postcode":{"type":"string","maxLength":20},"country":{"type":"string","pattern":"^[A-Za-z]{2}$","description":"ISO 3166-1 alpha-2, e.g. US"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180}},"required":["line1"],"additionalProperties":false}],"description":"One line, or its parts"},"notes":{"type":"string","maxLength":2000},"flexible":{"type":"boolean","default":true,"description":"false pins it: emergencies will never bump it"},"clientCompanyId":{"type":"string","format":"uuid","description":"The client company this customer belongs to (one of yours, active): it sees the job like one it filed"},"siteId":{"type":"integer","exclusiveMinimum":true,"minimum":0,"description":"Put this visit at an existing site (GET /v1/office/sites): the property, for its client company. It may then carry ids (such as `acme`) that a live visit at that site already holds. Your own work may go to a client's site; a client's work only to its own (422 different_client); a merged site's id puts it at the site it was merged into; 404 when there is no such site. Left out: CrewLink picks the site by a shared externalRef, else the address and client company ([Office guide](/docs/guides/office#sites))"},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200},"description":"Your ids for this job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" } (up to 10). Each (source, id) is unique per company among jobs that aren't cancelled: one already on another live job is refused with 409 duplicate_external_ref (jobId: that job); one whose every job is cancelled is free to reuse (the cancelled job keeps it). Find a job by any of them: GET /v1/office/jobs?externalSource=&externalRef= (the live job first, then the newest)"},"externalRef":{"type":"string","minLength":1,"maxLength":200,"description":"Shorthand for one externalRefs entry: { [externalSource]: externalRef }. Also the job's primary id (externalRef / externalSource on reads)"},"externalSource":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,99}$","description":"Whose id externalRef is. Default: the platform's slug for a platform key, `api` for other office keys, `booking_page` on the public endpoints (which may not use a granted platform's slug)"},"allowQueue":{"type":"boolean","default":false,"description":"true: when nobody can take it at that time, file it in the intake queue (202, status unscheduled) instead of answering 409. The office places it later; the customer is texted that a time is coming"},"techIds":{"type":"array","items":{"type":"integer"},"minItems":1,"maxItems":10,"description":"Who goes. A future time: these people, if they can make it (else 409 slot_taken, or the queue with allowQueue). A past time with allowPast: recorded as given"},"allowPast":{"type":"boolean","default":false,"description":"true: a start that has passed is written as history: booked as given (techIds, else the nearest qualified active people), with no availability check and no text to the customer; booking.created goes out with history: true (only to webhook endpoints with historyEvents: true). Mark it done with POST /v1/office/jobs/{id}/complete, or leave it to show as overdue. With allowQueue as well, a past start is queued instead. Owners only (an owner's sign-in, the company API key, a platform key with write): 403 owner_only for dispatchers and field managers"},"source":{"type":"string","enum":["import"],"description":"import: a job moved from another system (the old CrewLink). Its source is import; implies allowPast, and with techIds allowOutsideHours; the customer gets no confirmation or queued text (a future visit still gets its reminder); filedAt may be sent. Owners only (an owner's sign-in, the company API key, a platform key with write): 403 owner_only for dispatchers and field managers"},"allowOutsideHours":{"type":"boolean","description":"Imports (source import, where it is the default with techIds; false turns it off), or, from an owner, a daylight-only service: true books it outside daylight (no 409 outside_daylight; working hours still apply). 422 otherwise (403 owner_only from a dispatcher or field manager on a daylight-only service). A future time the old system already confirmed is booked for techIds even outside their working hours, on a day they don't work, in the on-call reserve or past the drive the engine would allow; their hours are left as they are and the job reads outsideHours: true when it is outside them. Still refused: a tech already on another visit then (409 tech_busy, or the queue with allowQueue), time off, missing skills, someone not on the team"},"remind":{"type":"boolean","description":"Default true (false for source import): false sends the customer no reminder text for this job. Stored on the job; an office move turns reminders back on"},"notify":{"type":"boolean","description":"Default true. false: the homeowner gets no text or email about this job, ever: no confirmation, reminder, on-the-way or ETA, running-late move, reschedule, cancellation, review link or invoice (anything already queued is dropped). Stored on the job (homeownerTexts on reads); a move with notify: true turns it back on. Jobs a platform books are notify false whatever is sent when its grant has homeownerMessaging off, and the company's homeownerMessaging setting (PATCH /v1/office/settings) can silence every job"},"filedAt":{"type":"string","format":"date-time","description":"With source import: when the job was first filed in the old system (not in the future). Kept as the job's createdAt, so filed-date filters and lead-time KPIs stay true"},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"description":"How exact lat/lng are: the `precision` of the GET /v1/public/geocode match you picked (rooftop, address, street or locality). Stored on the job. street and locality (no house found) are refused with 422 approximate_address unless allowApproximate is true. Left out: not checked (your own coordinates)"},"allowApproximate":{"type":"boolean","description":"true: book a street- or area-level location anyway (the dispatcher confirmed it); it is stored as that precision. Default false, except for imports (source import) and platform keys, where it defaults to true"}},"required":["customer"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local (the zone of the address, or of the techs sent)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"Platform keys see names as first name and last initial"},"manageToken":{"type":"string"},"externalRef":{"type":"string","nullable":true},"externalSource":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Every external id, by source"},"siteId":{"type":"number","nullable":true,"description":"The site (property, per client company) the visit is at"},"history":{"type":"boolean","description":"true: a past start written as history (allowPast or source import)"},"outsideHours":{"type":"boolean","description":"true: booked outside a tech's working hours (an import with allowOutsideHours)"}},"required":["jobId","start","local","timezone","techs","manageToken","externalRef","externalSource","externalRefs"],"additionalProperties":false}}}},"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"status":{"type":"string","enum":["unscheduled"]},"queued":{"type":"boolean","enum":[true]},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"description":"The time that was asked for"},"timezone":{"type":"string","description":"IANA zone of requested.local (the zone of the address, or of the techs sent)"},"reason":{"type":"string","description":"Why it couldn't be booked then, in words"},"siteId":{"type":"number","nullable":true,"description":"The site (property, per client company) it is at"}},"required":["jobId","status","queued","requested","timezone","reason"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"reason":{"type":"string","enum":["on_call_reserve","outside_hours","time_off","no_skills","not_on_team","doesnt_fit"],"description":"on_call_reserve: held for same-day emergencies; outside_hours: outside the tech's working hours; time_off: a block on their calendar; no_skills / not_on_team: they can't do this job (not_on_team also when they are a test tech and the job isn't for a test client, or the reverse); doesnt_fit: another job or the drive is in the way"},"personId":{"type":"number","nullable":true,"description":"The tech the reason is about, when it is about one"},"jobId":{"type":"number","description":"duplicate_external_ref: the job that already has this externalRef"},"conflictJobId":{"type":"number","description":"tech_busy: the job the tech is already on at that time"}},"required":["error","message"],"additionalProperties":false}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"Every job: filter, search and page (for syncing, and for the office)","tags":["Office"],"description":"Every job of the company except bare holds (ask for status=held to see those). By default ordered by when it last changed (updatedAt, then id): for a sync, keep the last `updatedAt` you processed and ask with updatedSince. Page with `nextCursor` (null on the last page), sending the same filters and sort; `total` counts every match. `status` filters the stored status; `jobStatus` and `tab` what people see (the intake tabs: unscheduled, scheduled, overdue, needs_attention, done, cancelled). from/to are local dates on the start time and leave out jobs with no time (waiting, queued, unscheduled, declined); filedFrom/filedTo on when it was created. q searches customer name, phone (4+ digits), address, city and external ids; #123 finds one job. externalRef + externalSource finds the job you booked under your own id: every job that has it (a cancelled one keeps its ids, and the id may have been booked again since), ordered (unless you send sort) with the one that isn't cancelled first, then the newest filed, so jobs[0] is the job the id points to now. Platform keys see techs as first name and last initial. GET /v1/office/jobs.csv returns the same search as a spreadsheet.","parameters":[{"schema":{"type":"string","pattern":"^[a-z_]+(,[a-z_]+)*$"},"in":"query","name":"status","required":false,"description":"The stored status: one or more of booked, held, waiting, queued, unscheduled, declined, cancelled, passed, comma separated"},{"schema":{"type":"string","pattern":"^(unscheduled|scheduled|en_route|on_site|done|cancelled)(,(unscheduled|scheduled|en_route|on_site|done|cancelled))*$"},"in":"query","name":"jobStatus","required":false,"description":"What people see: one or more of unscheduled, scheduled, en_route, on_site, done, cancelled"},{"schema":{"type":"string","enum":["all","unscheduled","scheduled","overdue","needs_attention","done","cancelled"]},"in":"query","name":"tab","required":false,"description":"An intake tab: all, unscheduled, scheduled (upcoming, not overdue), overdue, needs_attention, done, cancelled"},{"schema":{"type":"string","maxLength":200},"in":"query","name":"q","required":false,"description":"Customer name, phone (4+ digits), address or city, an external id; #123 for one job"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false,"description":"Jobs starting on or after this local date"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false,"description":"Jobs starting on or before this local date"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"filedFrom","required":false,"description":"Filed (created) on or after this local date"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"filedTo","required":false,"description":"Filed (created) on or before this local date"},{"schema":{"type":"integer"},"in":"query","name":"techId","required":false,"description":"Jobs with this person on them"},{"schema":{"type":"integer"},"in":"query","name":"serviceId","required":false},{"schema":{"type":"string","pattern":"^(office|api|platform|booking_page|client|out_of_area_request|crewconnect|recurring|emergency|import|share_link)(,(office|api|platform|booking_page|client|out_of_area_request|crewconnect|recurring|emergency|import|share_link))*$"},"in":"query","name":"source","required":false,"description":"Where it came from: office, api, platform, booking_page, client, out_of_area_request, crewconnect, recurring, emergency, import, share_link"},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"clientCompanyId","required":false,"description":"Work for this client company"},{"schema":{"type":"string","maxLength":80},"in":"query","name":"client","required":false,"description":"The same by the client's CrewLink ID"},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"updatedSince","required":false,"description":"Jobs changed at or after this instant"},{"schema":{"type":"string","maxLength":100},"in":"query","name":"externalSource","required":false},{"schema":{"type":"string","maxLength":200},"in":"query","name":"externalRef","required":false},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"siteId","required":false,"description":"Only the visits of this site"},{"schema":{"type":"string","enum":["updated_asc","filed_desc","filed_asc","start_desc","start_asc"]},"in":"query","name":"sort","required":false,"description":"updated_asc (the default: oldest change first, for syncing), filed_desc, filed_asc, start_desc or start_asc. With externalRef and no sort: jobs that aren't cancelled first, then the newest filed (so the first row is the job that id points to now)"},{"schema":{"type":"string","maxLength":400},"in":"query","name":"cursor","required":false,"description":"nextCursor from the previous page"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"in":"query","name":"limit","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"status":{"type":"string","enum":["booked","held","waiting","queued","unscheduled","declined","cancelled","passed"],"description":"The stored status: booked; held (a hold not booked yet); waiting (an out-of-area request); queued (in the intake queue: nobody could take it at the time asked); unscheduled (moved off the board, no new time yet); declined; cancelled; passed (CrewConnect: passed on to a partner)"},"stage":{"type":"string","enum":["scheduled","en_route","on_site","done"]},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"What people see: unscheduled (no time yet: waiting, queued or moved off the board), scheduled, en_route, on_site, done or cancelled (cancelled or declined)"},"overdue":{"type":"boolean","description":"Booked, not done, and its last visit has ended"},"overdueDays":{"type":"number","nullable":true,"description":"Whole days since the last visit ended, when overdue"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"start":{"type":"string","nullable":true,"description":"The real instant (UTC); null for waiting, queued, unscheduled and declined"},"local":{"type":"string","nullable":true,"description":"Wall clock in `timezone`, e.g. '2026-10-13T14:30'"},"end":{"type":"string","nullable":true,"description":"When the first day is booked to end (the longest visit), UTC"},"endLocal":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of local and endLocal (the zone of the address, or of the techs sent)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true,"description":"Read from the address (best effort)"},"state":{"type":"string","nullable":true,"description":"Two-letter US state read from the address, when there is one"},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true,"description":"The assigned technician (the first, when a crew goes)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false},"description":"Everyone assigned"},"externalRef":{"type":"string","nullable":true,"description":"The primary external id (see externalRefs)"},"externalSource":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Every external id on the job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" }"},"needsAttention":{"type":"object","properties":{"reason":{"type":"string"},"at":{"type":"string"}},"required":["reason","at"],"additionalProperties":false,"nullable":true},"doneAt":{"type":"string","nullable":true},"cancelledAfterDone":{"type":"boolean","description":"true: the job was cancelled after it was done (doneAt is kept; the techs' pay for it was taken back)"},"createdAt":{"type":"string","description":"When it was filed"},"updatedAt":{"type":"string"},"ageDays":{"type":"number","description":"Whole days since it was filed"},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"nullable":true,"description":"Queued and moved-off-the-board jobs: the time asked for (or the time it had)"},"requestedDays":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false},"nullable":true,"description":"Out-of-area requests: the days the customer picked"},"intakeReason":{"type":"string","nullable":true,"description":"Queued jobs: why it couldn't be booked when it was asked for"},"source":{"type":"string","enum":["office","api","platform","booking_page","client","out_of_area_request","crewconnect","recurring","emergency","import","share_link"],"description":"Where it came from"},"sentBy":{"type":"string","nullable":true,"description":"CrewConnect: the company that sent it"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"filedBy":{"type":"string","nullable":true,"description":"Who filed or booked it, e.g. \"Filed by Ann Lee · Globex Solar\""},"homeownerTexts":{"type":"boolean","description":"false: the homeowner gets no texts or emails about this job (booked with notify: false, the company's homeownerMessaging is off, or the platform that booked it has homeownerMessaging off on its grant)"},"outsideHours":{"type":"boolean","description":"true: booked outside its tech's working hours or on a day they don't work, honouring a time the old system had confirmed (POST /v1/office/jobs source import, allowOutsideHours). Their hours were not changed; a move clears it"},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"nullable":true,"description":"How exact lat/lng are, as reported when it was booked (geoPrecision on POST /v1/office/jobs): rooftop, address, street or locality. street and locality are approximate places someone confirmed. Null when not reported"},"siteId":{"type":"number","nullable":true,"description":"The site this visit belongs to: the property, for its client company, with every service ever done there (GET /v1/office/sites/{id}). Null until linked (a hold)"},"siteVisit":{"type":"object","properties":{"number":{"type":"number","nullable":true,"description":"Its place among the site's visits that aren't cancelled, oldest first; null when this one is cancelled"},"of":{"type":"number","description":"Visits at the site that aren't cancelled"},"revisit":{"type":"boolean","description":"A revisit service"}},"required":["number","of","revisit"],"additionalProperties":false,"nullable":true,"description":"\"Visit 3 at this site\" (number); null without a site"},"shareLinkId":{"type":"string","nullable":true,"description":"Booked or requested through this client share link (source share_link; GET /v1/office/clients/{clientId}/share-links), else null"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false,"nullable":true,"description":"The client's sales rep who booked it through a share link (null otherwise). filedBy then reads \"Booked by <rep> via link · <client>\""}},"required":["id","status","stage","jobStatus","overdue","overdueDays","service","start","local","end","endLocal","timezone","address","city","state","lat","lng","customer","tech","techs","externalRef","externalSource","externalRefs","needsAttention","doneAt","cancelledAfterDone","createdAt","updatedAt","ageDays","requested","requestedDays","intakeReason","source","sentBy","client","filedBy","homeownerTexts","outsideHours","geoPrecision","siteId","siteVisit","shareLinkId","bookedBy"],"additionalProperties":false}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"number"}},"required":["jobs","nextCursor","total"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/move":{"post":{"summary":"Move a job to another time and/or tech (a drag on the board)","tags":["Office"],"description":"Locks the old and new days, rechecks against the live schedule and writes it. With techIds, those people go if they fit; without, the tech(s) already on the job stay if they still fit, else the best tech(s) at that time go. 409 slot_taken when it can't be booked, with a `reason` and a message that says why: on_call_reserve (held for same-day emergencies), outside_hours, time_off, no_skills, not_on_team, or doesnt_fit (another job or the drive is in the way). The customer's manage link keeps working. A move turns reminder texts back on for a job booked with remind: false (an import). An unscheduled job (moved off the board by an emergency or a long job, with no new time yet, or waiting in the intake queue) is booked at that time the way an accepted request is: the customer gets a confirmation with a fresh manage link, and any pick-a-new-time offer they were sent stops working. notify: false sends the homeowner nothing about this move and keeps the job quiet from then on (stored on the job, as on POST /v1/office/jobs); true turns its texts back on; left out, the job keeps what it had. An imported job (source import) moved with techIds is placed at its old commitment the way POST /v1/office/jobs books an import (allowOutsideHours, implied; false turns it off): those techs go even outside their working hours, on a day they don't work, in the on-call reserve or past the drive the engine would allow, from the queue (queued or unscheduled: booked now) or from the board; the job goes on their clock when they share one, and reads outsideHours: true when it is outside their hours. Still refused: another visit of theirs at that time (409 tech_busy with conflictJobId), time off, missing skills, someone not on the team (409 slot_taken). A job for a test client only goes to test techs, any other never to one (409 slot_taken, not_on_team). allowOverlap: true with techIds (on an imported job; owners on any job, below; 422 without techIds) keeps the old app's double-booking: the overlap with another visit of theirs is not refused (no 409 tech_busy) and both jobs stay on the tech; time off, skills and the team are still checked. When an overlap was skipped, the answer lists it in doubleBooked and the moved job gets needs attention, reason 'Double-booked with #<conflictJobId> (imported from the old app)': with job.needs_attention, unless the move is notify: false (or the job's texts are off), when only the flag and its timeline entry are written. A past start is 409 in_the_past, except for imported jobs, where allowPast is implied (false turns it off), and for an owner's allowPast: true on any job (below): the job is written as history the way POST /v1/office/jobs writes a past import: booked as given with techIds (else the techs already on it; 422 when there are none), with no availability, hours or overlap checks, and visits of the service's length, so it reads overdue until it is marked done (POST /v1/office/jobs/{id}/complete). No homeowner text and no reminder (the time has passed; queued reminders are dropped); booking.created (from the queue) or booking.rescheduled goes out with its techs (always: notify only controls homeowner texts, never webhooks). The answer adds history: true. Owners (an owner's sign-in, the company API key, a platform key with write) may also use allowOverlap and allowPast on ANY job: allowOverlap with techIds books a double-booking anyway (time off, skills, the team, the test split, working hours and the on-call reserve are still checked; only the overlap and the drive give way), flagged needs attention 'Double-booked with #<conflictJobId>' (silent when notify is false) and listed in doubleBooked; allowPast: true writes a past start as history, as for an import. Dispatchers and field managers get 403 owner_only for either on a job that isn't an import. techIds: [] unassigns a booked job (owners, platforms, dispatchers): it keeps its time but goes back to the intake queue as unscheduled with intakeReason 'Unassigned', its visits are removed, booking.rescheduled goes out with techs [], status unscheduled, unassigned: true, from and requested (the kept time), and the homeowner is not texted (start and local must be left out; 422 for a job that isn't booked, is done, or with allowOverlap, allowPast or allowOutsideHours).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"start":{"type":"string","format":"date-time","description":"The instant (ISO 8601 with offset). Send this or local"},"local":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$","description":"Wall clock where the job is, 'YYYY-MM-DDTHH:MM', read in the zone of the branch serving it (no need to know that zone). Send this or start"},"techIds":{"type":"array","items":{"type":"integer"},"description":"Who goes (each checked). [] unassigns a booked job: it keeps its time and goes back to the intake queue as unscheduled (\"Unassigned\"), see the description"},"notify":{"type":"boolean","description":"false: no text about this move, and none for this job from now on (stored on the job); true: texts on again; default: unchanged"},"allowOutsideHours":{"type":"boolean","description":"Imported jobs (source import), or, from an owner, a job of a daylight-only service: true moves it outside daylight (no 409 outside_daylight; working hours still apply). 422 otherwise (403 owner_only from a dispatcher or field manager on a daylight-only service). Default true with techIds: the old commitment is honoured outside the techs' working hours (see the description). false: the usual checks"},"allowPast":{"type":"boolean","description":"Imported jobs (where it is the default), or any job when the caller is an owner (403 owner_only for dispatchers and field managers). true: a start that has passed is written as history (see the description); false: 409 in_the_past as for any job"},"allowOverlap":{"type":"boolean","description":"With techIds. Imported jobs (anyone who may move them; not with allowOutsideHours: false), or any job when the caller is an owner (403 owner_only for dispatchers and field managers). true: an overlap with another visit of the tech is booked anyway (no 409 tech_busy or slot_taken doesnt_fit) and the job is flagged needs attention. Time off, skills, the team and the test split are still checked (and, for a job that isn't an import, working hours and the on-call reserve)"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local (the job's)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}},"outsideHours":{"type":"boolean","description":"true: placed outside a tech's working hours (an imported job's old commitment)"},"history":{"type":"boolean","description":"true: a past start written as history (an imported job, or allowPast from an owner)"},"doubleBooked":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"conflictJobId":{"type":"number"}},"required":["personId","conflictJobId"],"additionalProperties":false},"description":"allowOverlap: the overlaps that were booked anyway (one per tech), when there were any"},"status":{"type":"string","enum":["unscheduled"],"description":"techIds []: the job is back in the intake queue (start and local are the time it keeps)"},"unassigned":{"type":"boolean","description":"true: techIds [] took everyone off the job"}},"required":["jobId","start","local","timezone","techs"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"owner_only: allowOverlap or allowPast on a job that is not an import, from someone who is not an owner","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false,"description":"owner_only: allowOverlap or allowPast on a job that is not an import, from someone who is not an owner"}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"reason":{"type":"string","enum":["on_call_reserve","outside_hours","time_off","no_skills","not_on_team","doesnt_fit"],"description":"on_call_reserve: held for same-day emergencies; outside_hours: outside the tech's working hours; time_off: a block on their calendar; no_skills / not_on_team: they can't do this job (not_on_team also when they are a test tech and the job isn't for a test client, or the reverse); doesnt_fit: another job or the drive is in the way"},"personId":{"type":"number","nullable":true,"description":"The tech the reason is about, when it is about one"},"conflictJobId":{"type":"number","description":"tech_busy: the job the tech is already on at that time"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/cancel":{"post":{"summary":"Cancel a job (no cutoff for the office)","tags":["Office"],"description":"Booked, unscheduled or queued jobs, done ones included. The customer is texted (unless notify is false, or the job's homeowner texts are off: homeownerTexts on reads) and queued reminders are dropped. To drop a request from the intake queue with a polite we-can't-make-it text instead, use POST /v1/office/intake/{id}/discard. A job that is done can be cancelled too (owners and dispatchers who may change it, the company API key, a platform key with write): it reads cancelled with cancelledAfterDone: true and keeps doneAt; booking.cancelled carries wasDone: true; nobody is texted unless notify: true is sent; KPIs and plan vs actual stop counting it; and each tech's pay for it is taken back (payReversed): from a pay week that is still open the job line is removed and that day's mileage redone without the stop; from a week that is closed or paid a negative adjustment 'Job #<id> cancelled after completion' goes into the open week.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":200},"notify":{"type":"boolean","description":"Default true (false for a job that is done). false: no cancellation text (queued reminders are still dropped), e.g. moving a cancelled job from another system"}},"additionalProperties":false,"default":{}}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"status":{"type":"string","enum":["cancelled"]},"wasDone":{"type":"boolean","description":"true: the job was done when it was cancelled (cancelledAfterDone on reads)"},"payReversed":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"amountCents":{"type":"number","description":"What was taken back, positive: the job pay plus what the stop added to that day's mileage"},"currency":{"type":"string"},"how":{"type":"string","enum":["removed","adjustment"],"description":"removed: still in an open pay week, the lines were taken out; adjustment: its week was closed or paid, a negative adjustment went into the open week"},"lineId":{"type":"number","nullable":true,"description":"The adjustment line (how adjustment)"}},"required":["personId","amountCents","currency","how","lineId"],"additionalProperties":false},"description":"wasDone: each tech whose pay for the job was taken back"}},"required":["jobId","status"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/history":{"post":{"summary":"Correct an imported job's history: the time and techs the old system had (no checks, nothing sent)","tags":["Office"],"description":"For a job imported with source import that is cancelled or done: sets its visit time and techs to what the old system had, exactly as sent, so the job keeps its id and externalRefs. end defaults to start plus the service's booked length (what a booking blocks). No availability, working hours, drive or overlap checks, and nothing is sent: no homeowner text or email, no tech notification, no webhook. The job's status stays as it is (doneAt is kept) and its timeline gets a history_corrected entry. A cancelled job stays off the schedule, the same as any job booked and then cancelled: it has no visits, so it never blocks a booking, never shows on the board, dispatch, the tech's day or the calendar, and never counts in KPIs or pay; job reads show its corrected start, end and techs. A done job's visits are replaced (one per tech and service day); what was already recorded when it was completed (plan vs actual, tech pay lines) is left as it was. A cancelled job keeps whoever the old system had, test people included (it is never booked or counted); a done job follows the test split (a test client's job only test people, any other never). 422 not_history for any other job (not an import, or booked, queued or unscheduled: use POST /v1/office/jobs/{id}/move), and for techIds that are not people of this company. Platform keys need write.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"start":{"type":"string","format":"date-time","description":"The instant (ISO 8601 with offset). Send this or local"},"local":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$","description":"Wall clock where the job is, 'YYYY-MM-DDTHH:MM', read in the zone of the branch serving it (no need to know that zone). Send this or start"},"end":{"type":"string","format":"date-time","description":"When the visit ended (ISO 8601 with offset), after start and within 24 hours of it. Default: start plus the service's booked length (what a booking blocks)"},"techIds":{"type":"array","items":{"type":"integer"},"minItems":1,"maxItems":20,"description":"The people the old system had on it (this company's; inactive people are fine)"}},"required":["techIds"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"status":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"Unchanged: cancelled or done"},"start":{"type":"string"},"local":{"type":"string"},"end":{"type":"string"},"endLocal":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local and endLocal (the job's)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}}},"required":["jobId","status","start","local","end","endLocal","timezone","techs"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/photos":{"get":{"summary":"A job's photos (view links last 15 minutes)","tags":["Office"],"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"photos":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"caption":{"type":"string","nullable":true},"at":{"type":"string"}},"required":["id","url","caption","at"],"additionalProperties":false}}},"required":["photos"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/webhooks":{"post":{"summary":"Subscribe a URL to events (owner only)","tags":["Webhooks"],"description":"Events: booking.created, booking.rescheduled, booking.cancelled, block.created, block.deleted, connection.requested, connection.accepted, connection.ended, job.en_route, job.arrived, job.completed, review.created, invoice.paid, deposit.paid, trip.booked, request.created, job.running_late, day.reviewed, message.posted, badge.earned, job.needs_attention, attachment.uploaded, person.updated, availability.updated, person.base.updated, dispatch.position, job.risk_changed, site.created, site.updated, site.merged, or * for all (except dispatch.position: only subscriptions that name it get it). A platform key subscribing for this company may also ask for external_message.created, external_message.updated, external_message.deleted (office replies in mirrored conversations, sent only to the owning platform's endpoints). Each delivery is a JSON POST signed with the returned secret: header CrewLink-Signature t=<unix>,v1=<hex HMAC-SHA256 of \"<t>.<body>\">. Failed deliveries retry after 1, 5, 30, 120 and 720 minutes. The URL must be https on a public host name: no IP addresses, no internal names (localhost, *.internal, ...), nothing that resolves to a private, loopback, link-local, CGNAT, multicast or reserved address, and port 443 or 8443 unless the host resolves to a public address. Redirects are never followed. 422 when the URL is refused.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","maxLength":2000,"pattern":"^https\\:\\/\\/"},"events":{"type":"array","items":{"type":"string","enum":["booking.created","booking.rescheduled","booking.cancelled","block.created","block.deleted","connection.requested","connection.accepted","connection.ended","job.en_route","job.arrived","job.completed","review.created","invoice.paid","deposit.paid","trip.booked","request.created","job.running_late","day.reviewed","message.posted","badge.earned","job.needs_attention","attachment.uploaded","person.updated","availability.updated","person.base.updated","dispatch.position","job.risk_changed","site.created","site.updated","site.merged","external_message.created","external_message.updated","external_message.deleted","*"]},"minItems":1},"historyEvents":{"type":"boolean","default":false,"description":"true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Change it with PATCH /v1/office/webhooks/{id}."}},"required":["url","events"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"historyEvents":{"type":"boolean","description":"true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Change it with PATCH /v1/office/webhooks/{id}."},"platform":{"type":"string","nullable":true,"description":"The platform (its slug) that subscribed it, or null for the company's own"},"secret":{"type":"string","description":"Shown once"}},"required":["id","url","events","active","historyEvents","platform","secret"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"Your webhook endpoints","tags":["Webhooks"],"description":"A platform key lists only the endpoints that platform subscribed; the company's own keys list all of them.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"endpoints":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"historyEvents":{"type":"boolean","description":"true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Change it with PATCH /v1/office/webhooks/{id}."},"platform":{"type":"string","nullable":true,"description":"The platform (its slug) that subscribed it, or null for the company's own"}},"required":["id","url","events","active","historyEvents","platform"],"additionalProperties":false}}},"required":["endpoints"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/webhooks/{id}":{"patch":{"summary":"Change an endpoint: events and historyEvents (owner only)","tags":["Webhooks"],"description":"events: replaces the endpoint's event list (URL and signing secret unchanged; \"*\" excludes dispatch.position, name it to get it). historyEvents: true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Send either or both. Only this company's endpoints (a platform key: only the ones it subscribed); 404 otherwise. Deliveries already queued are not changed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"historyEvents":{"type":"boolean","description":"true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Change it with PATCH /v1/office/webhooks/{id}."},"events":{"type":"array","items":{"type":"string","enum":["booking.created","booking.rescheduled","booking.cancelled","block.created","block.deleted","connection.requested","connection.accepted","connection.ended","job.en_route","job.arrived","job.completed","review.created","invoice.paid","deposit.paid","trip.booked","request.created","job.running_late","day.reviewed","message.posted","badge.earned","job.needs_attention","attachment.uploaded","person.updated","availability.updated","person.base.updated","dispatch.position","job.risk_changed","site.created","site.updated","site.merged","external_message.created","external_message.updated","external_message.deleted","*"]},"minItems":1,"description":"The new event list (replaces the old one)"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"historyEvents":{"type":"boolean","description":"true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Change it with PATCH /v1/office/webhooks/{id}."},"platform":{"type":"string","nullable":true,"description":"The platform (its slug) that subscribed it, or null for the company's own"}},"required":["id","url","events","active","historyEvents","platform"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"delete":{"summary":"Remove an endpoint (owner only; pending deliveries are dropped)","tags":["Webhooks"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}},"required":["id","deleted"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/webhooks/{id}/deliveries":{"get":{"summary":"An endpoint's delivery log (owner only)","tags":["Webhooks"],"description":"Newest first, for debugging a receiver: what was queued for this endpoint, how many attempts, the last HTTP status and error, when it goes next, and the body exactly as sent. Filters: event, jobId (data.jobId, data.bookingId or data.requestId), status (pending, delivered, failed); limit 1-200 (default 50); before: the `next` of the previous page. Only this company's endpoints (a platform key: only the ones it subscribed); 404 otherwise. A platform's platform-wide subscription has its own log: GET /v1/platform/webhooks/{id}/deliveries.","parameters":[{"schema":{"type":"string","maxLength":100},"in":"query","name":"event","required":false,"description":"Only this event, e.g. booking.created"},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"jobId","required":false,"description":"Only deliveries about this job: data.jobId, data.bookingId or data.requestId (request.created)"},{"schema":{"type":"string","enum":["pending","delivered","failed"]},"in":"query","name":"status","required":false},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"before","required":false,"description":"Cursor: only deliveries with a smaller id (pass `next` from the previous page)"},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"deliveries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number","description":"The delivery id (CrewLink-Delivery header, and `id` in the body)"},"endpointId":{"type":"string"},"event":{"type":"string"},"companyId":{"type":"string","description":"The company the event is about"},"status":{"type":"string","enum":["pending","delivered","failed"],"description":"pending (queued or waiting to retry), delivered (a 2xx came back) or failed (gave up after the last retry)"},"createdAt":{"type":"string","description":"When it was queued"},"attempts":{"type":"number"},"lastAttemptAt":{"type":"string","nullable":true},"nextAttemptAt":{"type":"string","nullable":true,"description":"When it goes (again); null unless pending"},"lastStatusCode":{"type":"number","nullable":true,"description":"HTTP status of the last attempt; null when no answer came back (refused host, timeout, connection error) or not tried yet"},"lastError":{"type":"string","nullable":true,"description":"Why the last attempt failed (up to 500 characters); null after a success"},"deliveredAt":{"type":"string","nullable":true},"payload":{"type":"object","additionalProperties":{},"description":"The body as sent: { id, event, createdAt, companyId, data }"}},"required":["id","endpointId","event","companyId","status","createdAt","attempts","lastAttemptAt","nextAttemptAt","lastStatusCode","lastError","deliveredAt","payload"],"additionalProperties":false},"description":"Newest first"},"next":{"type":"number","nullable":true,"description":"Pass as `before` for the next page; null on the last"}},"required":["deliveries","next"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/webhooks/{id}/deliveries/{deliveryId}/retry":{"post":{"summary":"Send one delivery again now (owner only)","tags":["Webhooks"],"description":"Requeues the delivery (pending, due now) whatever its status, with the same id and body; the next send loop (every 10 seconds) sends it. Attempts are kept, so one that had given up and fails again gives up again (retry it again later). 404 when the endpoint or delivery is not one you can see.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"path","name":"deliveryId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number","description":"The delivery id (CrewLink-Delivery header, and `id` in the body)"},"endpointId":{"type":"string"},"event":{"type":"string"},"companyId":{"type":"string","description":"The company the event is about"},"status":{"type":"string","enum":["pending","delivered","failed"],"description":"pending (queued or waiting to retry), delivered (a 2xx came back) or failed (gave up after the last retry)"},"createdAt":{"type":"string","description":"When it was queued"},"attempts":{"type":"number"},"lastAttemptAt":{"type":"string","nullable":true},"nextAttemptAt":{"type":"string","nullable":true,"description":"When it goes (again); null unless pending"},"lastStatusCode":{"type":"number","nullable":true,"description":"HTTP status of the last attempt; null when no answer came back (refused host, timeout, connection error) or not tried yet"},"lastError":{"type":"string","nullable":true,"description":"Why the last attempt failed (up to 500 characters); null after a success"},"deliveredAt":{"type":"string","nullable":true},"payload":{"type":"object","additionalProperties":{},"description":"The body as sent: { id, event, createdAt, companyId, data }"}},"required":["id","endpointId","event","companyId","status","createdAt","attempts","lastAttemptAt","nextAttemptAt","lastStatusCode","lastError","deliveredAt","payload"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/intake":{"get":{"summary":"The intake queue: counts per tab and dispatch risks","tags":["Intake"],"description":"How many jobs are in each tab (all, unscheduled, scheduled, overdue, needs attention, done, cancelled) and the dispatch risks: jobs still unscheduled 3 or more days after they were filed (oldest first), then visits that ended without being marked done. Rows for a tab come from GET /v1/office/jobs?tab=. Filters narrow the counts the same way.","parameters":[{"schema":{"type":"string","maxLength":200},"in":"query","name":"q","required":false,"description":"Customer name, phone (4+ digits), address or city, an external id; #123 for one job"},{"schema":{"type":"integer"},"in":"query","name":"techId","required":false,"description":"Jobs with this person on them"},{"schema":{"type":"integer"},"in":"query","name":"serviceId","required":false},{"schema":{"type":"string","pattern":"^(office|api|platform|booking_page|client|out_of_area_request|crewconnect|recurring|emergency|import|share_link)(,(office|api|platform|booking_page|client|out_of_area_request|crewconnect|recurring|emergency|import|share_link))*$"},"in":"query","name":"source","required":false,"description":"Where it came from: office, api, platform, booking_page, client, out_of_area_request, crewconnect, recurring, emergency, import, share_link"},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"clientCompanyId","required":false,"description":"Work for this client company"},{"schema":{"type":"string","maxLength":80},"in":"query","name":"client","required":false,"description":"The same by the client's CrewLink ID"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"filedFrom","required":false,"description":"Filed (created) on or after this local date"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"filedTo","required":false,"description":"Filed (created) on or before this local date"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"counts":{"type":"object","properties":{"all":{"type":"number"},"unscheduled":{"type":"number"},"scheduled":{"type":"number"},"overdue":{"type":"number"},"needs_attention":{"type":"number"},"done":{"type":"number"},"cancelled":{"type":"number"}},"required":["all","unscheduled","scheduled","overdue","needs_attention","done","cancelled"],"additionalProperties":false},"risks":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["aging","overdue"]},"job":{"type":"object","properties":{"id":{"type":"number"},"status":{"type":"string","enum":["booked","held","waiting","queued","unscheduled","declined","cancelled","passed"],"description":"The stored status: booked; held (a hold not booked yet); waiting (an out-of-area request); queued (in the intake queue: nobody could take it at the time asked); unscheduled (moved off the board, no new time yet); declined; cancelled; passed (CrewConnect: passed on to a partner)"},"stage":{"type":"string","enum":["scheduled","en_route","on_site","done"]},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"What people see: unscheduled (no time yet: waiting, queued or moved off the board), scheduled, en_route, on_site, done or cancelled (cancelled or declined)"},"overdue":{"type":"boolean","description":"Booked, not done, and its last visit has ended"},"overdueDays":{"type":"number","nullable":true,"description":"Whole days since the last visit ended, when overdue"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"start":{"type":"string","nullable":true,"description":"The real instant (UTC); null for waiting, queued, unscheduled and declined"},"local":{"type":"string","nullable":true,"description":"Wall clock in `timezone`, e.g. '2026-10-13T14:30'"},"end":{"type":"string","nullable":true,"description":"When the first day is booked to end (the longest visit), UTC"},"endLocal":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of local and endLocal (the zone of the address, or of the techs sent)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true,"description":"Read from the address (best effort)"},"state":{"type":"string","nullable":true,"description":"Two-letter US state read from the address, when there is one"},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true,"description":"The assigned technician (the first, when a crew goes)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false},"description":"Everyone assigned"},"externalRef":{"type":"string","nullable":true,"description":"The primary external id (see externalRefs)"},"externalSource":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Every external id on the job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" }"},"needsAttention":{"type":"object","properties":{"reason":{"type":"string"},"at":{"type":"string"}},"required":["reason","at"],"additionalProperties":false,"nullable":true},"doneAt":{"type":"string","nullable":true},"cancelledAfterDone":{"type":"boolean","description":"true: the job was cancelled after it was done (doneAt is kept; the techs' pay for it was taken back)"},"createdAt":{"type":"string","description":"When it was filed"},"updatedAt":{"type":"string"},"ageDays":{"type":"number","description":"Whole days since it was filed"},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"nullable":true,"description":"Queued and moved-off-the-board jobs: the time asked for (or the time it had)"},"requestedDays":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false},"nullable":true,"description":"Out-of-area requests: the days the customer picked"},"intakeReason":{"type":"string","nullable":true,"description":"Queued jobs: why it couldn't be booked when it was asked for"},"source":{"type":"string","enum":["office","api","platform","booking_page","client","out_of_area_request","crewconnect","recurring","emergency","import","share_link"],"description":"Where it came from"},"sentBy":{"type":"string","nullable":true,"description":"CrewConnect: the company that sent it"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"filedBy":{"type":"string","nullable":true,"description":"Who filed or booked it, e.g. \"Filed by Ann Lee · Globex Solar\""},"homeownerTexts":{"type":"boolean","description":"false: the homeowner gets no texts or emails about this job (booked with notify: false, the company's homeownerMessaging is off, or the platform that booked it has homeownerMessaging off on its grant)"},"outsideHours":{"type":"boolean","description":"true: booked outside its tech's working hours or on a day they don't work, honouring a time the old system had confirmed (POST /v1/office/jobs source import, allowOutsideHours). Their hours were not changed; a move clears it"},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"nullable":true,"description":"How exact lat/lng are, as reported when it was booked (geoPrecision on POST /v1/office/jobs): rooftop, address, street or locality. street and locality are approximate places someone confirmed. Null when not reported"},"siteId":{"type":"number","nullable":true,"description":"The site this visit belongs to: the property, for its client company, with every service ever done there (GET /v1/office/sites/{id}). Null until linked (a hold)"},"siteVisit":{"type":"object","properties":{"number":{"type":"number","nullable":true,"description":"Its place among the site's visits that aren't cancelled, oldest first; null when this one is cancelled"},"of":{"type":"number","description":"Visits at the site that aren't cancelled"},"revisit":{"type":"boolean","description":"A revisit service"}},"required":["number","of","revisit"],"additionalProperties":false,"nullable":true,"description":"\"Visit 3 at this site\" (number); null without a site"},"shareLinkId":{"type":"string","nullable":true,"description":"Booked or requested through this client share link (source share_link; GET /v1/office/clients/{clientId}/share-links), else null"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false,"nullable":true,"description":"The client's sales rep who booked it through a share link (null otherwise). filedBy then reads \"Booked by <rep> via link · <client>\""}},"required":["id","status","stage","jobStatus","overdue","overdueDays","service","start","local","end","endLocal","timezone","address","city","state","lat","lng","customer","tech","techs","externalRef","externalSource","externalRefs","needsAttention","doneAt","cancelledAfterDone","createdAt","updatedAt","ageDays","requested","requestedDays","intakeReason","source","sentBy","client","filedBy","homeownerTexts","outsideHours","geoPrecision","siteId","siteVisit","shareLinkId","bookedBy"],"additionalProperties":false}},"required":["kind","job"],"additionalProperties":false}},"agingDays":{"type":"number"}},"required":["counts","risks","agingDays"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs.csv":{"get":{"summary":"The job list as a spreadsheet (CSV)","tags":["Intake"],"description":"The rows GET /v1/office/jobs would list for these filters and sort, up to 5000, as RFC 4180 CSV with a UTF-8 BOM. Times are in each job's zone. No cursor or limit.","parameters":[{"schema":{"type":"string","pattern":"^[a-z_]+(,[a-z_]+)*$"},"in":"query","name":"status","required":false,"description":"The stored status: one or more of booked, held, waiting, queued, unscheduled, declined, cancelled, passed, comma separated"},{"schema":{"type":"string","pattern":"^(unscheduled|scheduled|en_route|on_site|done|cancelled)(,(unscheduled|scheduled|en_route|on_site|done|cancelled))*$"},"in":"query","name":"jobStatus","required":false,"description":"What people see: one or more of unscheduled, scheduled, en_route, on_site, done, cancelled"},{"schema":{"type":"string","enum":["all","unscheduled","scheduled","overdue","needs_attention","done","cancelled"]},"in":"query","name":"tab","required":false,"description":"An intake tab: all, unscheduled, scheduled (upcoming, not overdue), overdue, needs_attention, done, cancelled"},{"schema":{"type":"string","maxLength":200},"in":"query","name":"q","required":false,"description":"Customer name, phone (4+ digits), address or city, an external id; #123 for one job"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false,"description":"Jobs starting on or after this local date"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false,"description":"Jobs starting on or before this local date"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"filedFrom","required":false,"description":"Filed (created) on or after this local date"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"filedTo","required":false,"description":"Filed (created) on or before this local date"},{"schema":{"type":"integer"},"in":"query","name":"techId","required":false,"description":"Jobs with this person on them"},{"schema":{"type":"integer"},"in":"query","name":"serviceId","required":false},{"schema":{"type":"string","pattern":"^(office|api|platform|booking_page|client|out_of_area_request|crewconnect|recurring|emergency|import|share_link)(,(office|api|platform|booking_page|client|out_of_area_request|crewconnect|recurring|emergency|import|share_link))*$"},"in":"query","name":"source","required":false,"description":"Where it came from: office, api, platform, booking_page, client, out_of_area_request, crewconnect, recurring, emergency, import, share_link"},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"clientCompanyId","required":false,"description":"Work for this client company"},{"schema":{"type":"string","maxLength":80},"in":"query","name":"client","required":false,"description":"The same by the client's CrewLink ID"},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"updatedSince","required":false,"description":"Jobs changed at or after this instant"},{"schema":{"type":"string","maxLength":100},"in":"query","name":"externalSource","required":false},{"schema":{"type":"string","maxLength":200},"in":"query","name":"externalRef","required":false},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"siteId","required":false,"description":"Only the visits of this site"},{"schema":{"type":"string","enum":["updated_asc","filed_desc","filed_asc","start_desc","start_asc"]},"in":"query","name":"sort","required":false,"description":"updated_asc (the default: oldest change first, for syncing), filed_desc, filed_asc, start_desc or start_asc. With externalRef and no sort: jobs that aren't cancelled first, then the newest filed (so the first row is the job that id points to now)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"text/csv; charset=utf-8","content":{"text/csv":{"schema":{"type":"string","description":"text/csv; charset=utf-8"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/intake/{id}/options":{"get":{"summary":"Who could take a waiting job on a day: closest techs first, with their open times","tags":["Intake"],"description":"For an unscheduled job (queued, out-of-area or bumped): every qualified tech whose home is within `radius` of the job, closest first, with the distance and drive from home and each open start that day (15-minute steps) with the drive from where they'd set off. radius is in the company's units (`units`: mi in the US and Canada, else km); default 100 mi (160 km). For a job that needs more than one person, `crews` lists open starts with who would go. Book one with POST /v1/office/jobs/{id}/move (or, for an out-of-area request, POST /v1/office/requests/{id}/accept) with that start and techIds.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":true,"description":"Local date"},{"schema":{"type":"number","minimum":1,"maximum":2500},"in":"query","name":"radius","required":false},{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"date":{"type":"string"},"units":{"type":"string","enum":["mi","km"]},"radius":{"type":"number"},"outsideArea":{"type":"boolean","description":"Outside every service area: out-of-area requests are booked with anyone qualified"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"distance":{"type":"number","description":"Straight line from home, in `units`"},"driveMinutes":{"type":"number","description":"From home"},"slots":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"},"driveMinutes":{"type":"number","description":"One-way drive from the tech's previous stop that day, else home (rough road estimate)"},"from":{"type":"string","enum":["home","previous_job"]}},"required":["start","local","driveMinutes","from"],"additionalProperties":false}}},"required":["id","name","distance","driveMinutes","slots"],"additionalProperties":false}},"crews":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}}},"required":["start","local","techs"],"additionalProperties":false}}},"required":["jobId","date","units","radius","outsideArea","techs","crews"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/intake/{id}/discard":{"post":{"summary":"Drop a job from the intake queue","tags":["Intake"],"description":"A queued or out-of-area request is declined: the customer is texted that you can't make it (with `message` added, e.g. a referral), unless notify is false. A bumped job (it had a time before) is cancelled instead, with the usual cancellation text and booking.cancelled webhook.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","maxLength":300},"notify":{"type":"boolean","default":true}},"additionalProperties":false,"default":{}}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"status":{"type":"string","enum":["cancelled"]}},"required":["jobId","status"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/reports/status":{"get":{"summary":"Where the jobs filed in a date range stand now, the request funnel and cycle time","tags":["Reports"],"description":"Tiles count the jobs filed (created) in from..to (local dates) by where they stand now: unscheduled, scheduled (upcoming, not overdue), overdue (the visit ended, not marked done), needs attention, completed and cancelled, with completed and cancelled as a share of filed. The funnel follows the same jobs: filed, scheduled (put on the schedule at some point and not cancelled), visited (the tech arrived) and completed. Cycle time: for jobs marked done in the range, hours from when the job went on the schedule (when it was booked, or when the office placed it from the queue) to the done tap; median and 90th percentile. clientCompanyId (or client, its CrewLink ID) narrows everything to work that company sent you. Up to 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"clientCompanyId","required":false,"description":"Work for this client company"},{"schema":{"type":"string","maxLength":80},"in":"query","name":"client","required":false,"description":"The same by the client's CrewLink ID"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"metricsFrom":{"type":"string","nullable":true,"description":"\"Count KPIs from\" (local date): nothing before it counts. null: no limit"},"tiles":{"type":"object","properties":{"created":{"type":"number"},"unscheduled":{"type":"number"},"scheduled":{"type":"number"},"overdue":{"type":"number"},"needsAttention":{"type":"number"},"completed":{"type":"number"},"completedPct":{"type":"number","nullable":true},"cancelled":{"type":"number"},"cancelledPct":{"type":"number","nullable":true}},"required":["created","unscheduled","scheduled","overdue","needsAttention","completed","completedPct","cancelled","cancelledPct"],"additionalProperties":false},"funnel":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","enum":["filed","scheduled","visited","completed"]},"label":{"type":"string"},"count":{"type":"number"}},"required":["key","label","count"],"additionalProperties":false}},"cycle":{"type":"object","properties":{"n":{"type":"number"},"medianHours":{"type":"number","nullable":true},"p90Hours":{"type":"number","nullable":true}},"required":["n","medianHours","p90Hours"],"additionalProperties":false}},"required":["from","to","metricsFrom","tiles","funnel","cycle"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/reports/heatmap":{"get":{"summary":"Where the work is: jobs and techs per ~5 km cell, by state and by branch","tags":["Reports"],"description":"Jobs with a visit in from..to (and unscheduled jobs filed in it), not cancelled, counted per cell of about 5.5 km (a twentieth of a degree; the cell's centre is given, never an address), with the active techs whose home is in each cell. byState reads the state from the address where it can; byBranch counts each job at its nearest branch. Your branches come with their service areas, to draw the map. Up to 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"clientCompanyId","required":false,"description":"Work for this client company"},{"schema":{"type":"string","maxLength":80},"in":"query","name":"client","required":false,"description":"The same by the client's CrewLink ID"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"metricsFrom":{"type":"string","nullable":true,"description":"\"Count KPIs from\" (local date): nothing before it counts. null: no limit"},"jobs":{"type":"number"},"techs":{"type":"number"},"cells":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"lat":{"type":"number"},"lng":{"type":"number"},"jobs":{"type":"number"},"techs":{"type":"number"}},"required":["key","lat","lng","jobs","techs"],"additionalProperties":false}},"branches":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"lat":{"type":"number"},"lng":{"type":"number"},"areaKm":{"type":"number"}},"required":["id","name","lat","lng","areaKm"],"additionalProperties":false}},"byState":{"type":"array","items":{"type":"object","properties":{"state":{"type":"string"},"jobs":{"type":"number"}},"required":["state","jobs"],"additionalProperties":false}},"byBranch":{"type":"array","items":{"type":"object","properties":{"branch":{"type":"string"},"jobs":{"type":"number"}},"required":["branch","jobs"],"additionalProperties":false}}},"required":["from","to","metricsFrom","jobs","techs","cells","branches","byState","byBranch"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/sites":{"get":{"summary":"Every site (one per property per client company), with its visits","tags":["Office"],"description":"One row per site: a property for one client company (or your own work), holding every visit ever made there. The visit filters are those of GET /v1/office/jobs (q, jobStatus, tab, from/to, filedFrom/filedTo, techId, serviceId, source, clientCompanyId, client, status): a site is listed when any of its visits matches, and shows all of them. q finds the site by customer name, phone, address, city or any visit's external id; #123 by a visit id. externalSource + externalRef finds the site by its own ids or any visit's. siteStatus filters the site itself (active, idle, merged); updatedSince its last activity. Page with nextCursor (null on the last page). Platform keys see techs as first name and last initial.","parameters":[{"schema":{"type":"string","pattern":"^[a-z_]+(,[a-z_]+)*$"},"in":"query","name":"status","required":false,"description":"The stored status: one or more of booked, held, waiting, queued, unscheduled, declined, cancelled, passed, comma separated"},{"schema":{"type":"string","pattern":"^(unscheduled|scheduled|en_route|on_site|done|cancelled)(,(unscheduled|scheduled|en_route|on_site|done|cancelled))*$"},"in":"query","name":"jobStatus","required":false,"description":"What people see: one or more of unscheduled, scheduled, en_route, on_site, done, cancelled"},{"schema":{"type":"string","enum":["all","unscheduled","scheduled","overdue","needs_attention","done","cancelled"]},"in":"query","name":"tab","required":false,"description":"An intake tab: all, unscheduled, scheduled (upcoming, not overdue), overdue, needs_attention, done, cancelled"},{"schema":{"type":"string","maxLength":200},"in":"query","name":"q","required":false,"description":"Customer name, phone (4+ digits), address or city, an external id; #123 for one job"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false,"description":"Jobs starting on or after this local date"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false,"description":"Jobs starting on or before this local date"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"filedFrom","required":false,"description":"Filed (created) on or after this local date"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"filedTo","required":false,"description":"Filed (created) on or before this local date"},{"schema":{"type":"integer"},"in":"query","name":"techId","required":false,"description":"Jobs with this person on them"},{"schema":{"type":"integer"},"in":"query","name":"serviceId","required":false},{"schema":{"type":"string","pattern":"^(office|api|platform|booking_page|client|out_of_area_request|crewconnect|recurring|emergency|import|share_link)(,(office|api|platform|booking_page|client|out_of_area_request|crewconnect|recurring|emergency|import|share_link))*$"},"in":"query","name":"source","required":false,"description":"Where it came from: office, api, platform, booking_page, client, out_of_area_request, crewconnect, recurring, emergency, import, share_link"},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"clientCompanyId","required":false,"description":"Work for this client company"},{"schema":{"type":"string","maxLength":80},"in":"query","name":"client","required":false,"description":"The same by the client's CrewLink ID"},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"updatedSince","required":false,"description":"Jobs changed at or after this instant"},{"schema":{"type":"string","maxLength":100},"in":"query","name":"externalSource","required":false},{"schema":{"type":"string","maxLength":200},"in":"query","name":"externalRef","required":false},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"siteId","required":false,"description":"Only the visits of this site"},{"schema":{"type":"string","maxLength":400},"in":"query","name":"cursor","required":false,"description":"nextCursor from the previous page"},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"string","pattern":"^(active|idle|merged)(,(active|idle|merged))*$"},"in":"query","name":"siteStatus","required":false,"description":"The site's status: active, idle, merged (comma separated). Merged sites are left out unless asked for"},{"schema":{"type":"string","enum":["updated_asc","updated_desc","filed_desc","filed_asc"]},"in":"query","name":"sort","required":false,"description":"updated_asc (default, for syncing with updatedSince), updated_desc (most recent activity first), filed_desc, filed_asc (by its first visit)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"sites":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number","description":"The site id (siteId elsewhere)"},"status":{"type":"string","enum":["active","idle","merged"],"description":"active (a visit is scheduled, has no time yet, or needs attention), idle (nothing to do now) or merged (folded into mergedIntoId)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true,"description":"The customer of its most recent visit (the homeowner can change)"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company it is for (null: your own work)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"The site's ids, by source: its most recent visit's, plus PATCH"},"mergedIntoId":{"type":"number","nullable":true},"firstFiledAt":{"type":"string","description":"When its first visit was filed"},"updatedAt":{"type":"string","description":"Last activity"},"counts":{"type":"object","properties":{"visits":{"type":"number","description":"Every visit (holds aside)"},"active":{"type":"number","description":"Still to do (scheduled or without a time yet) or needing attention"},"done":{"type":"number"},"cancelled":{"type":"number"}},"required":["visits","active","done","cancelled"],"additionalProperties":false},"lastVisit":{"type":"object","properties":{"jobId":{"type":"number"},"at":{"type":"string","description":"When it was done"},"service":{"type":"string"}},"required":["jobId","at","service"],"additionalProperties":false,"nullable":true,"description":"The most recent visit done"},"nextVisit":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"service":{"type":"string"}},"required":["jobId","start","service"],"additionalProperties":false,"nullable":true,"description":"The next scheduled visit not yet done"},"visits":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"status":{"type":"string","enum":["booked","held","waiting","queued","unscheduled","declined","cancelled","passed"],"description":"The stored status: booked; held (a hold not booked yet); waiting (an out-of-area request); queued (in the intake queue: nobody could take it at the time asked); unscheduled (moved off the board, no new time yet); declined; cancelled; passed (CrewConnect: passed on to a partner)"},"stage":{"type":"string","enum":["scheduled","en_route","on_site","done"]},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"What people see: unscheduled (no time yet: waiting, queued or moved off the board), scheduled, en_route, on_site, done or cancelled (cancelled or declined)"},"overdue":{"type":"boolean","description":"Booked, not done, and its last visit has ended"},"overdueDays":{"type":"number","nullable":true,"description":"Whole days since the last visit ended, when overdue"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"start":{"type":"string","nullable":true,"description":"The real instant (UTC); null for waiting, queued, unscheduled and declined"},"local":{"type":"string","nullable":true,"description":"Wall clock in `timezone`, e.g. '2026-10-13T14:30'"},"end":{"type":"string","nullable":true,"description":"When the first day is booked to end (the longest visit), UTC"},"endLocal":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of local and endLocal (the zone of the address, or of the techs sent)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true,"description":"Read from the address (best effort)"},"state":{"type":"string","nullable":true,"description":"Two-letter US state read from the address, when there is one"},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true,"description":"The assigned technician (the first, when a crew goes)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false},"description":"Everyone assigned"},"externalRef":{"type":"string","nullable":true,"description":"The primary external id (see externalRefs)"},"externalSource":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Every external id on the job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" }"},"needsAttention":{"type":"object","properties":{"reason":{"type":"string"},"at":{"type":"string"}},"required":["reason","at"],"additionalProperties":false,"nullable":true},"doneAt":{"type":"string","nullable":true},"cancelledAfterDone":{"type":"boolean","description":"true: the job was cancelled after it was done (doneAt is kept; the techs' pay for it was taken back)"},"createdAt":{"type":"string","description":"When it was filed"},"updatedAt":{"type":"string"},"ageDays":{"type":"number","description":"Whole days since it was filed"},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"nullable":true,"description":"Queued and moved-off-the-board jobs: the time asked for (or the time it had)"},"requestedDays":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false},"nullable":true,"description":"Out-of-area requests: the days the customer picked"},"intakeReason":{"type":"string","nullable":true,"description":"Queued jobs: why it couldn't be booked when it was asked for"},"source":{"type":"string","enum":["office","api","platform","booking_page","client","out_of_area_request","crewconnect","recurring","emergency","import","share_link"],"description":"Where it came from"},"sentBy":{"type":"string","nullable":true,"description":"CrewConnect: the company that sent it"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"filedBy":{"type":"string","nullable":true,"description":"Who filed or booked it, e.g. \"Filed by Ann Lee · Globex Solar\""},"homeownerTexts":{"type":"boolean","description":"false: the homeowner gets no texts or emails about this job (booked with notify: false, the company's homeownerMessaging is off, or the platform that booked it has homeownerMessaging off on its grant)"},"outsideHours":{"type":"boolean","description":"true: booked outside its tech's working hours or on a day they don't work, honouring a time the old system had confirmed (POST /v1/office/jobs source import, allowOutsideHours). Their hours were not changed; a move clears it"},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"nullable":true,"description":"How exact lat/lng are, as reported when it was booked (geoPrecision on POST /v1/office/jobs): rooftop, address, street or locality. street and locality are approximate places someone confirmed. Null when not reported"},"siteId":{"type":"number","nullable":true,"description":"The site this visit belongs to: the property, for its client company, with every service ever done there (GET /v1/office/sites/{id}). Null until linked (a hold)"},"siteVisit":{"type":"object","properties":{"number":{"type":"number","nullable":true,"description":"Its place among the site's visits that aren't cancelled, oldest first; null when this one is cancelled"},"of":{"type":"number","description":"Visits at the site that aren't cancelled"},"revisit":{"type":"boolean","description":"A revisit service"}},"required":["number","of","revisit"],"additionalProperties":false,"nullable":true,"description":"\"Visit 3 at this site\" (number); null without a site"},"shareLinkId":{"type":"string","nullable":true,"description":"Booked or requested through this client share link (source share_link; GET /v1/office/clients/{clientId}/share-links), else null"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false,"nullable":true,"description":"The client's sales rep who booked it through a share link (null otherwise). filedBy then reads \"Booked by <rep> via link · <client>\""}},"required":["id","status","stage","jobStatus","overdue","overdueDays","service","start","local","end","endLocal","timezone","address","city","state","lat","lng","customer","tech","techs","externalRef","externalSource","externalRefs","needsAttention","doneAt","cancelledAfterDone","createdAt","updatedAt","ageDays","requested","requestedDays","intakeReason","source","sentBy","client","filedBy","homeownerTexts","outsideHours","geoPrecision","siteId","siteVisit","shareLinkId","bookedBy"],"additionalProperties":false},"description":"Every visit you may see, oldest filed first (siteVisit numbers them)"}},"required":["id","status","address","city","state","lat","lng","customer","client","externalRefs","mergedIntoId","firstFiledAt","updatedAt","counts","lastVisit","nextVisit","visits"],"additionalProperties":false}},"nextCursor":{"type":"string","nullable":true},"total":{"type":"number"}},"required":["sites","nextCursor","total"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/sites/{id}":{"get":{"summary":"One site: every visit and the whole timeline","tags":["Office"],"description":"The site with its visits and one timeline, oldest first: each visit requested or booked, the tech on the way, arrived, done, notes, check-ins, attention flags, cancellations, photos and texts to the customer, plus the site's own history (created, visits linked, merged, split, ids changed). A merged site answers with status merged and mergedIntoId (its visits now live there). Platform keys see techs as first name and last initial and no thread messages.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number","description":"The site id (siteId elsewhere)"},"status":{"type":"string","enum":["active","idle","merged"],"description":"active (a visit is scheduled, has no time yet, or needs attention), idle (nothing to do now) or merged (folded into mergedIntoId)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true,"description":"The customer of its most recent visit (the homeowner can change)"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company it is for (null: your own work)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"The site's ids, by source: its most recent visit's, plus PATCH"},"mergedIntoId":{"type":"number","nullable":true},"firstFiledAt":{"type":"string","description":"When its first visit was filed"},"updatedAt":{"type":"string","description":"Last activity"},"counts":{"type":"object","properties":{"visits":{"type":"number","description":"Every visit (holds aside)"},"active":{"type":"number","description":"Still to do (scheduled or without a time yet) or needing attention"},"done":{"type":"number"},"cancelled":{"type":"number"}},"required":["visits","active","done","cancelled"],"additionalProperties":false},"lastVisit":{"type":"object","properties":{"jobId":{"type":"number"},"at":{"type":"string","description":"When it was done"},"service":{"type":"string"}},"required":["jobId","at","service"],"additionalProperties":false,"nullable":true,"description":"The most recent visit done"},"nextVisit":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"service":{"type":"string"}},"required":["jobId","start","service"],"additionalProperties":false,"nullable":true,"description":"The next scheduled visit not yet done"},"visits":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"status":{"type":"string","enum":["booked","held","waiting","queued","unscheduled","declined","cancelled","passed"],"description":"The stored status: booked; held (a hold not booked yet); waiting (an out-of-area request); queued (in the intake queue: nobody could take it at the time asked); unscheduled (moved off the board, no new time yet); declined; cancelled; passed (CrewConnect: passed on to a partner)"},"stage":{"type":"string","enum":["scheduled","en_route","on_site","done"]},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"What people see: unscheduled (no time yet: waiting, queued or moved off the board), scheduled, en_route, on_site, done or cancelled (cancelled or declined)"},"overdue":{"type":"boolean","description":"Booked, not done, and its last visit has ended"},"overdueDays":{"type":"number","nullable":true,"description":"Whole days since the last visit ended, when overdue"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"start":{"type":"string","nullable":true,"description":"The real instant (UTC); null for waiting, queued, unscheduled and declined"},"local":{"type":"string","nullable":true,"description":"Wall clock in `timezone`, e.g. '2026-10-13T14:30'"},"end":{"type":"string","nullable":true,"description":"When the first day is booked to end (the longest visit), UTC"},"endLocal":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of local and endLocal (the zone of the address, or of the techs sent)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true,"description":"Read from the address (best effort)"},"state":{"type":"string","nullable":true,"description":"Two-letter US state read from the address, when there is one"},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true,"description":"The assigned technician (the first, when a crew goes)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false},"description":"Everyone assigned"},"externalRef":{"type":"string","nullable":true,"description":"The primary external id (see externalRefs)"},"externalSource":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Every external id on the job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" }"},"needsAttention":{"type":"object","properties":{"reason":{"type":"string"},"at":{"type":"string"}},"required":["reason","at"],"additionalProperties":false,"nullable":true},"doneAt":{"type":"string","nullable":true},"cancelledAfterDone":{"type":"boolean","description":"true: the job was cancelled after it was done (doneAt is kept; the techs' pay for it was taken back)"},"createdAt":{"type":"string","description":"When it was filed"},"updatedAt":{"type":"string"},"ageDays":{"type":"number","description":"Whole days since it was filed"},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"nullable":true,"description":"Queued and moved-off-the-board jobs: the time asked for (or the time it had)"},"requestedDays":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false},"nullable":true,"description":"Out-of-area requests: the days the customer picked"},"intakeReason":{"type":"string","nullable":true,"description":"Queued jobs: why it couldn't be booked when it was asked for"},"source":{"type":"string","enum":["office","api","platform","booking_page","client","out_of_area_request","crewconnect","recurring","emergency","import","share_link"],"description":"Where it came from"},"sentBy":{"type":"string","nullable":true,"description":"CrewConnect: the company that sent it"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"filedBy":{"type":"string","nullable":true,"description":"Who filed or booked it, e.g. \"Filed by Ann Lee · Globex Solar\""},"homeownerTexts":{"type":"boolean","description":"false: the homeowner gets no texts or emails about this job (booked with notify: false, the company's homeownerMessaging is off, or the platform that booked it has homeownerMessaging off on its grant)"},"outsideHours":{"type":"boolean","description":"true: booked outside its tech's working hours or on a day they don't work, honouring a time the old system had confirmed (POST /v1/office/jobs source import, allowOutsideHours). Their hours were not changed; a move clears it"},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"nullable":true,"description":"How exact lat/lng are, as reported when it was booked (geoPrecision on POST /v1/office/jobs): rooftop, address, street or locality. street and locality are approximate places someone confirmed. Null when not reported"},"siteId":{"type":"number","nullable":true,"description":"The site this visit belongs to: the property, for its client company, with every service ever done there (GET /v1/office/sites/{id}). Null until linked (a hold)"},"siteVisit":{"type":"object","properties":{"number":{"type":"number","nullable":true,"description":"Its place among the site's visits that aren't cancelled, oldest first; null when this one is cancelled"},"of":{"type":"number","description":"Visits at the site that aren't cancelled"},"revisit":{"type":"boolean","description":"A revisit service"}},"required":["number","of","revisit"],"additionalProperties":false,"nullable":true,"description":"\"Visit 3 at this site\" (number); null without a site"},"shareLinkId":{"type":"string","nullable":true,"description":"Booked or requested through this client share link (source share_link; GET /v1/office/clients/{clientId}/share-links), else null"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false,"nullable":true,"description":"The client's sales rep who booked it through a share link (null otherwise). filedBy then reads \"Booked by <rep> via link · <client>\""}},"required":["id","status","stage","jobStatus","overdue","overdueDays","service","start","local","end","endLocal","timezone","address","city","state","lat","lng","customer","tech","techs","externalRef","externalSource","externalRefs","needsAttention","doneAt","cancelledAfterDone","createdAt","updatedAt","ageDays","requested","requestedDays","intakeReason","source","sentBy","client","filedBy","homeownerTexts","outsideHours","geoPrecision","siteId","siteVisit","shareLinkId","bookedBy"],"additionalProperties":false},"description":"Every visit you may see, oldest filed first (siteVisit numbers them)"},"timeline":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","description":"requested, booked, en_route, arrived, checkin, done, note, attention, attention_cleared, history_corrected, cancelled, photo, text; and for the site itself: site_created, linked, merged, merged_into, split, split_from, refs"},"at":{"type":"string"},"jobId":{"type":"number","nullable":true,"description":"The visit it is about"},"visit":{"type":"number","nullable":true,"description":"That visit's place at the site (1 = the first filed)"},"service":{"type":"string","nullable":true},"by":{"type":"string","enum":["tech","office","customer","rep","system"],"description":"rep: a client's sales rep booked it through a share link (text then says who)"},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true},"text":{"type":"string","nullable":true}},"required":["kind","at","jobId","visit","service","by","tech","text"],"additionalProperties":false},"description":"Oldest first"},"activity":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"photos":{"type":"number"},"texts":{"type":"number","description":"Texts to the customer"},"messages":{"type":"number","description":"Messages in its thread (0 for platform keys)"}},"required":["jobId","photos","texts","messages"],"additionalProperties":false}}},"required":["id","status","address","city","state","lat","lng","customer","client","externalRefs","mergedIntoId","firstFiledAt","updatedAt","counts","lastVisit","nextVisit","visits","timeline","activity"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"patch":{"summary":"Set or clear a site's own external ids","tags":["Office"],"description":"externalRefs: { source: id } sets that source's id for the site, { source: null } removes it; other sources are kept. Audited in the timeline; sends site.updated. 409 already_merged for a merged site. Platform keys need write.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200,"nullable":true},"description":"{ source: id } to set, { source: null } to remove; other sources are kept"}},"required":["externalRefs"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number","description":"The site id (siteId elsewhere)"},"status":{"type":"string","enum":["active","idle","merged"],"description":"active (a visit is scheduled, has no time yet, or needs attention), idle (nothing to do now) or merged (folded into mergedIntoId)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true,"description":"The customer of its most recent visit (the homeowner can change)"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company it is for (null: your own work)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"The site's ids, by source: its most recent visit's, plus PATCH"},"mergedIntoId":{"type":"number","nullable":true},"firstFiledAt":{"type":"string","description":"When its first visit was filed"},"updatedAt":{"type":"string","description":"Last activity"},"counts":{"type":"object","properties":{"visits":{"type":"number","description":"Every visit (holds aside)"},"active":{"type":"number","description":"Still to do (scheduled or without a time yet) or needing attention"},"done":{"type":"number"},"cancelled":{"type":"number"}},"required":["visits","active","done","cancelled"],"additionalProperties":false},"lastVisit":{"type":"object","properties":{"jobId":{"type":"number"},"at":{"type":"string","description":"When it was done"},"service":{"type":"string"}},"required":["jobId","at","service"],"additionalProperties":false,"nullable":true,"description":"The most recent visit done"},"nextVisit":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"service":{"type":"string"}},"required":["jobId","start","service"],"additionalProperties":false,"nullable":true,"description":"The next scheduled visit not yet done"},"visits":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"status":{"type":"string","enum":["booked","held","waiting","queued","unscheduled","declined","cancelled","passed"],"description":"The stored status: booked; held (a hold not booked yet); waiting (an out-of-area request); queued (in the intake queue: nobody could take it at the time asked); unscheduled (moved off the board, no new time yet); declined; cancelled; passed (CrewConnect: passed on to a partner)"},"stage":{"type":"string","enum":["scheduled","en_route","on_site","done"]},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"What people see: unscheduled (no time yet: waiting, queued or moved off the board), scheduled, en_route, on_site, done or cancelled (cancelled or declined)"},"overdue":{"type":"boolean","description":"Booked, not done, and its last visit has ended"},"overdueDays":{"type":"number","nullable":true,"description":"Whole days since the last visit ended, when overdue"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"start":{"type":"string","nullable":true,"description":"The real instant (UTC); null for waiting, queued, unscheduled and declined"},"local":{"type":"string","nullable":true,"description":"Wall clock in `timezone`, e.g. '2026-10-13T14:30'"},"end":{"type":"string","nullable":true,"description":"When the first day is booked to end (the longest visit), UTC"},"endLocal":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of local and endLocal (the zone of the address, or of the techs sent)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true,"description":"Read from the address (best effort)"},"state":{"type":"string","nullable":true,"description":"Two-letter US state read from the address, when there is one"},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true,"description":"The assigned technician (the first, when a crew goes)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false},"description":"Everyone assigned"},"externalRef":{"type":"string","nullable":true,"description":"The primary external id (see externalRefs)"},"externalSource":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Every external id on the job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" }"},"needsAttention":{"type":"object","properties":{"reason":{"type":"string"},"at":{"type":"string"}},"required":["reason","at"],"additionalProperties":false,"nullable":true},"doneAt":{"type":"string","nullable":true},"cancelledAfterDone":{"type":"boolean","description":"true: the job was cancelled after it was done (doneAt is kept; the techs' pay for it was taken back)"},"createdAt":{"type":"string","description":"When it was filed"},"updatedAt":{"type":"string"},"ageDays":{"type":"number","description":"Whole days since it was filed"},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"nullable":true,"description":"Queued and moved-off-the-board jobs: the time asked for (or the time it had)"},"requestedDays":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false},"nullable":true,"description":"Out-of-area requests: the days the customer picked"},"intakeReason":{"type":"string","nullable":true,"description":"Queued jobs: why it couldn't be booked when it was asked for"},"source":{"type":"string","enum":["office","api","platform","booking_page","client","out_of_area_request","crewconnect","recurring","emergency","import","share_link"],"description":"Where it came from"},"sentBy":{"type":"string","nullable":true,"description":"CrewConnect: the company that sent it"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"filedBy":{"type":"string","nullable":true,"description":"Who filed or booked it, e.g. \"Filed by Ann Lee · Globex Solar\""},"homeownerTexts":{"type":"boolean","description":"false: the homeowner gets no texts or emails about this job (booked with notify: false, the company's homeownerMessaging is off, or the platform that booked it has homeownerMessaging off on its grant)"},"outsideHours":{"type":"boolean","description":"true: booked outside its tech's working hours or on a day they don't work, honouring a time the old system had confirmed (POST /v1/office/jobs source import, allowOutsideHours). Their hours were not changed; a move clears it"},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"nullable":true,"description":"How exact lat/lng are, as reported when it was booked (geoPrecision on POST /v1/office/jobs): rooftop, address, street or locality. street and locality are approximate places someone confirmed. Null when not reported"},"siteId":{"type":"number","nullable":true,"description":"The site this visit belongs to: the property, for its client company, with every service ever done there (GET /v1/office/sites/{id}). Null until linked (a hold)"},"siteVisit":{"type":"object","properties":{"number":{"type":"number","nullable":true,"description":"Its place among the site's visits that aren't cancelled, oldest first; null when this one is cancelled"},"of":{"type":"number","description":"Visits at the site that aren't cancelled"},"revisit":{"type":"boolean","description":"A revisit service"}},"required":["number","of","revisit"],"additionalProperties":false,"nullable":true,"description":"\"Visit 3 at this site\" (number); null without a site"},"shareLinkId":{"type":"string","nullable":true,"description":"Booked or requested through this client share link (source share_link; GET /v1/office/clients/{clientId}/share-links), else null"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false,"nullable":true,"description":"The client's sales rep who booked it through a share link (null otherwise). filedBy then reads \"Booked by <rep> via link · <client>\""}},"required":["id","status","stage","jobStatus","overdue","overdueDays","service","start","local","end","endLocal","timezone","address","city","state","lat","lng","customer","tech","techs","externalRef","externalSource","externalRefs","needsAttention","doneAt","cancelledAfterDone","createdAt","updatedAt","ageDays","requested","requestedDays","intakeReason","source","sentBy","client","filedBy","homeownerTexts","outsideHours","geoPrecision","siteId","siteVisit","shareLinkId","bookedBy"],"additionalProperties":false},"description":"Every visit you may see, oldest filed first (siteVisit numbers them)"}},"required":["id","status","address","city","state","lat","lng","customer","client","externalRefs","mergedIntoId","firstFiledAt","updatedAt","counts","lastVisit","nextVisit","visits"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/sites/{id}/merge":{"post":{"summary":"Merge another site into this one","tags":["Office"],"description":"Every visit of fromSiteId moves to this site (the survivor); their ids join this site's (this site's win where both have the same source); the absorbed site is kept with status merged and mergedIntoId (never deleted), so GET /v1/office/sites/{fromSiteId} still answers. The same client company, or your own work into a client's site (it keeps the client); two client companies never (422 different_client), nor a client's site into your own work. 409 already_merged when either was merged before. Audited on both timelines; sends site.updated and site.merged for the survivor. Platform keys need write.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fromSiteId":{"type":"integer","exclusiveMinimum":true,"minimum":0}},"required":["fromSiteId"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number","description":"The site id (siteId elsewhere)"},"status":{"type":"string","enum":["active","idle","merged"],"description":"active (a visit is scheduled, has no time yet, or needs attention), idle (nothing to do now) or merged (folded into mergedIntoId)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true,"description":"The customer of its most recent visit (the homeowner can change)"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company it is for (null: your own work)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"The site's ids, by source: its most recent visit's, plus PATCH"},"mergedIntoId":{"type":"number","nullable":true},"firstFiledAt":{"type":"string","description":"When its first visit was filed"},"updatedAt":{"type":"string","description":"Last activity"},"counts":{"type":"object","properties":{"visits":{"type":"number","description":"Every visit (holds aside)"},"active":{"type":"number","description":"Still to do (scheduled or without a time yet) or needing attention"},"done":{"type":"number"},"cancelled":{"type":"number"}},"required":["visits","active","done","cancelled"],"additionalProperties":false},"lastVisit":{"type":"object","properties":{"jobId":{"type":"number"},"at":{"type":"string","description":"When it was done"},"service":{"type":"string"}},"required":["jobId","at","service"],"additionalProperties":false,"nullable":true,"description":"The most recent visit done"},"nextVisit":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"service":{"type":"string"}},"required":["jobId","start","service"],"additionalProperties":false,"nullable":true,"description":"The next scheduled visit not yet done"},"visits":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"status":{"type":"string","enum":["booked","held","waiting","queued","unscheduled","declined","cancelled","passed"],"description":"The stored status: booked; held (a hold not booked yet); waiting (an out-of-area request); queued (in the intake queue: nobody could take it at the time asked); unscheduled (moved off the board, no new time yet); declined; cancelled; passed (CrewConnect: passed on to a partner)"},"stage":{"type":"string","enum":["scheduled","en_route","on_site","done"]},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"What people see: unscheduled (no time yet: waiting, queued or moved off the board), scheduled, en_route, on_site, done or cancelled (cancelled or declined)"},"overdue":{"type":"boolean","description":"Booked, not done, and its last visit has ended"},"overdueDays":{"type":"number","nullable":true,"description":"Whole days since the last visit ended, when overdue"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"start":{"type":"string","nullable":true,"description":"The real instant (UTC); null for waiting, queued, unscheduled and declined"},"local":{"type":"string","nullable":true,"description":"Wall clock in `timezone`, e.g. '2026-10-13T14:30'"},"end":{"type":"string","nullable":true,"description":"When the first day is booked to end (the longest visit), UTC"},"endLocal":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of local and endLocal (the zone of the address, or of the techs sent)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true,"description":"Read from the address (best effort)"},"state":{"type":"string","nullable":true,"description":"Two-letter US state read from the address, when there is one"},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true,"description":"The assigned technician (the first, when a crew goes)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false},"description":"Everyone assigned"},"externalRef":{"type":"string","nullable":true,"description":"The primary external id (see externalRefs)"},"externalSource":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Every external id on the job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" }"},"needsAttention":{"type":"object","properties":{"reason":{"type":"string"},"at":{"type":"string"}},"required":["reason","at"],"additionalProperties":false,"nullable":true},"doneAt":{"type":"string","nullable":true},"cancelledAfterDone":{"type":"boolean","description":"true: the job was cancelled after it was done (doneAt is kept; the techs' pay for it was taken back)"},"createdAt":{"type":"string","description":"When it was filed"},"updatedAt":{"type":"string"},"ageDays":{"type":"number","description":"Whole days since it was filed"},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"nullable":true,"description":"Queued and moved-off-the-board jobs: the time asked for (or the time it had)"},"requestedDays":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false},"nullable":true,"description":"Out-of-area requests: the days the customer picked"},"intakeReason":{"type":"string","nullable":true,"description":"Queued jobs: why it couldn't be booked when it was asked for"},"source":{"type":"string","enum":["office","api","platform","booking_page","client","out_of_area_request","crewconnect","recurring","emergency","import","share_link"],"description":"Where it came from"},"sentBy":{"type":"string","nullable":true,"description":"CrewConnect: the company that sent it"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"filedBy":{"type":"string","nullable":true,"description":"Who filed or booked it, e.g. \"Filed by Ann Lee · Globex Solar\""},"homeownerTexts":{"type":"boolean","description":"false: the homeowner gets no texts or emails about this job (booked with notify: false, the company's homeownerMessaging is off, or the platform that booked it has homeownerMessaging off on its grant)"},"outsideHours":{"type":"boolean","description":"true: booked outside its tech's working hours or on a day they don't work, honouring a time the old system had confirmed (POST /v1/office/jobs source import, allowOutsideHours). Their hours were not changed; a move clears it"},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"nullable":true,"description":"How exact lat/lng are, as reported when it was booked (geoPrecision on POST /v1/office/jobs): rooftop, address, street or locality. street and locality are approximate places someone confirmed. Null when not reported"},"siteId":{"type":"number","nullable":true,"description":"The site this visit belongs to: the property, for its client company, with every service ever done there (GET /v1/office/sites/{id}). Null until linked (a hold)"},"siteVisit":{"type":"object","properties":{"number":{"type":"number","nullable":true,"description":"Its place among the site's visits that aren't cancelled, oldest first; null when this one is cancelled"},"of":{"type":"number","description":"Visits at the site that aren't cancelled"},"revisit":{"type":"boolean","description":"A revisit service"}},"required":["number","of","revisit"],"additionalProperties":false,"nullable":true,"description":"\"Visit 3 at this site\" (number); null without a site"},"shareLinkId":{"type":"string","nullable":true,"description":"Booked or requested through this client share link (source share_link; GET /v1/office/clients/{clientId}/share-links), else null"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false,"nullable":true,"description":"The client's sales rep who booked it through a share link (null otherwise). filedBy then reads \"Booked by <rep> via link · <client>\""}},"required":["id","status","stage","jobStatus","overdue","overdueDays","service","start","local","end","endLocal","timezone","address","city","state","lat","lng","customer","tech","techs","externalRef","externalSource","externalRefs","needsAttention","doneAt","cancelledAfterDone","createdAt","updatedAt","ageDays","requested","requestedDays","intakeReason","source","sentBy","client","filedBy","homeownerTexts","outsideHours","geoPrecision","siteId","siteVisit","shareLinkId","bookedBy"],"additionalProperties":false},"description":"Every visit you may see, oldest filed first (siteVisit numbers them)"},"mergedSiteId":{"type":"number"},"movedJobIds":{"type":"array","items":{"type":"number"}}},"required":["id","status","address","city","state","lat","lng","customer","client","externalRefs","mergedIntoId","firstFiledAt","updatedAt","counts","lastVisit","nextVisit","visits","mergedSiteId","movedJobIds"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/sites/{id}/split":{"post":{"summary":"Take a visit out of this site","tags":["Office"],"description":"jobId (a visit at this site) moves to toSiteId (another site of the same client company, or a client's site for your own work), or without it to a new site of its own (409 only_visit when it is the site's only visit). If it was the last visit, this site is kept as merged into toSiteId. Audited on both timelines; sends site.updated for each (site.created for a new one). The backfill never merges a split site back. Platform keys need write.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"integer","exclusiveMinimum":true,"minimum":0},"toSiteId":{"type":"integer","exclusiveMinimum":true,"minimum":0}},"required":["jobId"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"object","properties":{"id":{"type":"number","description":"The site id (siteId elsewhere)"},"status":{"type":"string","enum":["active","idle","merged"],"description":"active (a visit is scheduled, has no time yet, or needs attention), idle (nothing to do now) or merged (folded into mergedIntoId)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true,"description":"The customer of its most recent visit (the homeowner can change)"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company it is for (null: your own work)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"The site's ids, by source: its most recent visit's, plus PATCH"},"mergedIntoId":{"type":"number","nullable":true},"firstFiledAt":{"type":"string","description":"When its first visit was filed"},"updatedAt":{"type":"string","description":"Last activity"},"counts":{"type":"object","properties":{"visits":{"type":"number","description":"Every visit (holds aside)"},"active":{"type":"number","description":"Still to do (scheduled or without a time yet) or needing attention"},"done":{"type":"number"},"cancelled":{"type":"number"}},"required":["visits","active","done","cancelled"],"additionalProperties":false},"lastVisit":{"type":"object","properties":{"jobId":{"type":"number"},"at":{"type":"string","description":"When it was done"},"service":{"type":"string"}},"required":["jobId","at","service"],"additionalProperties":false,"nullable":true,"description":"The most recent visit done"},"nextVisit":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"service":{"type":"string"}},"required":["jobId","start","service"],"additionalProperties":false,"nullable":true,"description":"The next scheduled visit not yet done"},"visits":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"status":{"type":"string","enum":["booked","held","waiting","queued","unscheduled","declined","cancelled","passed"],"description":"The stored status: booked; held (a hold not booked yet); waiting (an out-of-area request); queued (in the intake queue: nobody could take it at the time asked); unscheduled (moved off the board, no new time yet); declined; cancelled; passed (CrewConnect: passed on to a partner)"},"stage":{"type":"string","enum":["scheduled","en_route","on_site","done"]},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"What people see: unscheduled (no time yet: waiting, queued or moved off the board), scheduled, en_route, on_site, done or cancelled (cancelled or declined)"},"overdue":{"type":"boolean","description":"Booked, not done, and its last visit has ended"},"overdueDays":{"type":"number","nullable":true,"description":"Whole days since the last visit ended, when overdue"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"start":{"type":"string","nullable":true,"description":"The real instant (UTC); null for waiting, queued, unscheduled and declined"},"local":{"type":"string","nullable":true,"description":"Wall clock in `timezone`, e.g. '2026-10-13T14:30'"},"end":{"type":"string","nullable":true,"description":"When the first day is booked to end (the longest visit), UTC"},"endLocal":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of local and endLocal (the zone of the address, or of the techs sent)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true,"description":"Read from the address (best effort)"},"state":{"type":"string","nullable":true,"description":"Two-letter US state read from the address, when there is one"},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true,"description":"The assigned technician (the first, when a crew goes)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false},"description":"Everyone assigned"},"externalRef":{"type":"string","nullable":true,"description":"The primary external id (see externalRefs)"},"externalSource":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Every external id on the job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" }"},"needsAttention":{"type":"object","properties":{"reason":{"type":"string"},"at":{"type":"string"}},"required":["reason","at"],"additionalProperties":false,"nullable":true},"doneAt":{"type":"string","nullable":true},"cancelledAfterDone":{"type":"boolean","description":"true: the job was cancelled after it was done (doneAt is kept; the techs' pay for it was taken back)"},"createdAt":{"type":"string","description":"When it was filed"},"updatedAt":{"type":"string"},"ageDays":{"type":"number","description":"Whole days since it was filed"},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"nullable":true,"description":"Queued and moved-off-the-board jobs: the time asked for (or the time it had)"},"requestedDays":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false},"nullable":true,"description":"Out-of-area requests: the days the customer picked"},"intakeReason":{"type":"string","nullable":true,"description":"Queued jobs: why it couldn't be booked when it was asked for"},"source":{"type":"string","enum":["office","api","platform","booking_page","client","out_of_area_request","crewconnect","recurring","emergency","import","share_link"],"description":"Where it came from"},"sentBy":{"type":"string","nullable":true,"description":"CrewConnect: the company that sent it"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"filedBy":{"type":"string","nullable":true,"description":"Who filed or booked it, e.g. \"Filed by Ann Lee · Globex Solar\""},"homeownerTexts":{"type":"boolean","description":"false: the homeowner gets no texts or emails about this job (booked with notify: false, the company's homeownerMessaging is off, or the platform that booked it has homeownerMessaging off on its grant)"},"outsideHours":{"type":"boolean","description":"true: booked outside its tech's working hours or on a day they don't work, honouring a time the old system had confirmed (POST /v1/office/jobs source import, allowOutsideHours). Their hours were not changed; a move clears it"},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"nullable":true,"description":"How exact lat/lng are, as reported when it was booked (geoPrecision on POST /v1/office/jobs): rooftop, address, street or locality. street and locality are approximate places someone confirmed. Null when not reported"},"siteId":{"type":"number","nullable":true,"description":"The site this visit belongs to: the property, for its client company, with every service ever done there (GET /v1/office/sites/{id}). Null until linked (a hold)"},"siteVisit":{"type":"object","properties":{"number":{"type":"number","nullable":true,"description":"Its place among the site's visits that aren't cancelled, oldest first; null when this one is cancelled"},"of":{"type":"number","description":"Visits at the site that aren't cancelled"},"revisit":{"type":"boolean","description":"A revisit service"}},"required":["number","of","revisit"],"additionalProperties":false,"nullable":true,"description":"\"Visit 3 at this site\" (number); null without a site"},"shareLinkId":{"type":"string","nullable":true,"description":"Booked or requested through this client share link (source share_link; GET /v1/office/clients/{clientId}/share-links), else null"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false,"nullable":true,"description":"The client's sales rep who booked it through a share link (null otherwise). filedBy then reads \"Booked by <rep> via link · <client>\""}},"required":["id","status","stage","jobStatus","overdue","overdueDays","service","start","local","end","endLocal","timezone","address","city","state","lat","lng","customer","tech","techs","externalRef","externalSource","externalRefs","needsAttention","doneAt","cancelledAfterDone","createdAt","updatedAt","ageDays","requested","requestedDays","intakeReason","source","sentBy","client","filedBy","homeownerTexts","outsideHours","geoPrecision","siteId","siteVisit","shareLinkId","bookedBy"],"additionalProperties":false},"description":"Every visit you may see, oldest filed first (siteVisit numbers them)"}},"required":["id","status","address","city","state","lat","lng","customer","client","externalRefs","mergedIntoId","firstFiledAt","updatedAt","counts","lastVisit","nextVisit","visits"],"additionalProperties":false,"nullable":true,"description":"This site after (null when it was emptied: merged into toSiteId)"},"to":{"type":"object","properties":{"id":{"type":"number","description":"The site id (siteId elsewhere)"},"status":{"type":"string","enum":["active","idle","merged"],"description":"active (a visit is scheduled, has no time yet, or needs attention), idle (nothing to do now) or merged (folded into mergedIntoId)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"state":{"type":"string","nullable":true},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true,"description":"The customer of its most recent visit (the homeowner can change)"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company it is for (null: your own work)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"The site's ids, by source: its most recent visit's, plus PATCH"},"mergedIntoId":{"type":"number","nullable":true},"firstFiledAt":{"type":"string","description":"When its first visit was filed"},"updatedAt":{"type":"string","description":"Last activity"},"counts":{"type":"object","properties":{"visits":{"type":"number","description":"Every visit (holds aside)"},"active":{"type":"number","description":"Still to do (scheduled or without a time yet) or needing attention"},"done":{"type":"number"},"cancelled":{"type":"number"}},"required":["visits","active","done","cancelled"],"additionalProperties":false},"lastVisit":{"type":"object","properties":{"jobId":{"type":"number"},"at":{"type":"string","description":"When it was done"},"service":{"type":"string"}},"required":["jobId","at","service"],"additionalProperties":false,"nullable":true,"description":"The most recent visit done"},"nextVisit":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"service":{"type":"string"}},"required":["jobId","start","service"],"additionalProperties":false,"nullable":true,"description":"The next scheduled visit not yet done"},"visits":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"status":{"type":"string","enum":["booked","held","waiting","queued","unscheduled","declined","cancelled","passed"],"description":"The stored status: booked; held (a hold not booked yet); waiting (an out-of-area request); queued (in the intake queue: nobody could take it at the time asked); unscheduled (moved off the board, no new time yet); declined; cancelled; passed (CrewConnect: passed on to a partner)"},"stage":{"type":"string","enum":["scheduled","en_route","on_site","done"]},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"What people see: unscheduled (no time yet: waiting, queued or moved off the board), scheduled, en_route, on_site, done or cancelled (cancelled or declined)"},"overdue":{"type":"boolean","description":"Booked, not done, and its last visit has ended"},"overdueDays":{"type":"number","nullable":true,"description":"Whole days since the last visit ended, when overdue"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"start":{"type":"string","nullable":true,"description":"The real instant (UTC); null for waiting, queued, unscheduled and declined"},"local":{"type":"string","nullable":true,"description":"Wall clock in `timezone`, e.g. '2026-10-13T14:30'"},"end":{"type":"string","nullable":true,"description":"When the first day is booked to end (the longest visit), UTC"},"endLocal":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of local and endLocal (the zone of the address, or of the techs sent)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true,"description":"Read from the address (best effort)"},"state":{"type":"string","nullable":true,"description":"Two-letter US state read from the address, when there is one"},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true,"description":"The assigned technician (the first, when a crew goes)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false},"description":"Everyone assigned"},"externalRef":{"type":"string","nullable":true,"description":"The primary external id (see externalRefs)"},"externalSource":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Every external id on the job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" }"},"needsAttention":{"type":"object","properties":{"reason":{"type":"string"},"at":{"type":"string"}},"required":["reason","at"],"additionalProperties":false,"nullable":true},"doneAt":{"type":"string","nullable":true},"cancelledAfterDone":{"type":"boolean","description":"true: the job was cancelled after it was done (doneAt is kept; the techs' pay for it was taken back)"},"createdAt":{"type":"string","description":"When it was filed"},"updatedAt":{"type":"string"},"ageDays":{"type":"number","description":"Whole days since it was filed"},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"nullable":true,"description":"Queued and moved-off-the-board jobs: the time asked for (or the time it had)"},"requestedDays":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false},"nullable":true,"description":"Out-of-area requests: the days the customer picked"},"intakeReason":{"type":"string","nullable":true,"description":"Queued jobs: why it couldn't be booked when it was asked for"},"source":{"type":"string","enum":["office","api","platform","booking_page","client","out_of_area_request","crewconnect","recurring","emergency","import","share_link"],"description":"Where it came from"},"sentBy":{"type":"string","nullable":true,"description":"CrewConnect: the company that sent it"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"filedBy":{"type":"string","nullable":true,"description":"Who filed or booked it, e.g. \"Filed by Ann Lee · Globex Solar\""},"homeownerTexts":{"type":"boolean","description":"false: the homeowner gets no texts or emails about this job (booked with notify: false, the company's homeownerMessaging is off, or the platform that booked it has homeownerMessaging off on its grant)"},"outsideHours":{"type":"boolean","description":"true: booked outside its tech's working hours or on a day they don't work, honouring a time the old system had confirmed (POST /v1/office/jobs source import, allowOutsideHours). Their hours were not changed; a move clears it"},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"nullable":true,"description":"How exact lat/lng are, as reported when it was booked (geoPrecision on POST /v1/office/jobs): rooftop, address, street or locality. street and locality are approximate places someone confirmed. Null when not reported"},"siteId":{"type":"number","nullable":true,"description":"The site this visit belongs to: the property, for its client company, with every service ever done there (GET /v1/office/sites/{id}). Null until linked (a hold)"},"siteVisit":{"type":"object","properties":{"number":{"type":"number","nullable":true,"description":"Its place among the site's visits that aren't cancelled, oldest first; null when this one is cancelled"},"of":{"type":"number","description":"Visits at the site that aren't cancelled"},"revisit":{"type":"boolean","description":"A revisit service"}},"required":["number","of","revisit"],"additionalProperties":false,"nullable":true,"description":"\"Visit 3 at this site\" (number); null without a site"},"shareLinkId":{"type":"string","nullable":true,"description":"Booked or requested through this client share link (source share_link; GET /v1/office/clients/{clientId}/share-links), else null"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false,"nullable":true,"description":"The client's sales rep who booked it through a share link (null otherwise). filedBy then reads \"Booked by <rep> via link · <client>\""}},"required":["id","status","stage","jobStatus","overdue","overdueDays","service","start","local","end","endLocal","timezone","address","city","state","lat","lng","customer","tech","techs","externalRef","externalSource","externalRefs","needsAttention","doneAt","cancelledAfterDone","createdAt","updatedAt","ageDays","requested","requestedDays","intakeReason","source","sentBy","client","filedBy","homeownerTexts","outsideHours","geoPrecision","siteId","siteVisit","shareLinkId","bookedBy"],"additionalProperties":false},"description":"Every visit you may see, oldest filed first (siteVisit numbers them)"}},"required":["id","status","address","city","state","lat","lng","customer","client","externalRefs","mergedIntoId","firstFiledAt","updatedAt","counts","lastVisit","nextVisit","visits"],"additionalProperties":false}},"required":["from","to"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/me/prefs":{"get":{"summary":"Your preferences and this sign-in (any role)","tags":["Users"],"description":"The signed-in person's display time zone, theme and colour-vision filter, the zone screens should use (theirs, else the company's), and what this sign-in is and when it ends. The company API key gets the defaults and `stored: false`.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"prefs":{"type":"object","properties":{"timeZone":{"type":"string","nullable":true,"description":"Your display zone (IANA, e.g. America/Denver), or null to use the company zone"},"theme":{"type":"string","enum":["device","light","blue","dark"]},"colorFilter":{"type":"string","enum":["none","protan","deutan","tritan"]}},"required":["timeZone","theme","colorFilter"],"additionalProperties":false},"timeZone":{"type":"string","description":"The zone screens show times in: yours, else the company zone"},"companyTimeZone":{"type":"string"},"stored":{"type":"boolean","description":"false for the company API key, which has no person to remember them for"},"session":{"type":"object","properties":{"kind":{"type":"string","enum":["session","user_token","api_key"],"description":"session: signed in with an emailed code or a passkey (30 days); user_token: a long-lived token for an integration or device; api_key: the company API key"},"user":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string"}},"required":["name","email","role"],"additionalProperties":false,"nullable":true},"startedAt":{"type":"string","nullable":true},"expiresAt":{"type":"string","nullable":true,"description":"When this sign-in ends; null for tokens and the API key, which last until replaced"}},"required":["kind","user","startedAt","expiresAt"],"additionalProperties":false}},"required":["prefs","timeZone","companyTimeZone","stored","session"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"patch":{"summary":"Change your preferences (any role)","tags":["Users"],"description":"Send any of timeZone (an IANA zone such as America/Denver, or null for the company zone), theme (device, light, blue, dark) and colorFilter (none, protan, deutan, tritan). Only your own. 422 for a zone CrewLink doesn't know. With the company API key nothing is kept (`stored: false`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"timeZone":{"type":"string","maxLength":64,"nullable":true},"theme":{"type":"string","enum":["device","light","blue","dark"]},"colorFilter":{"type":"string","enum":["none","protan","deutan","tritan"]}},"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"prefs":{"type":"object","properties":{"timeZone":{"type":"string","nullable":true,"description":"Your display zone (IANA, e.g. America/Denver), or null to use the company zone"},"theme":{"type":"string","enum":["device","light","blue","dark"]},"colorFilter":{"type":"string","enum":["none","protan","deutan","tritan"]}},"required":["timeZone","theme","colorFilter"],"additionalProperties":false},"timeZone":{"type":"string","description":"The zone screens show times in: yours, else the company zone"},"companyTimeZone":{"type":"string"},"stored":{"type":"boolean","description":"false for the company API key, which has no person to remember them for"},"session":{"type":"object","properties":{"kind":{"type":"string","enum":["session","user_token","api_key"],"description":"session: signed in with an emailed code or a passkey (30 days); user_token: a long-lived token for an integration or device; api_key: the company API key"},"user":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string"}},"required":["name","email","role"],"additionalProperties":false,"nullable":true},"startedAt":{"type":"string","nullable":true},"expiresAt":{"type":"string","nullable":true,"description":"When this sign-in ends; null for tokens and the API key, which last until replaced"}},"required":["kind","user","startedAt","expiresAt"],"additionalProperties":false}},"required":["prefs","timeZone","companyTimeZone","stored","session"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/me/analytics":{"get":{"summary":"Product analytics identity for this sign-in","tags":["Users"],"description":"What CrewLink's own apps send to product analytics (PostHog) for the signed-in person: the distinct id (your user id, or company:<id> for the company API key; never an email, phone or name), person properties (role, company_id, plan, trade, created_at), the `company` group with its properties (name, slug, kind, plan, plan_status, interval, trade, size bucket, created_at, is_demo, is_test, is_internal) and the properties sent with every event (env, is_demo, is_test, is_internal). enabled is false, with nothing else filled in, during a super admin's act-as session (reason act_as) or when the company turned product analytics off (reason opted_out, PATCH /v1/office/settings productAnalytics). CrewLink's own notes.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"reason":{"type":"string","enum":["act_as","opted_out"],"nullable":true},"distinctId":{"type":"string","nullable":true},"env":{"type":"string","description":"production, staging, development, test (ANALYTICS_ENV on the server)"},"person":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]}},"company":{"type":"object","properties":{"id":{"type":"string"},"properties":{"type":"object","additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"enum":["null"],"nullable":true}]}}},"required":["id","properties"],"additionalProperties":false,"nullable":true},"register":{"type":"object","properties":{"env":{"type":"string"},"is_demo":{"type":"boolean"},"is_test":{"type":"boolean"},"is_internal":{"type":"boolean"}},"required":["env","is_demo","is_test","is_internal"],"additionalProperties":false}},"required":["enabled","reason","distinctId","env","person","company","register"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}":{"get":{"summary":"One job in full: customer, visits, timeline, files, texts","tags":["Office"],"description":"Platform keys see techs as first name and last initial. Attachment `url`s are signed download links good for 15 minutes.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"status":{"type":"string","enum":["booked","held","waiting","queued","unscheduled","declined","cancelled","passed"],"description":"The stored status: booked; held (a hold not booked yet); waiting (an out-of-area request); queued (in the intake queue: nobody could take it at the time asked); unscheduled (moved off the board, no new time yet); declined; cancelled; passed (CrewConnect: passed on to a partner)"},"stage":{"type":"string","enum":["scheduled","en_route","on_site","done"]},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"],"description":"What people see: unscheduled (no time yet: waiting, queued or moved off the board), scheduled, en_route, on_site, done or cancelled (cancelled or declined)"},"overdue":{"type":"boolean","description":"Booked, not done, and its last visit has ended"},"overdueDays":{"type":"number","nullable":true,"description":"Whole days since the last visit ended, when overdue"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"start":{"type":"string","nullable":true,"description":"The real instant (UTC); null for waiting, queued, unscheduled and declined"},"local":{"type":"string","nullable":true,"description":"Wall clock in `timezone`, e.g. '2026-10-13T14:30'"},"end":{"type":"string","nullable":true,"description":"When the first day is booked to end (the longest visit), UTC"},"endLocal":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of local and endLocal (the zone of the address, or of the techs sent)"},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true,"description":"Read from the address (best effort)"},"state":{"type":"string","nullable":true,"description":"Two-letter US state read from the address, when there is one"},"lat":{"type":"number"},"lng":{"type":"number"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"},"email":{"type":"string","nullable":true}},"required":["name","phone","email"],"additionalProperties":false,"nullable":true},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true,"description":"The assigned technician (the first, when a crew goes)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false},"description":"Everyone assigned"},"externalRef":{"type":"string","nullable":true,"description":"The primary external id (see externalRefs)"},"externalSource":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Every external id on the job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" }"},"needsAttention":{"type":"object","properties":{"reason":{"type":"string"},"at":{"type":"string"}},"required":["reason","at"],"additionalProperties":false,"nullable":true},"doneAt":{"type":"string","nullable":true},"cancelledAfterDone":{"type":"boolean","description":"true: the job was cancelled after it was done (doneAt is kept; the techs' pay for it was taken back)"},"createdAt":{"type":"string","description":"When it was filed"},"updatedAt":{"type":"string"},"ageDays":{"type":"number","description":"Whole days since it was filed"},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"nullable":true,"description":"Queued and moved-off-the-board jobs: the time asked for (or the time it had)"},"requestedDays":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false},"nullable":true,"description":"Out-of-area requests: the days the customer picked"},"intakeReason":{"type":"string","nullable":true,"description":"Queued jobs: why it couldn't be booked when it was asked for"},"source":{"type":"string","enum":["office","api","platform","booking_page","client","out_of_area_request","crewconnect","recurring","emergency","import","share_link"],"description":"Where it came from"},"sentBy":{"type":"string","nullable":true,"description":"CrewConnect: the company that sent it"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"filedBy":{"type":"string","nullable":true,"description":"Who filed or booked it, e.g. \"Filed by Ann Lee · Globex Solar\""},"homeownerTexts":{"type":"boolean","description":"false: the homeowner gets no texts or emails about this job (booked with notify: false, the company's homeownerMessaging is off, or the platform that booked it has homeownerMessaging off on its grant)"},"outsideHours":{"type":"boolean","description":"true: booked outside its tech's working hours or on a day they don't work, honouring a time the old system had confirmed (POST /v1/office/jobs source import, allowOutsideHours). Their hours were not changed; a move clears it"},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"],"nullable":true,"description":"How exact lat/lng are, as reported when it was booked (geoPrecision on POST /v1/office/jobs): rooftop, address, street or locality. street and locality are approximate places someone confirmed. Null when not reported"},"siteId":{"type":"number","nullable":true,"description":"The site this visit belongs to: the property, for its client company, with every service ever done there (GET /v1/office/sites/{id}). Null until linked (a hold)"},"siteVisit":{"type":"object","properties":{"number":{"type":"number","nullable":true,"description":"Its place among the site's visits that aren't cancelled, oldest first; null when this one is cancelled"},"of":{"type":"number","description":"Visits at the site that aren't cancelled"},"revisit":{"type":"boolean","description":"A revisit service"}},"required":["number","of","revisit"],"additionalProperties":false,"nullable":true,"description":"\"Visit 3 at this site\" (number); null without a site"},"shareLinkId":{"type":"string","nullable":true,"description":"Booked or requested through this client share link (source share_link; GET /v1/office/clients/{clientId}/share-links), else null"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false,"nullable":true,"description":"The client's sales rep who booked it through a share link (null otherwise). filedBy then reads \"Booked by <rep> via link · <client>\""},"addressParts":{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":200},"line2":{"type":"string","maxLength":200},"city":{"type":"string","maxLength":100},"region":{"type":"string","maxLength":100,"description":"State or province"},"postcode":{"type":"string","maxLength":20},"country":{"type":"string","pattern":"^[A-Za-z]{2}$","description":"ISO 3166-1 alpha-2, e.g. US"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180}},"required":["line1"],"additionalProperties":false,"nullable":true},"notes":{"type":"string","nullable":true,"description":"The notes given at booking"},"visits":{"type":"array","items":{"type":"object","properties":{"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false},"date":{"type":"string"},"start":{"type":"string"},"end":{"type":"string"},"local":{"type":"string"},"endLocal":{"type":"string"}},"required":["tech","date","start","end","local","endLocal"],"additionalProperties":false}},"timeline":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","description":"created, en_route, arrived, checkin, done, note, attention, attention_cleared, history_corrected"},"at":{"type":"string"},"by":{"type":"string","enum":["tech","office","system"]},"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Platform keys: first name and last initial (\"Maria G.\"). Office keys: the full name"}},"required":["id","displayName"],"additionalProperties":false,"nullable":true},"text":{"type":"string","nullable":true}},"required":["kind","at","by","tech","text"],"additionalProperties":false}},"attachments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"jobId":{"type":"number"},"kind":{"type":"string","enum":["proposal","photo","document"]},"name":{"type":"string"},"contentType":{"type":"string"},"bytes":{"type":"number"},"status":{"type":"string","enum":["pending","uploaded"],"description":"pending: an upload URL was issued and not confirmed yet"},"uploadedBy":{"type":"object","properties":{"kind":{"type":"string","enum":["platform","user","company"]},"id":{"type":"string","nullable":true}},"required":["kind","id"],"additionalProperties":false,"description":"Who added it: a platform (its id), a user (their id) or the company API key (the company id)"},"createdAt":{"type":"string"},"uploadedAt":{"type":"string","nullable":true},"url":{"type":"string","nullable":true,"description":"Signed link to download it, good for 15 minutes; null until uploaded. Ask again for a fresh one"}},"required":["id","jobId","kind","name","contentType","bytes","status","uploadedBy","createdAt","uploadedAt","url"],"additionalProperties":false}},"textsCount":{"type":"number","description":"Texts to the customer about this job (GET /v1/office/jobs/{id}/texts lists them)"},"arrivedAt":{"type":"string","nullable":true},"enRouteAt":{"type":"string","nullable":true,"description":"The first On the way tap (UTC), else null. With arrivedAt and doneAt: the field timestamps"}},"required":["id","status","stage","jobStatus","overdue","overdueDays","service","start","local","end","endLocal","timezone","address","city","state","lat","lng","customer","tech","techs","externalRef","externalSource","externalRefs","needsAttention","doneAt","cancelledAfterDone","createdAt","updatedAt","ageDays","requested","requestedDays","intakeReason","source","sentBy","client","filedBy","homeownerTexts","outsideHours","geoPrecision","siteId","siteVisit","shareLinkId","bookedBy","addressParts","notes","visits","timeline","attachments","textsCount","arrivedAt","enRouteAt"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/complete":{"post":{"summary":"Mark a job done from the office or an integration","tags":["Office"],"description":"For work finished outside the field app. Moves the job to done (as of `at`, default now; up to 30 days back, or back to the job's own day when it started earlier, as history does: a job imported with source import, or a past time written with allowPast on a booking or a move), records each visit as done in plan vs actual (KPIs count it), adds `notes` to the timeline and drops queued reminder texts. By default it also texts the customer the review link, schedules CrewConnect payouts and sends job.completed with source `api` (`office` for a signed-in office user). `silent: true` sends no review text and schedules no payout (use it when the customer already heard from you); job.completed still goes out, with silent: true, to webhook endpoints with historyEvents: true (others don't get a silent completion). 409 already when it is done, not_booked when it isn't a booked job.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"at":{"type":"string","format":"date-time","description":"When the work was finished (ISO 8601); default now"},"notes":{"type":"string","minLength":1,"maxLength":4000},"silent":{"type":"boolean","default":false,"description":"true: no review text and no payout; job.completed still goes out, with silent: true, to endpoints with historyEvents: true"},"pay":{"type":"boolean","default":true,"description":"false: no technician pay line for this job (history whose pay was settled outside CrewLink, e.g. an import). Default true: each tech's pay goes to the pay period of `at`"}},"additionalProperties":false,"default":{}}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"stage":{"type":"string","enum":["done"]},"doneAt":{"type":"string"},"silent":{"type":"boolean"},"minutesOnSite":{"type":"number","nullable":true}},"required":["jobId","stage","doneAt","silent","minutesOnSite"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/notes":{"post":{"summary":"Add an office note to a job","tags":["Office"],"description":"It shows on the job's timeline (by: office) and in the field app's job history.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":4000}},"required":["text"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"at":{"type":"string"}},"required":["id","at"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/attention":{"post":{"summary":"Flag a job as needing attention","tags":["Office"],"description":"Sets needsAttention on the job (shown in the list, the detail and the intake tabs), adds it to the timeline and sends job.needs_attention. Flagging again replaces the reason. DELETE clears it. A tech asking for an office review at check-in, a customer text that fails after three tries, and a booked job not closed out (not marked done 3 hours after its scheduled end, or by 21:00 local that day; checked every 15 minutes, once per job; reason \"Not closed out: still <stage> since <time>\", source system, by closeout; quiet when the job has notify false) set the same flag.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","minLength":1,"maxLength":500}},"required":["reason"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"needsAttention":{"type":"object","properties":{"reason":{"type":"string"},"at":{"type":"string"}},"required":["reason","at"],"additionalProperties":false}},"required":["jobId","needsAttention"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"delete":{"summary":"Clear a job's attention flag","tags":["Office"],"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"needsAttention":{"enum":["null"],"nullable":true}},"required":["jobId","needsAttention"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/catalog":{"get":{"summary":"The company and its bookable services (as GET /v1/public/{slug})","tags":["Office"],"description":"The same answer as the public catalog, for a caller with a key (a platform names the company with X-CrewLink-Company).","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"company":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"timezone":{"type":"string","description":"The company's default zone; open times carry the zone of the branch serving the address"},"country":{"type":"string"},"units":{"type":"string","enum":["mi","km"]}},"required":["name","slug","timezone","country","units"],"additionalProperties":false},"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"minutes":{"type":"number"},"peopleNeeded":{"type":"number"},"days":{"type":"number"},"priceCents":{"type":"number","nullable":true},"depositCents":{"type":"number"},"currency":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true},"daylightOnly":{"type":"boolean","description":"true: only times when the whole visit is in daylight at the address are offered and booked (409 outside_daylight otherwise)"}},"required":["id","name","minutes","peopleNeeded","days","priceCents","depositCents","currency","category","daylightOnly"],"additionalProperties":false}}},"required":["company","services"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/availability":{"post":{"summary":"Open start times at an address (as the booking page, with a key)","tags":["Office"],"description":"The same answer as POST /v1/public/{slug}/availability, without the per-IP limits. excludeJobId leaves that job's own visits out, so a reschedule sees the times it could move to. leadMinutes: nothing sooner than this many minutes from now (default 60). earliestDay: next_business_day offers nothing before the next Monday-to-Friday day after today in the address's zone (Friday, Saturday and Sunday give Monday; public holidays aren't known, block them as time off); with leadMinutes too, the later of the two wins. stepMinutes (1, 5, 15, 30 or 60; default 15) is the grid of local minutes the start times lie on: 60 gives only whole hours, 1 any minute; each is checked against travel and jobs as before, and any of them holds or books as given. Send the same to POST /v1/office/holds.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"First local date, YYYY-MM-DD (default today)"},"days":{"type":"integer","minimum":1,"maximum":28,"default":14},"excludeJobId":{"type":"integer","description":"A job of this company whose visits don't count (rescheduling it)"},"leadMinutes":{"type":"integer","minimum":0,"maximum":1440,"default":60},"earliestDay":{"type":"string","enum":["next_business_day"],"description":"next_business_day: nothing before the next business day (Monday to Friday) after today"},"clientCompanyId":{"type":"string","format":"uuid","description":"The job is for this client company (one of the company's, active). A test client: only test people's times; otherwise test people's never"},"stepMinutes":{"anyOf":[{"type":"number","enum":[1]},{"type":"number","enum":[5]},{"type":"number","enum":[15]},{"type":"number","enum":[30]},{"type":"number","enum":[60]}],"default":15,"description":"Start times on this grid of local minutes: 60 = on the hour only, 15 = :00/:15/:30/:45 (the default), 1 = any minute. Every one is still travel- and job-feasible. One of 1, 5, 15, 30, 60 (422 otherwise)"}},"required":["serviceId","lat","lng"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string","description":"IANA zone of `local`: the zone of the address (a branch whose area holds it gives its own; CrewLink's own notes)"},"slots":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string","description":"The real instant (UTC)"},"local":{"type":"string","description":"Company-local wall clock, e.g. '2026-10-12T14:30'"}},"required":["start","local"],"additionalProperties":false}},"outsideArea":{"type":"boolean"},"nearestKm":{"type":"number","nullable":true,"description":"When outsideArea: straight-line km to the nearest branch"}},"required":["timezone","slots","outsideArea","nearestKm"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/holds":{"post":{"summary":"Hold a start time (as the booking page, with a key)","tags":["Office"],"description":"Rechecks the time under a lock, assigns the best tech(s) and holds it for 5 minutes; book it with POST /v1/office/jobs { holdId, holdSecret, customer, ... }. Unlike the booking page: no per-IP hold cap and no one-hour lead unless you send one (leadMinutes, default 0: the time just must not have passed), and earliestDay next_business_day refuses anything before the next business day (Monday to Friday) after today, as POST /v1/office/availability offers. notify: false books the job with no homeowner texts or emails at all (stored on the job; also the default when your platform grant has homeownerMessaging off). 409 slot_taken when it's gone, in_the_past when it has passed, too_soon when it is inside leadMinutes or before earliestDay.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"start":{"type":"string","format":"date-time"},"leadMinutes":{"type":"integer","minimum":0,"maximum":1440,"default":0,"description":"Refuse (409 too_soon) a start sooner than this many minutes from now"},"earliestDay":{"type":"string","enum":["next_business_day"],"description":"next_business_day: refuse (409 too_soon) a start before the next business day (Monday to Friday) after today"},"notify":{"type":"boolean","description":"false: no texts or emails to the homeowner for this job, ever (confirmation, reminder, on-the-way, moves, cancellation, review, invoice). Kept when the hold is booked"},"clientCompanyId":{"type":"string","format":"uuid","description":"The job is for this client company (one of the company's, active); kept on the hold. A test client's hold goes to test people only, any other never to a test person"}},"required":["serviceId","lat","lng","start"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"holdId":{"type":"number"},"holdSecret":{"type":"string"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local"},"expiresAt":{"type":"string"},"notify":{"type":"boolean","description":"false: the job will send the homeowner nothing"}},"required":["holdId","holdSecret","start","local","timezone","expiresAt","notify"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}/availability":{"put":{"summary":"Replace someone's weekly working hours","tags":["Office"],"description":"The whole week at once, Monday first: each day off (null or []), one window [start, end], or several windows [[start, end], ...] in minutes after local midnight. Idempotent: sending the same week again changes nothing and sends no event. Several windows a day are stored as sent (`availability`); `hours` is each day's envelope, first start to last end, and `merged` is always empty (kept for older integrations). The answer is always the stored week. This replaces a change the tech made in the field app that hasn't taken effect yet. `timezone`, when sent, must keep the same clock as the person's zone (`timezone` on GET /v1/office/team: their own, else their branch's, else their home's; 422 timezone_mismatch otherwise; to change it, PATCH /v1/office/team/{id} with timezone). Jobs already booked stay put; outsideHours lists the ones now outside. Sends availability.updated and person.updated when the hours change.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string","maxLength":64},"hours":{"type":"array","items":{"anyOf":[{"enum":["null"],"nullable":true},{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"integer","minimum":0,"maximum":1440},{"type":"integer","minimum":0,"maximum":1440}]},{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"integer","minimum":0,"maximum":1440},{"type":"integer","minimum":0,"maximum":1440}]},"maxItems":8}]},"minItems":7,"maxItems":7,"description":"Monday first: null, [start, end] or [[start, end], ...]"},"force":{"type":"boolean","description":"Owners only: override the integration that owns this team's availability for this one request (recorded). Ignored when nobody else owns it"}},"required":["hours"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"personId":{"type":"number"},"timezone":{"type":"string"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"description":"As stored, the same shape as the request: every window per weekday, [] off"},"hours":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}],"nullable":true},"description":"Each day's envelope (first start to last end), or null off"},"merged":{"type":"array","items":{"type":"number"},"description":"Always empty (several windows are stored now)"},"changed":{"type":"boolean"},"outsideHours":{"type":"array","items":{"type":"number"},"description":"Booked job ids now outside the hours"}},"required":["personId","timezone","availability","hours","merged","changed","outsideHours"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"managedBy":{"type":"string","description":"managed_by_integration: the slug of the platform that owns it, e.g. acme"},"managedByName":{"type":"string","description":"Its name, e.g. Your platform"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}/bases":{"post":{"summary":"Set where someone starts and ends their days for a while (lodging, a temporary address)","tags":["Office"],"description":"From `from` to `to` (local dates, inclusive; up to 366 days) the person sets off from and returns to this base instead of home: open times and routing use it. 409 trip_conflict when a booked trip covers any of those days (the trip's lodging already counts), 409 base_conflict when another base does, 409 duplicate_external_ref (with baseId) when one of the externalRefs is already on a base of this company (a resent base is recognised, never doubled). 409 managed_by_integration when another integration owns this team's availability. Sends person.base.updated.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"to":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"lat":{"type":"number","minimum":-90,"maximum":90,"description":"Required unless address has lat"},"lng":{"type":"number","minimum":-180,"maximum":180,"description":"Required unless address has lng"},"address":{"anyOf":[{"type":"string","maxLength":300},{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":200},"line2":{"type":"string","maxLength":200},"city":{"type":"string","maxLength":100},"region":{"type":"string","maxLength":100,"description":"State or province"},"postcode":{"type":"string","maxLength":20},"country":{"type":"string","pattern":"^[A-Za-z]{2}$","description":"ISO 3166-1 alpha-2, e.g. US"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180}},"required":["line1"],"additionalProperties":false}]},"reason":{"type":"string","maxLength":200},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200},"description":"Your ids for this base (kept and returned). Each (source, id) is unique among the company's bases: sending it again is 409 duplicate_external_ref with baseId"},"force":{"type":"boolean","description":"Owners only: override the integration that owns this team's availability for this one request (recorded)"}},"required":["from","to"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"personId":{"type":"number"},"from":{"type":"string","description":"Local date, first day"},"to":{"type":"string","description":"Local date, last day (inclusive)"},"lat":{"type":"number"},"lng":{"type":"number"},"address":{"type":"string","nullable":true},"addressParts":{"type":"object","additionalProperties":{"type":"string"},"nullable":true},"reason":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"}},"tripId":{"type":"string","nullable":true,"description":"Set when a booked trip made it"},"createdAt":{"type":"string"}},"required":["id","personId","from","to","lat","lng","address","addressParts","reason","externalRefs","tripId","createdAt"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"tripId":{"type":"string"},"baseId":{"type":"string"},"managedBy":{"type":"string"},"managedByName":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"Someone's bases that overlap a date range","tags":["Office"],"description":"from..to are local dates, inclusive (default today and the 90 days after). Trips write a base too (tripId set).","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false},{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"bases":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"personId":{"type":"number"},"from":{"type":"string","description":"Local date, first day"},"to":{"type":"string","description":"Local date, last day (inclusive)"},"lat":{"type":"number"},"lng":{"type":"number"},"address":{"type":"string","nullable":true},"addressParts":{"type":"object","additionalProperties":{"type":"string"},"nullable":true},"reason":{"type":"string","nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"}},"tripId":{"type":"string","nullable":true,"description":"Set when a booked trip made it"},"createdAt":{"type":"string"}},"required":["id","personId","from","to","lat","lng","address","addressParts","reason","externalRefs","tripId","createdAt"],"additionalProperties":false}}},"required":["bases"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}/bases/{baseId}":{"delete":{"summary":"Remove a base (the person goes back to setting off from home)","tags":["Office"],"description":"409 trip_base for a trip's base: it goes with the trip. 409 managed_by_integration when another integration owns this team's availability (owners: ?force=true overrides, recorded). Sends person.base.updated.","parameters":[{"schema":{"type":"string","enum":["true","false"]},"in":"query","name":"force","required":false,"description":"Owners only: true overrides the integration that owns availability (recorded)"},{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"baseId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}},"required":["id","deleted"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"managedBy":{"type":"string","description":"managed_by_integration: the slug of the platform that owns it, e.g. acme"},"managedByName":{"type":"string","description":"Its name, e.g. Your platform"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/platforms":{"get":{"summary":"Platforms this company has granted access, newest first (revoked ones included)","tags":["Platforms"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"grants":{"type":"array","items":{"type":"object","properties":{"platform":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"}},"required":["slug","name"],"additionalProperties":false},"scopes":{"type":"array","items":{"type":"string","enum":["read","write","webhooks"]}},"homeownerMessaging":{"type":"string","enum":["on","off"],"description":"off: jobs this platform books here never text or email the homeowner (no confirmation, reminder, on-the-way, move, cancellation, review or invoice); they're booked notify false and anything queued is dropped. on: the usual texts (the job's notify flag and the company's homeownerMessaging setting still apply)"},"peopleNames":{"type":"string","enum":["short","full"],"description":"How the platform sees your people: short (default) first name and last initial (\"Maria G.\"); full: their full names, for a platform that runs your company for you"},"grantedAt":{"type":"string"},"revokedAt":{"type":"string","nullable":true},"grantedBy":{"type":"string","nullable":true,"description":"The owner's user id; null when CrewLink set it up or the company API key granted it"}},"required":["platform","scopes","homeownerMessaging","peopleNames","grantedAt","revokedAt","grantedBy"],"additionalProperties":false}}},"required":["grants"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/platforms/{slug}/grant":{"post":{"summary":"Let a platform act for this company (owner only)","tags":["Platforms"],"description":"read: see jobs, the schedule and the catalog; write: book, move, cancel, complete and add files (read included); webhooks: subscribe to this company's events. Granting again replaces the scopes. homeownerMessaging: off means jobs the platform books never text or email the homeowner; left out, a grant keeps its option (a new one is on, or, on a client company, the platform's option on the provider that manages it). A new grant sends platform.grant.created to the platform.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scopes":{"type":"array","items":{"type":"string","enum":["read","write","webhooks"]},"minItems":1},"homeownerMessaging":{"type":"string","enum":["on","off"],"description":"off: jobs this platform books here never text or email the homeowner (no confirmation, reminder, on-the-way, move, cancellation, review or invoice); they're booked notify false and anything queued is dropped. on: the usual texts (the job's notify flag and the company's homeownerMessaging setting still apply)"},"peopleNames":{"type":"string","enum":["short","full"],"description":"How the platform sees your people: short (default) first name and last initial (\"Maria G.\"); full: their full names, for a platform that runs your company for you"}},"required":["scopes"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":60},"in":"path","name":"slug","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"platform":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"}},"required":["slug","name"],"additionalProperties":false},"scopes":{"type":"array","items":{"type":"string","enum":["read","write","webhooks"]}},"homeownerMessaging":{"type":"string","enum":["on","off"],"description":"off: jobs this platform books here never text or email the homeowner (no confirmation, reminder, on-the-way, move, cancellation, review or invoice); they're booked notify false and anything queued is dropped. on: the usual texts (the job's notify flag and the company's homeownerMessaging setting still apply)"},"peopleNames":{"type":"string","enum":["short","full"],"description":"How the platform sees your people: short (default) first name and last initial (\"Maria G.\"); full: their full names, for a platform that runs your company for you"},"grantedAt":{"type":"string"},"revokedAt":{"type":"string","nullable":true},"grantedBy":{"type":"string","nullable":true,"description":"The owner's user id; null when CrewLink set it up or the company API key granted it"}},"required":["platform","scopes","homeownerMessaging","peopleNames","grantedAt","revokedAt","grantedBy"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"patch":{"summary":"Change a platform grant's options (owner only)","tags":["Platforms"],"description":"homeownerMessaging: off stops every text and email to homeowners for the jobs this platform books here, including jobs already booked (anything queued for them is dropped when it comes due); on lets them through again (a job booked with notify: false stays quiet). peopleNames: full shows the platform your people's full names (for a platform that runs your company for you); short (the default) shows first name and last initial. Scopes are changed with POST .../grant. 404 when the platform has no live grant here.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"homeownerMessaging":{"type":"string","enum":["on","off"],"description":"off: jobs this platform books here never text or email the homeowner (no confirmation, reminder, on-the-way, move, cancellation, review or invoice); they're booked notify false and anything queued is dropped. on: the usual texts (the job's notify flag and the company's homeownerMessaging setting still apply)"},"peopleNames":{"type":"string","enum":["short","full"],"description":"How the platform sees your people: short (default) first name and last initial (\"Maria G.\"); full: their full names, for a platform that runs your company for you"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":60},"in":"path","name":"slug","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"platform":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"}},"required":["slug","name"],"additionalProperties":false},"scopes":{"type":"array","items":{"type":"string","enum":["read","write","webhooks"]}},"homeownerMessaging":{"type":"string","enum":["on","off"],"description":"off: jobs this platform books here never text or email the homeowner (no confirmation, reminder, on-the-way, move, cancellation, review or invoice); they're booked notify false and anything queued is dropped. on: the usual texts (the job's notify flag and the company's homeownerMessaging setting still apply)"},"peopleNames":{"type":"string","enum":["short","full"],"description":"How the platform sees your people: short (default) first name and last initial (\"Maria G.\"); full: their full names, for a platform that runs your company for you"},"grantedAt":{"type":"string"},"revokedAt":{"type":"string","nullable":true},"grantedBy":{"type":"string","nullable":true,"description":"The owner's user id; null when CrewLink set it up or the company API key granted it"}},"required":["platform","scopes","homeownerMessaging","peopleNames","grantedAt","revokedAt","grantedBy"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"delete":{"summary":"Revoke a platform's access (owner only)","tags":["Platforms"],"description":"Its key stops working for this company at once, its webhook endpoints here are turned off, and its platform-wide subscription stops getting this company's events (it gets platform.grant.revoked). Jobs it booked stay.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":60},"in":"path","name":"slug","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"platform":{"type":"string"},"revoked":{"type":"boolean","enum":[true]}},"required":["platform","revoked"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/platform/grants":{"get":{"summary":"The companies that granted this platform access, and the scopes","tags":["Platforms"],"description":"Live grants only. Use a company's slug or id as X-CrewLink-Company.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"platform":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"}},"required":["slug","name"],"additionalProperties":false},"grants":{"type":"array","items":{"type":"object","properties":{"company":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"timezone":{"type":"string"}},"required":["id","slug","name","timezone"],"additionalProperties":false},"scopes":{"type":"array","items":{"type":"string"}},"homeownerMessaging":{"type":"string","enum":["on","off"],"description":"off: jobs you book for this company never text or email its homeowners (set by the company's owner or CrewLink)"},"peopleNames":{"type":"string","enum":["short","full"],"description":"full: you see this company's people by their full names; short: \"First L.\" (set by the company's owner or CrewLink)"},"grantedAt":{"type":"string"},"revokedAt":{"type":"string","nullable":true}},"required":["company","scopes","homeownerMessaging","peopleNames","grantedAt","revokedAt"],"additionalProperties":false}}},"required":["platform","grants"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/platform/webhooks":{"post":{"summary":"Subscribe one URL to every granted company's events","tags":["Platforms"],"description":"One subscription for the whole platform: it gets the events of every company that granted this platform the webhooks scope (each body says which, in companyId), plus platform.grant.created and platform.grant.revoked when a company grants or revokes access. Events: booking.created, booking.rescheduled, booking.cancelled, block.created, block.deleted, connection.requested, connection.accepted, connection.ended, job.en_route, job.arrived, job.completed, review.created, invoice.paid, deposit.paid, trip.booked, request.created, job.running_late, day.reviewed, message.posted, badge.earned, job.needs_attention, attachment.uploaded, person.updated, availability.updated, person.base.updated, dispatch.position, job.risk_changed, site.created, site.updated, site.merged, platform.grant.created, platform.grant.revoked, external_message.created, external_message.updated, external_message.deleted, * (* is every event except dispatch.position, which must be named). The signing secret is returned once, here; it belongs to this subscription only. Same URL rules, signature and retries as company webhooks.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","maxLength":2000,"pattern":"^https\\:\\/\\/"},"events":{"type":"array","items":{"type":"string","enum":["booking.created","booking.rescheduled","booking.cancelled","block.created","block.deleted","connection.requested","connection.accepted","connection.ended","job.en_route","job.arrived","job.completed","review.created","invoice.paid","deposit.paid","trip.booked","request.created","job.running_late","day.reviewed","message.posted","badge.earned","job.needs_attention","attachment.uploaded","person.updated","availability.updated","person.base.updated","dispatch.position","job.risk_changed","site.created","site.updated","site.merged","platform.grant.created","platform.grant.revoked","external_message.created","external_message.updated","external_message.deleted","*"]},"minItems":1},"historyEvents":{"type":"boolean","default":false,"description":"true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Change it with PATCH /v1/platform/webhooks/{id}."}},"required":["url","events"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"historyEvents":{"type":"boolean","description":"true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Change it with PATCH /v1/platform/webhooks/{id}."},"secret":{"type":"string","description":"Shown once"}},"required":["id","url","events","active","historyEvents","secret"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"This platform's platform-wide subscriptions","tags":["Platforms"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"endpoints":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"historyEvents":{"type":"boolean","description":"true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Change it with PATCH /v1/platform/webhooks/{id}."}},"required":["id","url","events","active","historyEvents"],"additionalProperties":false}}},"required":["endpoints"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/platform/webhooks/{id}":{"patch":{"summary":"Change a subscription: events and historyEvents","tags":["Platforms"],"description":"events: replaces the subscription's event list (the URL and signing secret stay the same; include \"*\" to keep everything, and name dispatch.position explicitly since \"*\" excludes it). historyEvents: true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Send either or both. Any endpoint this platform made (its platform-wide subscription, or one it subscribed for a company with POST /v1/office/webhooks); 404 otherwise. Deliveries already queued are not changed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"historyEvents":{"type":"boolean","description":"true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Change it with PATCH /v1/platform/webhooks/{id}."},"events":{"type":"array","items":{"type":"string","enum":["booking.created","booking.rescheduled","booking.cancelled","block.created","block.deleted","connection.requested","connection.accepted","connection.ended","job.en_route","job.arrived","job.completed","review.created","invoice.paid","deposit.paid","trip.booked","request.created","job.running_late","day.reviewed","message.posted","badge.earned","job.needs_attention","attachment.uploaded","person.updated","availability.updated","person.base.updated","dispatch.position","job.risk_changed","site.created","site.updated","site.merged","platform.grant.created","platform.grant.revoked","external_message.created","external_message.updated","external_message.deleted","*"]},"minItems":1,"description":"The new event list (replaces the old one)"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"historyEvents":{"type":"boolean","description":"true: also get history emissions: booking.created / booking.rescheduled with history: true (a past time written as history by an owner or an import), booking.rescheduled with unassigned: true (techIds [] took everyone off), and job.completed with silent: true. Default false: those are not sent to this endpoint (every other event is). Change it with PATCH /v1/platform/webhooks/{id}."}},"required":["id","url","events","active","historyEvents"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"delete":{"summary":"Remove a platform-wide subscription (pending deliveries are dropped)","tags":["Platforms"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}},"required":["id","deleted"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/platform/webhooks/{id}/deliveries":{"get":{"summary":"A subscription's delivery log","tags":["Platforms"],"description":"Newest first, for debugging a receiver: what was queued for this endpoint, how many attempts, the last HTTP status and error, when it goes next, and the body exactly as sent. Filters: event, jobId (data.jobId, data.bookingId or data.requestId), status (pending, delivered, failed); limit 1-200 (default 50); before: the `next` of the previous page. Any endpoint this platform made (its platform-wide subscription, or one it subscribed for a company with POST /v1/office/webhooks); 404 otherwise. Only deliveries about companies that grant this platform access now are shown.","parameters":[{"schema":{"type":"string","maxLength":100},"in":"query","name":"event","required":false,"description":"Only this event, e.g. booking.created"},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"jobId","required":false,"description":"Only deliveries about this job: data.jobId, data.bookingId or data.requestId (request.created)"},{"schema":{"type":"string","enum":["pending","delivered","failed"]},"in":"query","name":"status","required":false},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"query","name":"before","required":false,"description":"Cursor: only deliveries with a smaller id (pass `next` from the previous page)"},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"deliveries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number","description":"The delivery id (CrewLink-Delivery header, and `id` in the body)"},"endpointId":{"type":"string"},"event":{"type":"string"},"companyId":{"type":"string","description":"The company the event is about"},"status":{"type":"string","enum":["pending","delivered","failed"],"description":"pending (queued or waiting to retry), delivered (a 2xx came back) or failed (gave up after the last retry)"},"createdAt":{"type":"string","description":"When it was queued"},"attempts":{"type":"number"},"lastAttemptAt":{"type":"string","nullable":true},"nextAttemptAt":{"type":"string","nullable":true,"description":"When it goes (again); null unless pending"},"lastStatusCode":{"type":"number","nullable":true,"description":"HTTP status of the last attempt; null when no answer came back (refused host, timeout, connection error) or not tried yet"},"lastError":{"type":"string","nullable":true,"description":"Why the last attempt failed (up to 500 characters); null after a success"},"deliveredAt":{"type":"string","nullable":true},"payload":{"type":"object","additionalProperties":{},"description":"The body as sent: { id, event, createdAt, companyId, data }"}},"required":["id","endpointId","event","companyId","status","createdAt","attempts","lastAttemptAt","nextAttemptAt","lastStatusCode","lastError","deliveredAt","payload"],"additionalProperties":false},"description":"Newest first"},"next":{"type":"number","nullable":true,"description":"Pass as `before` for the next page; null on the last"}},"required":["deliveries","next"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/platform/webhooks/{id}/deliveries/{deliveryId}/retry":{"post":{"summary":"Send one delivery again now","tags":["Platforms"],"description":"Requeues the delivery (pending, due now) whatever its status, with the same id and body; the next send loop (every 10 seconds) sends it. Attempts are kept, so one that had given up and fails again gives up again. 404 when the subscription or delivery is not one this platform can see.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true},{"schema":{"type":"integer","exclusiveMinimum":true,"minimum":0},"in":"path","name":"deliveryId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number","description":"The delivery id (CrewLink-Delivery header, and `id` in the body)"},"endpointId":{"type":"string"},"event":{"type":"string"},"companyId":{"type":"string","description":"The company the event is about"},"status":{"type":"string","enum":["pending","delivered","failed"],"description":"pending (queued or waiting to retry), delivered (a 2xx came back) or failed (gave up after the last retry)"},"createdAt":{"type":"string","description":"When it was queued"},"attempts":{"type":"number"},"lastAttemptAt":{"type":"string","nullable":true},"nextAttemptAt":{"type":"string","nullable":true,"description":"When it goes (again); null unless pending"},"lastStatusCode":{"type":"number","nullable":true,"description":"HTTP status of the last attempt; null when no answer came back (refused host, timeout, connection error) or not tried yet"},"lastError":{"type":"string","nullable":true,"description":"Why the last attempt failed (up to 500 characters); null after a success"},"deliveredAt":{"type":"string","nullable":true},"payload":{"type":"object","additionalProperties":{},"description":"The body as sent: { id, event, createdAt, companyId, data }"}},"required":["id","endpointId","event","companyId","status","createdAt","attempts","lastAttemptAt","nextAttemptAt","lastStatusCode","lastError","deliveredAt","payload"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/attachments":{"post":{"summary":"Add a file to a job: get an upload URL","tags":["Office"],"description":"For proposals, photos and documents up to 20 MB. PUT the bytes to `uploadUrl` within 15 minutes with the same Content-Type, then confirm with POST .../attachments/{attachmentId}/uploaded (that sends attachment.uploaded). The bytes go straight to storage. To send the file through the API in one call instead, use POST .../attachments/upload.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["proposal","photo","document"]},"name":{"type":"string","minLength":1,"maxLength":200,"description":"File name to show and download as"},"contentType":{"type":"string","enum":["application/pdf","image/jpeg","image/png","image/heic","image/webp","text/plain","text/csv","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]},"bytes":{"type":"integer","minimum":1,"maximum":20971520,"description":"Size of the file, up to 20 MB"}},"required":["kind","name","contentType","bytes"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"attachment":{"type":"object","properties":{"id":{"type":"string"},"jobId":{"type":"number"},"kind":{"type":"string","enum":["proposal","photo","document"]},"name":{"type":"string"},"contentType":{"type":"string"},"bytes":{"type":"number"},"status":{"type":"string","enum":["pending","uploaded"],"description":"pending: an upload URL was issued and not confirmed yet"},"uploadedBy":{"type":"object","properties":{"kind":{"type":"string","enum":["platform","user","company"]},"id":{"type":"string","nullable":true}},"required":["kind","id"],"additionalProperties":false,"description":"Who added it: a platform (its id), a user (their id) or the company API key (the company id)"},"createdAt":{"type":"string"},"uploadedAt":{"type":"string","nullable":true},"url":{"type":"string","nullable":true,"description":"Signed link to download it, good for 15 minutes; null until uploaded. Ask again for a fresh one"}},"required":["id","jobId","kind","name","contentType","bytes","status","uploadedBy","createdAt","uploadedAt","url"],"additionalProperties":false},"uploadUrl":{"type":"string"},"expiresAt":{"type":"string"}},"required":["attachment","uploadUrl","expiresAt"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"A job's files, with download links","tags":["Office"],"description":"Uploaded files, oldest first. Each `url` is a signed download link good for 15 minutes.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"attachments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"jobId":{"type":"number"},"kind":{"type":"string","enum":["proposal","photo","document"]},"name":{"type":"string"},"contentType":{"type":"string"},"bytes":{"type":"number"},"status":{"type":"string","enum":["pending","uploaded"],"description":"pending: an upload URL was issued and not confirmed yet"},"uploadedBy":{"type":"object","properties":{"kind":{"type":"string","enum":["platform","user","company"]},"id":{"type":"string","nullable":true}},"required":["kind","id"],"additionalProperties":false,"description":"Who added it: a platform (its id), a user (their id) or the company API key (the company id)"},"createdAt":{"type":"string"},"uploadedAt":{"type":"string","nullable":true},"url":{"type":"string","nullable":true,"description":"Signed link to download it, good for 15 minutes; null until uploaded. Ask again for a fresh one"}},"required":["id","jobId","kind","name","contentType","bytes","status","uploadedBy","createdAt","uploadedAt","url"],"additionalProperties":false}}},"required":["attachments"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/attachments/{attachmentId}/uploaded":{"post":{"summary":"Confirm a file was uploaded to its URL","tags":["Office"],"description":"Checks the file in storage first: `409 not_uploaded` when nothing is at the upload URL yet (PUT it, then confirm again); `422 upload_mismatch` when it is bigger than the `bytes` declared or than 20 MB, or isn't the declared Content-Type (the file and the pending attachment are then removed: start again). Then marks it uploaded, records the real size and sends attachment.uploaded. Repeating it is harmless (no second event).","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"attachmentId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"jobId":{"type":"number"},"kind":{"type":"string","enum":["proposal","photo","document"]},"name":{"type":"string"},"contentType":{"type":"string"},"bytes":{"type":"number"},"status":{"type":"string","enum":["pending","uploaded"],"description":"pending: an upload URL was issued and not confirmed yet"},"uploadedBy":{"type":"object","properties":{"kind":{"type":"string","enum":["platform","user","company"]},"id":{"type":"string","nullable":true}},"required":["kind","id"],"additionalProperties":false,"description":"Who added it: a platform (its id), a user (their id) or the company API key (the company id)"},"createdAt":{"type":"string"},"uploadedAt":{"type":"string","nullable":true},"url":{"type":"string","nullable":true,"description":"Signed link to download it, good for 15 minutes; null until uploaded. Ask again for a fresh one"}},"required":["id","jobId","kind","name","contentType","bytes","status","uploadedBy","createdAt","uploadedAt","url"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/jobs/{id}/attachments/{attachmentId}":{"delete":{"summary":"Remove a file from a job","tags":["Office"],"description":"It disappears from the job (office, platforms and the field app) at once, and the file is deleted from storage. A platform key removes only the files that platform added (403 otherwise).","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"attachmentId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"deleted":{"type":"boolean","enum":[true]}},"required":["id","deleted"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/attachments/upload":{"post":{"summary":"Add a file to a job in one call (direct upload)","tags":["Office"],"description":"Send the file through the API, up to 20 MB, either as multipart/form-data (a `file` part, and `kind`, optionally `name`, as fields or query) or as the raw bytes with the file's own Content-Type (`kind` and `name` in the query). Sends attachment.uploaded. 413 when it is too big, 415 for a type that isn't accepted (application/pdf, image/jpeg, image/png, image/heic, image/webp, text/plain, text/csv, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet).","parameters":[{"schema":{"type":"string","enum":["proposal","photo","document"]},"in":"query","name":"kind","required":false},{"schema":{"type":"string","minLength":1,"maxLength":200},"in":"query","name":"name","required":false},{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"jobId":{"type":"number"},"kind":{"type":"string","enum":["proposal","photo","document"]},"name":{"type":"string"},"contentType":{"type":"string"},"bytes":{"type":"number"},"status":{"type":"string","enum":["pending","uploaded"],"description":"pending: an upload URL was issued and not confirmed yet"},"uploadedBy":{"type":"object","properties":{"kind":{"type":"string","enum":["platform","user","company"]},"id":{"type":"string","nullable":true}},"required":["kind","id"],"additionalProperties":false,"description":"Who added it: a platform (its id), a user (their id) or the company API key (the company id)"},"createdAt":{"type":"string"},"uploadedAt":{"type":"string","nullable":true},"url":{"type":"string","nullable":true,"description":"Signed link to download it, good for 15 minutes; null until uploaded. Ask again for a fresh one"}},"required":["id","jobId","kind","name","contentType","bytes","status","uploadedBy","createdAt","uploadedAt","url"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"413":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"415":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/tech/jobs/{id}/attachments":{"get":{"summary":"A job's files (proposal, photos, documents)","tags":["Field app"],"description":"For a job on my schedule. Each `url` is a signed download link good for 15 minutes.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"attachments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"jobId":{"type":"number"},"kind":{"type":"string","enum":["proposal","photo","document"]},"name":{"type":"string"},"contentType":{"type":"string"},"bytes":{"type":"number"},"status":{"type":"string","enum":["pending","uploaded"],"description":"pending: an upload URL was issued and not confirmed yet"},"uploadedBy":{"type":"object","properties":{"kind":{"type":"string","enum":["platform","user","company"]},"id":{"type":"string","nullable":true}},"required":["kind","id"],"additionalProperties":false,"description":"Who added it: a platform (its id), a user (their id) or the company API key (the company id)"},"createdAt":{"type":"string"},"uploadedAt":{"type":"string","nullable":true},"url":{"type":"string","nullable":true,"description":"Signed link to download it, good for 15 minutes; null until uploaded. Ask again for a fresh one"}},"required":["id","jobId","kind","name","contentType","bytes","status","uploadedBy","createdAt","uploadedAt","url"],"additionalProperties":false}}},"required":["attachments"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/external-messages":{"post":{"summary":"Mirror a message into a job's conversations (platform), or answer in one (office)","tags":["Mirrored conversations"],"description":"From a platform key (write): stores one of your messages on this job. Idempotent by your message id (externalRefs.<your platform slug>, required): the same id again updates that message in place (200, status updated or unchanged); a deleted one stays deleted. Mirrored messages notify nobody: no text, email or push to techs, clients or homeowners, nothing in CrewLink messaging and no webhook, whatever the job or grant says. Send crewlinkMessageId with your id to link an office reply you got as external_message.created instead of storing a copy (status linked). From the office (owner, dispatcher, field manager in scope, or the company API key): channel client, tech or internal and a body; it is stored with source crewlink_office and sent as external_message.created to the platform that owns the job's conversation (the one that booked it, else the one that mirrored messages into it) and to no one else. 409 no_platform when no platform owns it. Office replies take no attachments.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"channel":{"type":"string","enum":["client","tech","internal","dm","system"],"description":"client | tech | internal | dm | system. The office writes client, tech or internal"},"body":{"type":"string","maxLength":20000,"default":"","description":"Markdown, up to 20000 characters (may be empty with attachments)"},"author":{"type":"object","properties":{"externalId":{"type":"string","minLength":1,"maxLength":200,"description":"Your id for the person (with personId absent, a CrewLink person whose externalRefs.<platform> is this id is linked)"},"displayName":{"type":"string","minLength":1,"maxLength":200},"role":{"type":"string","minLength":1,"maxLength":40,"description":"Free text: installer, technician, homeowner, admin, ..."},"personId":{"type":"integer","description":"The CrewLink person (one of this company), when you know it"}},"required":["displayName"],"additionalProperties":false,"description":"Required from a platform; ignored for an office reply (the signed-in user)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200},"description":"Your ids: { \"<platform>\": message id (required from a platform, unique per company), \"<platform>Job\": the job or case it belongs to, \"<platform>Visit\": the visit }. For a platform: { \"acme\": \"msg_1\", \"acmeJob\": \"job_9\", \"acmeVisit\": \"visit_3\" }"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"type":{"type":"string","minLength":1,"maxLength":100,"description":"MIME type, e.g. image/jpeg"},"size":{"type":"integer","minimum":0,"maximum":2147483647,"description":"Bytes"},"url":{"type":"string","format":"uri","maxLength":2000,"pattern":"^https\\:\\/\\/","description":"Where the file lives (yours, https): CrewLink copies it into its own storage on ingest (backfill: within minutes) and keeps this link as sourceUrl"},"key":{"type":"string","maxLength":300,"description":"Or: a file uploaded to CrewLink first (POST /v1/office/external-messages/attachments)"}},"required":["name","type","size"],"additionalProperties":false,"description":"Exactly one of url or key"},"maxItems":20,"default":[],"description":"Up to 20; platforms only"},"createdAt":{"type":"string","format":"date-time","description":"When it was written (default now; may be any time in the past, not more than 5 minutes ahead). Platforms only"},"editedAt":{"type":"string","format":"date-time","description":"When it was last edited. Platforms only"},"deletedAt":{"type":"string","format":"date-time","description":"Mirror a message that is already deleted (stored as a tombstone). Platforms only"},"threadKey":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$","description":"Your thread id, when the job's message is in a group or DM thread"},"crewlinkMessageId":{"type":"integer","description":"Link your id (externalRefs) to an office reply you received as external_message.created instead of storing a copy"}},"required":["channel"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","maxLength":100},"in":"query","name":"externalSource","required":false},{"schema":{"type":"string","minLength":1,"maxLength":220},"in":"path","name":"id","required":true,"description":"The CrewLink job id, or `ref:<your id>` for the job you booked under your own id (externalRefs.<your platform slug>; another source with ?externalSource=)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"object","properties":{"id":{"type":"number"},"jobId":{"type":"number","nullable":true},"threadKey":{"type":"string","nullable":true},"channel":{"type":"string","enum":["client","tech","internal","dm","system"]},"source":{"type":"string","enum":["platform","crewlink_office"],"description":"platform: mirrored in; crewlink_office: written in CrewLink (sent to the platform as external_message.created)"},"platform":{"type":"string","nullable":true,"description":"Slug of the platform it came from or goes to"},"author":{"type":"object","properties":{"externalId":{"type":"string","nullable":true},"displayName":{"type":"string"},"role":{"type":"string","nullable":true},"personId":{"type":"number","nullable":true},"userId":{"type":"string","nullable":true}},"required":["externalId","displayName","role","personId","userId"],"additionalProperties":false},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"A signed download link from CrewLink's storage, good for 15 minutes (ask again for a fresh one); while a linked file isn't copied yet, its sourceUrl"},"sourceUrl":{"type":"string","nullable":true,"description":"The https link the platform sent, kept as given (null for a file uploaded to CrewLink)"},"copied":{"type":"boolean","description":"The file is in CrewLink's storage. false: the copy failed or is waiting; it is retried in the background"}},"required":["name","type","size","url","sourceUrl","copied"],"additionalProperties":false}},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"{ \"<platform>\": message id, \"<platform>Job\": case, \"<platform>Visit\": visit }, as known"},"caseRef":{"type":"string","nullable":true,"description":"The platform's job or case id it belongs to (kept for your platform; group by siteId in CrewLink)"},"siteId":{"type":"number","nullable":true,"description":"The CrewLink site (one property for one client company) of the message's job, as it is now (a merge moves it); null outside a job"},"createdAt":{"type":"string"},"receivedAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"deletedAt":{"type":"string","nullable":true}},"required":["id","jobId","threadKey","channel","source","platform","author","body","attachments","externalRefs","caseRef","siteId","createdAt","receivedAt","editedAt","deletedAt"],"additionalProperties":false},"status":{"type":"string","enum":["created","updated","unchanged","linked"],"description":"created: new; updated: a message with that id was there and now matches what you sent; unchanged: it already did (or it is a tombstone, which stays deleted); linked: your id now points at an office reply"}},"required":["message","status"],"additionalProperties":false}}}},"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"object","properties":{"id":{"type":"number"},"jobId":{"type":"number","nullable":true},"threadKey":{"type":"string","nullable":true},"channel":{"type":"string","enum":["client","tech","internal","dm","system"]},"source":{"type":"string","enum":["platform","crewlink_office"],"description":"platform: mirrored in; crewlink_office: written in CrewLink (sent to the platform as external_message.created)"},"platform":{"type":"string","nullable":true,"description":"Slug of the platform it came from or goes to"},"author":{"type":"object","properties":{"externalId":{"type":"string","nullable":true},"displayName":{"type":"string"},"role":{"type":"string","nullable":true},"personId":{"type":"number","nullable":true},"userId":{"type":"string","nullable":true}},"required":["externalId","displayName","role","personId","userId"],"additionalProperties":false},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"A signed download link from CrewLink's storage, good for 15 minutes (ask again for a fresh one); while a linked file isn't copied yet, its sourceUrl"},"sourceUrl":{"type":"string","nullable":true,"description":"The https link the platform sent, kept as given (null for a file uploaded to CrewLink)"},"copied":{"type":"boolean","description":"The file is in CrewLink's storage. false: the copy failed or is waiting; it is retried in the background"}},"required":["name","type","size","url","sourceUrl","copied"],"additionalProperties":false}},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"{ \"<platform>\": message id, \"<platform>Job\": case, \"<platform>Visit\": visit }, as known"},"caseRef":{"type":"string","nullable":true,"description":"The platform's job or case id it belongs to (kept for your platform; group by siteId in CrewLink)"},"siteId":{"type":"number","nullable":true,"description":"The CrewLink site (one property for one client company) of the message's job, as it is now (a merge moves it); null outside a job"},"createdAt":{"type":"string"},"receivedAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"deletedAt":{"type":"string","nullable":true}},"required":["id","jobId","threadKey","channel","source","platform","author","body","attachments","externalRefs","caseRef","siteId","createdAt","receivedAt","editedAt","deletedAt"],"additionalProperties":false},"status":{"type":"string","enum":["created","updated","unchanged","linked"],"description":"created: new; updated: a message with that id was there and now matches what you sent; unchanged: it already did (or it is a tombstone, which stays deleted); linked: your id now points at an office reply"},"platform":{"type":"string","nullable":true,"description":"Office replies: the platform it is sent to"},"webhookQueued":{"type":"boolean","description":"Office replies: an external_message.created delivery was queued (false when the platform has no endpoint for it, or its grant lacks webhooks)"}},"required":["message","status"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}},"get":{"summary":"A job's mirrored conversations","tags":["Mirrored conversations"],"description":"Office users (in their scope) and platform keys (read; a platform sees only its own platform's messages). Every channel, oldest first by when each was written, paged with nextCursor. Never for techs or client companies.","parameters":[{"schema":{"type":"string","maxLength":100},"in":"query","name":"channel","required":false,"description":"Comma list of client, tech, internal, dm, system"},{"schema":{"type":"string","maxLength":200},"in":"query","name":"cursor","required":false,"description":"nextCursor from the previous page"},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"in":"query","name":"limit","required":false},{"schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"in":"query","name":"order","required":false,"description":"By when each message was written (createdAt, then id): asc (oldest first, the default) or desc"},{"schema":{"type":"string","enum":["true","false"],"default":"true"},"in":"query","name":"includeDeleted","required":false,"description":"Tombstones (deleted messages: empty body, deletedAt set) are listed unless false"},{"schema":{"type":"string","maxLength":100},"in":"query","name":"externalSource","required":false,"description":"With a `ref:` job id: whose id it is (default your platform slug)"},{"schema":{"type":"string","minLength":1,"maxLength":220},"in":"path","name":"id","required":true,"description":"The CrewLink job id, or `ref:<your id>` for the job you booked under your own id (externalRefs.<your platform slug>; another source with ?externalSource=)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"jobId":{"type":"number","nullable":true},"threadKey":{"type":"string","nullable":true},"channel":{"type":"string","enum":["client","tech","internal","dm","system"]},"source":{"type":"string","enum":["platform","crewlink_office"],"description":"platform: mirrored in; crewlink_office: written in CrewLink (sent to the platform as external_message.created)"},"platform":{"type":"string","nullable":true,"description":"Slug of the platform it came from or goes to"},"author":{"type":"object","properties":{"externalId":{"type":"string","nullable":true},"displayName":{"type":"string"},"role":{"type":"string","nullable":true},"personId":{"type":"number","nullable":true},"userId":{"type":"string","nullable":true}},"required":["externalId","displayName","role","personId","userId"],"additionalProperties":false},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"A signed download link from CrewLink's storage, good for 15 minutes (ask again for a fresh one); while a linked file isn't copied yet, its sourceUrl"},"sourceUrl":{"type":"string","nullable":true,"description":"The https link the platform sent, kept as given (null for a file uploaded to CrewLink)"},"copied":{"type":"boolean","description":"The file is in CrewLink's storage. false: the copy failed or is waiting; it is retried in the background"}},"required":["name","type","size","url","sourceUrl","copied"],"additionalProperties":false}},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"{ \"<platform>\": message id, \"<platform>Job\": case, \"<platform>Visit\": visit }, as known"},"caseRef":{"type":"string","nullable":true,"description":"The platform's job or case id it belongs to (kept for your platform; group by siteId in CrewLink)"},"siteId":{"type":"number","nullable":true,"description":"The CrewLink site (one property for one client company) of the message's job, as it is now (a merge moves it); null outside a job"},"createdAt":{"type":"string"},"receivedAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"deletedAt":{"type":"string","nullable":true}},"required":["id","jobId","threadKey","channel","source","platform","author","body","attachments","externalRefs","caseRef","siteId","createdAt","receivedAt","editedAt","deletedAt"],"additionalProperties":false}},"nextCursor":{"type":"string","nullable":true}},"required":["messages","nextCursor"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/sites/{id}/external-messages":{"get":{"summary":"A site's mirrored conversations: every visit's, in one list","tags":["Mirrored conversations"],"description":"The mirrored messages of every job at this site (one property for one client company), oldest first, each with its jobId and siteId. Office users see the visits in their scope; platform keys (read) their own platform's messages. Never for techs or client companies. A merged site has no visits of its own (they moved to the site it was merged into).","parameters":[{"schema":{"type":"string","maxLength":100},"in":"query","name":"channel","required":false,"description":"Comma list of client, tech, internal, dm, system"},{"schema":{"type":"string","maxLength":200},"in":"query","name":"cursor","required":false,"description":"nextCursor from the previous page"},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"in":"query","name":"limit","required":false},{"schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"in":"query","name":"order","required":false,"description":"By when each message was written (createdAt, then id): asc (oldest first, the default) or desc"},{"schema":{"type":"string","enum":["true","false"],"default":"true"},"in":"query","name":"includeDeleted","required":false,"description":"Tombstones (deleted messages: empty body, deletedAt set) are listed unless false"},{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"jobId":{"type":"number","nullable":true},"threadKey":{"type":"string","nullable":true},"channel":{"type":"string","enum":["client","tech","internal","dm","system"]},"source":{"type":"string","enum":["platform","crewlink_office"],"description":"platform: mirrored in; crewlink_office: written in CrewLink (sent to the platform as external_message.created)"},"platform":{"type":"string","nullable":true,"description":"Slug of the platform it came from or goes to"},"author":{"type":"object","properties":{"externalId":{"type":"string","nullable":true},"displayName":{"type":"string"},"role":{"type":"string","nullable":true},"personId":{"type":"number","nullable":true},"userId":{"type":"string","nullable":true}},"required":["externalId","displayName","role","personId","userId"],"additionalProperties":false},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"A signed download link from CrewLink's storage, good for 15 minutes (ask again for a fresh one); while a linked file isn't copied yet, its sourceUrl"},"sourceUrl":{"type":"string","nullable":true,"description":"The https link the platform sent, kept as given (null for a file uploaded to CrewLink)"},"copied":{"type":"boolean","description":"The file is in CrewLink's storage. false: the copy failed or is waiting; it is retried in the background"}},"required":["name","type","size","url","sourceUrl","copied"],"additionalProperties":false}},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"{ \"<platform>\": message id, \"<platform>Job\": case, \"<platform>Visit\": visit }, as known"},"caseRef":{"type":"string","nullable":true,"description":"The platform's job or case id it belongs to (kept for your platform; group by siteId in CrewLink)"},"siteId":{"type":"number","nullable":true,"description":"The CrewLink site (one property for one client company) of the message's job, as it is now (a merge moves it); null outside a job"},"createdAt":{"type":"string"},"receivedAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"deletedAt":{"type":"string","nullable":true}},"required":["id","jobId","threadKey","channel","source","platform","author","body","attachments","externalRefs","caseRef","siteId","createdAt","receivedAt","editedAt","deletedAt"],"additionalProperties":false}},"nextCursor":{"type":"string","nullable":true}},"required":["messages","nextCursor"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/external-threads/{threadKey}/messages":{"post":{"summary":"Mirror a message in a DM or group that is not about one job (platform)","tags":["Mirrored conversations"],"description":"Platform keys (write) only. Idempotent by your message id (externalRefs.<your platform slug>, required): the same id again updates that message in place (200, status updated or unchanged); a deleted one stays deleted. Mirrored messages notify nobody: no text, email or push to techs, clients or homeowners, nothing in CrewLink messaging and no webhook, whatever the job or grant says. Company-scope office users read the thread; nobody else does.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"channel":{"type":"string","enum":["client","tech","internal","dm","system"],"description":"client | tech | internal | dm | system. The office writes client, tech or internal"},"body":{"type":"string","maxLength":20000,"default":"","description":"Markdown, up to 20000 characters (may be empty with attachments)"},"author":{"type":"object","properties":{"externalId":{"type":"string","minLength":1,"maxLength":200,"description":"Your id for the person (with personId absent, a CrewLink person whose externalRefs.<platform> is this id is linked)"},"displayName":{"type":"string","minLength":1,"maxLength":200},"role":{"type":"string","minLength":1,"maxLength":40,"description":"Free text: installer, technician, homeowner, admin, ..."},"personId":{"type":"integer","description":"The CrewLink person (one of this company), when you know it"}},"required":["displayName"],"additionalProperties":false,"description":"Required from a platform; ignored for an office reply (the signed-in user)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200},"description":"Your ids: { \"<platform>\": message id (required from a platform, unique per company), \"<platform>Job\": the job or case it belongs to, \"<platform>Visit\": the visit }. For a platform: { \"acme\": \"msg_1\", \"acmeJob\": \"job_9\", \"acmeVisit\": \"visit_3\" }"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"type":{"type":"string","minLength":1,"maxLength":100,"description":"MIME type, e.g. image/jpeg"},"size":{"type":"integer","minimum":0,"maximum":2147483647,"description":"Bytes"},"url":{"type":"string","format":"uri","maxLength":2000,"pattern":"^https\\:\\/\\/","description":"Where the file lives (yours, https): CrewLink copies it into its own storage on ingest (backfill: within minutes) and keeps this link as sourceUrl"},"key":{"type":"string","maxLength":300,"description":"Or: a file uploaded to CrewLink first (POST /v1/office/external-messages/attachments)"}},"required":["name","type","size"],"additionalProperties":false,"description":"Exactly one of url or key"},"maxItems":20,"default":[],"description":"Up to 20; platforms only"},"createdAt":{"type":"string","format":"date-time","description":"When it was written (default now; may be any time in the past, not more than 5 minutes ahead). Platforms only"},"editedAt":{"type":"string","format":"date-time","description":"When it was last edited. Platforms only"},"deletedAt":{"type":"string","format":"date-time","description":"Mirror a message that is already deleted (stored as a tombstone). Platforms only"},"crewlinkMessageId":{"type":"integer","description":"Link your id (externalRefs) to an office reply you received as external_message.created instead of storing a copy"}},"required":["channel"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$"},"in":"path","name":"threadKey","required":true,"description":"Your thread id (a DM or group outside a job)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"object","properties":{"id":{"type":"number"},"jobId":{"type":"number","nullable":true},"threadKey":{"type":"string","nullable":true},"channel":{"type":"string","enum":["client","tech","internal","dm","system"]},"source":{"type":"string","enum":["platform","crewlink_office"],"description":"platform: mirrored in; crewlink_office: written in CrewLink (sent to the platform as external_message.created)"},"platform":{"type":"string","nullable":true,"description":"Slug of the platform it came from or goes to"},"author":{"type":"object","properties":{"externalId":{"type":"string","nullable":true},"displayName":{"type":"string"},"role":{"type":"string","nullable":true},"personId":{"type":"number","nullable":true},"userId":{"type":"string","nullable":true}},"required":["externalId","displayName","role","personId","userId"],"additionalProperties":false},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"A signed download link from CrewLink's storage, good for 15 minutes (ask again for a fresh one); while a linked file isn't copied yet, its sourceUrl"},"sourceUrl":{"type":"string","nullable":true,"description":"The https link the platform sent, kept as given (null for a file uploaded to CrewLink)"},"copied":{"type":"boolean","description":"The file is in CrewLink's storage. false: the copy failed or is waiting; it is retried in the background"}},"required":["name","type","size","url","sourceUrl","copied"],"additionalProperties":false}},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"{ \"<platform>\": message id, \"<platform>Job\": case, \"<platform>Visit\": visit }, as known"},"caseRef":{"type":"string","nullable":true,"description":"The platform's job or case id it belongs to (kept for your platform; group by siteId in CrewLink)"},"siteId":{"type":"number","nullable":true,"description":"The CrewLink site (one property for one client company) of the message's job, as it is now (a merge moves it); null outside a job"},"createdAt":{"type":"string"},"receivedAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"deletedAt":{"type":"string","nullable":true}},"required":["id","jobId","threadKey","channel","source","platform","author","body","attachments","externalRefs","caseRef","siteId","createdAt","receivedAt","editedAt","deletedAt"],"additionalProperties":false},"status":{"type":"string","enum":["created","updated","unchanged","linked"],"description":"created: new; updated: a message with that id was there and now matches what you sent; unchanged: it already did (or it is a tombstone, which stays deleted); linked: your id now points at an office reply"}},"required":["message","status"],"additionalProperties":false}}}},"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"object","properties":{"id":{"type":"number"},"jobId":{"type":"number","nullable":true},"threadKey":{"type":"string","nullable":true},"channel":{"type":"string","enum":["client","tech","internal","dm","system"]},"source":{"type":"string","enum":["platform","crewlink_office"],"description":"platform: mirrored in; crewlink_office: written in CrewLink (sent to the platform as external_message.created)"},"platform":{"type":"string","nullable":true,"description":"Slug of the platform it came from or goes to"},"author":{"type":"object","properties":{"externalId":{"type":"string","nullable":true},"displayName":{"type":"string"},"role":{"type":"string","nullable":true},"personId":{"type":"number","nullable":true},"userId":{"type":"string","nullable":true}},"required":["externalId","displayName","role","personId","userId"],"additionalProperties":false},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"A signed download link from CrewLink's storage, good for 15 minutes (ask again for a fresh one); while a linked file isn't copied yet, its sourceUrl"},"sourceUrl":{"type":"string","nullable":true,"description":"The https link the platform sent, kept as given (null for a file uploaded to CrewLink)"},"copied":{"type":"boolean","description":"The file is in CrewLink's storage. false: the copy failed or is waiting; it is retried in the background"}},"required":["name","type","size","url","sourceUrl","copied"],"additionalProperties":false}},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"{ \"<platform>\": message id, \"<platform>Job\": case, \"<platform>Visit\": visit }, as known"},"caseRef":{"type":"string","nullable":true,"description":"The platform's job or case id it belongs to (kept for your platform; group by siteId in CrewLink)"},"siteId":{"type":"number","nullable":true,"description":"The CrewLink site (one property for one client company) of the message's job, as it is now (a merge moves it); null outside a job"},"createdAt":{"type":"string"},"receivedAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"deletedAt":{"type":"string","nullable":true}},"required":["id","jobId","threadKey","channel","source","platform","author","body","attachments","externalRefs","caseRef","siteId","createdAt","receivedAt","editedAt","deletedAt"],"additionalProperties":false},"status":{"type":"string","enum":["created","updated","unchanged","linked"],"description":"created: new; updated: a message with that id was there and now matches what you sent; unchanged: it already did (or it is a tombstone, which stays deleted); linked: your id now points at an office reply"}},"required":["message","status"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}},"get":{"summary":"A mirrored DM or group outside a job","tags":["Mirrored conversations"],"description":"Company-scope office users and platform keys (read; their own platform's messages). Oldest first, paged with nextCursor.","parameters":[{"schema":{"type":"string","maxLength":100},"in":"query","name":"channel","required":false,"description":"Comma list of client, tech, internal, dm, system"},{"schema":{"type":"string","maxLength":200},"in":"query","name":"cursor","required":false,"description":"nextCursor from the previous page"},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"in":"query","name":"limit","required":false},{"schema":{"type":"string","enum":["asc","desc"],"default":"asc"},"in":"query","name":"order","required":false,"description":"By when each message was written (createdAt, then id): asc (oldest first, the default) or desc"},{"schema":{"type":"string","enum":["true","false"],"default":"true"},"in":"query","name":"includeDeleted","required":false,"description":"Tombstones (deleted messages: empty body, deletedAt set) are listed unless false"},{"schema":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$"},"in":"path","name":"threadKey","required":true,"description":"Your thread id (a DM or group outside a job)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"jobId":{"type":"number","nullable":true},"threadKey":{"type":"string","nullable":true},"channel":{"type":"string","enum":["client","tech","internal","dm","system"]},"source":{"type":"string","enum":["platform","crewlink_office"],"description":"platform: mirrored in; crewlink_office: written in CrewLink (sent to the platform as external_message.created)"},"platform":{"type":"string","nullable":true,"description":"Slug of the platform it came from or goes to"},"author":{"type":"object","properties":{"externalId":{"type":"string","nullable":true},"displayName":{"type":"string"},"role":{"type":"string","nullable":true},"personId":{"type":"number","nullable":true},"userId":{"type":"string","nullable":true}},"required":["externalId","displayName","role","personId","userId"],"additionalProperties":false},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"A signed download link from CrewLink's storage, good for 15 minutes (ask again for a fresh one); while a linked file isn't copied yet, its sourceUrl"},"sourceUrl":{"type":"string","nullable":true,"description":"The https link the platform sent, kept as given (null for a file uploaded to CrewLink)"},"copied":{"type":"boolean","description":"The file is in CrewLink's storage. false: the copy failed or is waiting; it is retried in the background"}},"required":["name","type","size","url","sourceUrl","copied"],"additionalProperties":false}},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"{ \"<platform>\": message id, \"<platform>Job\": case, \"<platform>Visit\": visit }, as known"},"caseRef":{"type":"string","nullable":true,"description":"The platform's job or case id it belongs to (kept for your platform; group by siteId in CrewLink)"},"siteId":{"type":"number","nullable":true,"description":"The CrewLink site (one property for one client company) of the message's job, as it is now (a merge moves it); null outside a job"},"createdAt":{"type":"string"},"receivedAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"deletedAt":{"type":"string","nullable":true}},"required":["id","jobId","threadKey","channel","source","platform","author","body","attachments","externalRefs","caseRef","siteId","createdAt","receivedAt","editedAt","deletedAt"],"additionalProperties":false}},"nextCursor":{"type":"string","nullable":true}},"required":["messages","nextCursor"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/external-messages/{id}":{"patch":{"summary":"Edit a mirrored message (platform), or your own office reply (office)","tags":["Mirrored conversations"],"description":"Platform keys (write): their own mirrored messages only; send the fields that changed (editedAt defaults to now). Mirrored messages notify nobody: no text, email or push to techs, clients or homeowners, nothing in CrewLink messaging and no webhook, whatever the job or grant says. 409 deleted for a tombstone, not_mirrored for an office reply. Office users: only their own replies, and the company API key only the replies it wrote (`body` only; 403 not_yours otherwise); the owning platform gets external_message.updated.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","maxLength":20000},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"type":{"type":"string","minLength":1,"maxLength":100,"description":"MIME type, e.g. image/jpeg"},"size":{"type":"integer","minimum":0,"maximum":2147483647,"description":"Bytes"},"url":{"type":"string","format":"uri","maxLength":2000,"pattern":"^https\\:\\/\\/","description":"Where the file lives (yours, https): CrewLink copies it into its own storage on ingest (backfill: within minutes) and keeps this link as sourceUrl"},"key":{"type":"string","maxLength":300,"description":"Or: a file uploaded to CrewLink first (POST /v1/office/external-messages/attachments)"}},"required":["name","type","size"],"additionalProperties":false,"description":"Exactly one of url or key"},"maxItems":20},"channel":{"type":"string","enum":["client","tech","internal","dm","system"]},"author":{"type":"object","properties":{"externalId":{"type":"string","minLength":1,"maxLength":200,"description":"Your id for the person (with personId absent, a CrewLink person whose externalRefs.<platform> is this id is linked)"},"displayName":{"type":"string","minLength":1,"maxLength":200},"role":{"type":"string","minLength":1,"maxLength":40,"description":"Free text: installer, technician, homeowner, admin, ..."},"personId":{"type":"integer","description":"The CrewLink person (one of this company), when you know it"}},"required":["displayName"],"additionalProperties":false},"editedAt":{"type":"string","format":"date-time","description":"When it was edited (default now)"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":220},"in":"path","name":"id","required":true,"description":"The CrewLink message id, or `ref:<your message id>` (externalRefs.<your platform slug>)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"object","properties":{"id":{"type":"number"},"jobId":{"type":"number","nullable":true},"threadKey":{"type":"string","nullable":true},"channel":{"type":"string","enum":["client","tech","internal","dm","system"]},"source":{"type":"string","enum":["platform","crewlink_office"],"description":"platform: mirrored in; crewlink_office: written in CrewLink (sent to the platform as external_message.created)"},"platform":{"type":"string","nullable":true,"description":"Slug of the platform it came from or goes to"},"author":{"type":"object","properties":{"externalId":{"type":"string","nullable":true},"displayName":{"type":"string"},"role":{"type":"string","nullable":true},"personId":{"type":"number","nullable":true},"userId":{"type":"string","nullable":true}},"required":["externalId","displayName","role","personId","userId"],"additionalProperties":false},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"A signed download link from CrewLink's storage, good for 15 minutes (ask again for a fresh one); while a linked file isn't copied yet, its sourceUrl"},"sourceUrl":{"type":"string","nullable":true,"description":"The https link the platform sent, kept as given (null for a file uploaded to CrewLink)"},"copied":{"type":"boolean","description":"The file is in CrewLink's storage. false: the copy failed or is waiting; it is retried in the background"}},"required":["name","type","size","url","sourceUrl","copied"],"additionalProperties":false}},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"{ \"<platform>\": message id, \"<platform>Job\": case, \"<platform>Visit\": visit }, as known"},"caseRef":{"type":"string","nullable":true,"description":"The platform's job or case id it belongs to (kept for your platform; group by siteId in CrewLink)"},"siteId":{"type":"number","nullable":true,"description":"The CrewLink site (one property for one client company) of the message's job, as it is now (a merge moves it); null outside a job"},"createdAt":{"type":"string"},"receivedAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"deletedAt":{"type":"string","nullable":true}},"required":["id","jobId","threadKey","channel","source","platform","author","body","attachments","externalRefs","caseRef","siteId","createdAt","receivedAt","editedAt","deletedAt"],"additionalProperties":false},"status":{"type":"string","enum":["created","updated","unchanged","linked"],"description":"created: new; updated: a message with that id was there and now matches what you sent; unchanged: it already did (or it is a tombstone, which stays deleted); linked: your id now points at an office reply"},"webhookQueued":{"type":"boolean","description":"Office edits: an external_message.updated delivery was queued"}},"required":["message","status"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}},"delete":{"summary":"Delete a mirrored message (platform), or your own office reply (office): a tombstone","tags":["Mirrored conversations"],"description":"Platform keys (write): their own mirrored messages only. The body and files are removed (files in CrewLink's storage are deleted); the row stays with deletedAt, so a replay of the same id can't bring it back. Deleting again is harmless. Mirrored messages notify nobody: no text, email or push to techs, clients or homeowners, nothing in CrewLink messaging and no webhook, whatever the job or grant says. Office users: only their own replies, and the company API key only the replies it wrote (403 not_yours otherwise); the owning platform gets external_message.deleted.","parameters":[{"schema":{"type":"string","format":"date-time"},"in":"query","name":"deletedAt","required":false,"description":"When it was deleted (default now)"},{"schema":{"type":"string","minLength":1,"maxLength":220},"in":"path","name":"id","required":true,"description":"The CrewLink message id, or `ref:<your message id>` (externalRefs.<your platform slug>)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"object","properties":{"id":{"type":"number"},"jobId":{"type":"number","nullable":true},"threadKey":{"type":"string","nullable":true},"channel":{"type":"string","enum":["client","tech","internal","dm","system"]},"source":{"type":"string","enum":["platform","crewlink_office"],"description":"platform: mirrored in; crewlink_office: written in CrewLink (sent to the platform as external_message.created)"},"platform":{"type":"string","nullable":true,"description":"Slug of the platform it came from or goes to"},"author":{"type":"object","properties":{"externalId":{"type":"string","nullable":true},"displayName":{"type":"string"},"role":{"type":"string","nullable":true},"personId":{"type":"number","nullable":true},"userId":{"type":"string","nullable":true}},"required":["externalId","displayName","role","personId","userId"],"additionalProperties":false},"body":{"type":"string"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"type":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"A signed download link from CrewLink's storage, good for 15 minutes (ask again for a fresh one); while a linked file isn't copied yet, its sourceUrl"},"sourceUrl":{"type":"string","nullable":true,"description":"The https link the platform sent, kept as given (null for a file uploaded to CrewLink)"},"copied":{"type":"boolean","description":"The file is in CrewLink's storage. false: the copy failed or is waiting; it is retried in the background"}},"required":["name","type","size","url","sourceUrl","copied"],"additionalProperties":false}},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"{ \"<platform>\": message id, \"<platform>Job\": case, \"<platform>Visit\": visit }, as known"},"caseRef":{"type":"string","nullable":true,"description":"The platform's job or case id it belongs to (kept for your platform; group by siteId in CrewLink)"},"siteId":{"type":"number","nullable":true,"description":"The CrewLink site (one property for one client company) of the message's job, as it is now (a merge moves it); null outside a job"},"createdAt":{"type":"string"},"receivedAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"deletedAt":{"type":"string","nullable":true}},"required":["id","jobId","threadKey","channel","source","platform","author","body","attachments","externalRefs","caseRef","siteId","createdAt","receivedAt","editedAt","deletedAt"],"additionalProperties":false},"status":{"type":"string","enum":["created","updated","unchanged","linked"],"description":"created: new; updated: a message with that id was there and now matches what you sent; unchanged: it already did (or it is a tombstone, which stays deleted); linked: your id now points at an office reply"},"webhookQueued":{"type":"boolean","description":"Office deletes: an external_message.deleted delivery was queued"}},"required":["message","status"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/external-messages/backfill":{"post":{"summary":"Backfill mirrored history in bulk (platform)","tags":["Mirrored conversations"],"description":"Platform keys (write). Up to 500 messages a call, each with its original createdAt (and editedAt or deletedAt), in any order: reads always order by createdAt. The request body may be up to 16 MB (413 beyond). Each is stored like a single POST (idempotent by your message id, so a backfill can be re-run or resumed), and one bad message doesn't stop the others: `results` has one entry per message in the order sent. Mirrored messages notify nobody: no text, email or push to techs, clients or homeowners, nothing in CrewLink messaging and no webhook, whatever the job or grant says. 30 calls a minute per platform key (429 with Retry-After), on top of the key's overall limit.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{"type":"object","properties":{"channel":{"type":"string","enum":["client","tech","internal","dm","system"],"description":"client | tech | internal | dm | system. The office writes client, tech or internal"},"body":{"type":"string","maxLength":20000,"default":"","description":"Markdown, up to 20000 characters (may be empty with attachments)"},"author":{"type":"object","properties":{"externalId":{"type":"string","minLength":1,"maxLength":200,"description":"Your id for the person (with personId absent, a CrewLink person whose externalRefs.<platform> is this id is linked)"},"displayName":{"type":"string","minLength":1,"maxLength":200},"role":{"type":"string","minLength":1,"maxLength":40,"description":"Free text: installer, technician, homeowner, admin, ..."},"personId":{"type":"integer","description":"The CrewLink person (one of this company), when you know it"}},"required":["displayName"],"additionalProperties":false,"description":"Required from a platform; ignored for an office reply (the signed-in user)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200},"description":"Your ids: { \"<platform>\": message id (required from a platform, unique per company), \"<platform>Job\": the job or case it belongs to, \"<platform>Visit\": the visit }. For a platform: { \"acme\": \"msg_1\", \"acmeJob\": \"job_9\", \"acmeVisit\": \"visit_3\" }"},"attachments":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"type":{"type":"string","minLength":1,"maxLength":100,"description":"MIME type, e.g. image/jpeg"},"size":{"type":"integer","minimum":0,"maximum":2147483647,"description":"Bytes"},"url":{"type":"string","format":"uri","maxLength":2000,"pattern":"^https\\:\\/\\/","description":"Where the file lives (yours, https): CrewLink copies it into its own storage on ingest (backfill: within minutes) and keeps this link as sourceUrl"},"key":{"type":"string","maxLength":300,"description":"Or: a file uploaded to CrewLink first (POST /v1/office/external-messages/attachments)"}},"required":["name","type","size"],"additionalProperties":false,"description":"Exactly one of url or key"},"maxItems":20,"default":[],"description":"Up to 20; platforms only"},"createdAt":{"type":"string","format":"date-time","description":"When it was written (default now; may be any time in the past, not more than 5 minutes ahead). Platforms only"},"editedAt":{"type":"string","format":"date-time","description":"When it was last edited. Platforms only"},"deletedAt":{"type":"string","format":"date-time","description":"Mirror a message that is already deleted (stored as a tombstone). Platforms only"},"threadKey":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,199}$","description":"Your thread id, when the job's message is in a group or DM thread"},"crewlinkMessageId":{"type":"integer","description":"Link your id (externalRefs) to an office reply you received as external_message.created instead of storing a copy"},"jobId":{"type":"integer","description":"The CrewLink job"},"jobExternalRef":{"type":"string","minLength":1,"maxLength":200,"description":"Or the job by your id for it"},"jobExternalSource":{"type":"string","maxLength":100,"description":"Whose id jobExternalRef is (default your platform slug)"}},"required":["channel"],"additionalProperties":false,"description":"A message: on a job (jobId or jobExternalRef, optionally in threadKey), or in a thread outside a job (threadKey alone)"},"minItems":1,"maxItems":500}},"required":["messages"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"created":{"type":"number"},"updated":{"type":"number"},"unchanged":{"type":"number"},"failed":{"type":"number"},"results":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"status":{"type":"string","enum":["created","updated","unchanged","linked","error"]},"id":{"type":"number","nullable":true},"error":{"type":"string","nullable":true},"message":{"type":"string","nullable":true}},"required":["index","status","id","error","message"],"additionalProperties":false}}},"required":["created","updated","unchanged","failed","results"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/external-messages/attachments":{"post":{"summary":"Upload a file for a mirrored message (platform): get an upload URL","tags":["Mirrored conversations"],"description":"Platform keys (write). PUT the bytes to `uploadUrl` (same Content-Type) within 15 minutes, then send { name, type, size, key } in a message's attachments; the file is checked in storage then (422 bad_attachment when missing, bigger than declared or another type). application/pdf, image/jpeg, image/png, image/heic, image/webp, text/plain, text/csv, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, up to 20 MB. Or skip this and send a file's https url, stored as given.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contentType":{"type":"string","enum":["application/pdf","image/jpeg","image/png","image/heic","image/webp","text/plain","text/csv","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]},"name":{"type":"string","minLength":1,"maxLength":200},"size":{"type":"integer","minimum":1,"maximum":20971520}},"required":["contentType","name","size"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"},"uploadUrl":{"type":"string"},"expiresInSeconds":{"type":"number"}},"required":["key","uploadUrl","expiresInSeconds"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/services":{"get":{"summary":"Your services, including turned-off ones","tags":["Office"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"minutes":{"type":"number"},"skills":{"type":"number"},"peopleNeeded":{"type":"number"},"days":{"type":"number"},"priceCents":{"type":"number","nullable":true},"depositCents":{"type":"number"},"currency":{"type":"string"},"active":{"type":"boolean"},"basePayCents":{"type":"number","nullable":true,"description":"Tech pay per completed job (routes/pay.ts); null: none"},"paySplit":{"type":"string","enum":["split","each"],"description":"Multi-person jobs: split (the base pay is shared equally) or each (everyone gets it)"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true},"daylightOnly":{"type":"boolean","description":"true: offered and booked only when the whole visit is in daylight at the address (start at or after local sunrise, end at or before local sunset); 409 outside_daylight otherwise"}},"required":["id","name","minutes","skills","peopleNeeded","days","priceCents","depositCents","currency","active","basePayCents","paySplit","category","daylightOnly"],"additionalProperties":false}}},"required":["services"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"post":{"summary":"Add a service","tags":["Office"],"description":"Active services show on the booking page and to CrewConnect senders. 422 when the deposit is more than the price.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"minutes":{"type":"integer","minimum":5,"maximum":1440,"description":"Default length of one person's visit; the engine blocks 20% more until it learns each person's time"},"skills":{"type":"integer","minimum":0,"description":"Bit mask: who can do it (people whose skills include every bit)","default":1},"peopleNeeded":{"type":"integer","minimum":1,"maximum":10,"description":"People who go together","default":1},"days":{"type":"integer","minimum":1,"maximum":14,"description":"Consecutive days, same people and start time each day","default":1},"priceCents":{"type":"integer","minimum":0,"nullable":true,"description":"List price; null: no set price"},"depositCents":{"type":"integer","minimum":0,"description":"Taken when the customer books online; 0: none","default":0},"currency":{"type":"string","pattern":"^[A-Z]{3}$","description":"ISO 4217, e.g. USD","default":"USD"},"active":{"type":"boolean","description":"false: no longer offered or booked (booked jobs stay)","default":true},"basePayCents":{"type":"integer","minimum":0,"maximum":10000000,"nullable":true,"description":"What the tech is paid per completed job (owner only); null: no base pay. A person's own rate (PUT /v1/office/team/{id}/pay-rates/{serviceId}) wins"},"paySplit":{"type":"string","enum":["split","each"],"description":"Multi-person jobs: split the base pay equally (default), or pay each person all of it (owner only)","default":"split"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true,"description":"residential, commercial or other (shown on the booking page catalog; integrators branch on it); null: not set"},"daylightOnly":{"type":"boolean","description":"Owner only. true: times are offered and booked only when the whole visit is in daylight at the job's address on that date (start at or after local sunrise, end at or before local sunset, worked out from the address and date); outside it holds, bookings and moves get 409 outside_daylight. Imports and history are exempt; an owner may override it with allowOutsideHours on an office create or move. Default false"}},"required":["name","minutes"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"minutes":{"type":"number"},"skills":{"type":"number"},"peopleNeeded":{"type":"number"},"days":{"type":"number"},"priceCents":{"type":"number","nullable":true},"depositCents":{"type":"number"},"currency":{"type":"string"},"active":{"type":"boolean"},"basePayCents":{"type":"number","nullable":true,"description":"Tech pay per completed job (routes/pay.ts); null: none"},"paySplit":{"type":"string","enum":["split","each"],"description":"Multi-person jobs: split (the base pay is shared equally) or each (everyone gets it)"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true},"daylightOnly":{"type":"boolean","description":"true: offered and booked only when the whole visit is in daylight at the address (start at or after local sunrise, end at or before local sunset); 409 outside_daylight otherwise"}},"required":["id","name","minutes","skills","peopleNeeded","days","priceCents","depositCents","currency","active","basePayCents","paySplit","category","daylightOnly"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/services/{id}":{"patch":{"summary":"Change a service, or turn it off","tags":["Office"],"description":"Changes apply to times offered and jobs booked from now on; booked jobs keep their visits. active: false takes it off the booking page and CrewConnect and stops recurring visits of it from being booked. basePayCents and paySplit (tech pay) and daylightOnly are owner only (403 otherwise); a new base pay applies to jobs completed from now on. 422 when the deposit would be more than the price.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"minutes":{"type":"integer","minimum":5,"maximum":1440,"description":"Default length of one person's visit; the engine blocks 20% more until it learns each person's time"},"skills":{"type":"integer","minimum":0,"description":"Bit mask: who can do it (people whose skills include every bit)"},"peopleNeeded":{"type":"integer","minimum":1,"maximum":10,"description":"People who go together"},"days":{"type":"integer","minimum":1,"maximum":14,"description":"Consecutive days, same people and start time each day"},"priceCents":{"type":"integer","minimum":0,"nullable":true,"description":"List price; null: no set price"},"depositCents":{"type":"integer","minimum":0,"description":"Taken when the customer books online; 0: none"},"currency":{"type":"string","pattern":"^[A-Z]{3}$","description":"ISO 4217, e.g. USD"},"active":{"type":"boolean","description":"false: no longer offered or booked (booked jobs stay)"},"basePayCents":{"type":"integer","minimum":0,"maximum":10000000,"nullable":true,"description":"What the tech is paid per completed job (owner only); null: no base pay. A person's own rate (PUT /v1/office/team/{id}/pay-rates/{serviceId}) wins"},"paySplit":{"type":"string","enum":["split","each"],"description":"Multi-person jobs: split the base pay equally (default), or pay each person all of it (owner only)"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true,"description":"residential, commercial or other (shown on the booking page catalog; integrators branch on it); null: not set"},"daylightOnly":{"type":"boolean","description":"Owner only. true: times are offered and booked only when the whole visit is in daylight at the job's address on that date (start at or after local sunrise, end at or before local sunset, worked out from the address and date); outside it holds, bookings and moves get 409 outside_daylight. Imports and history are exempt; an owner may override it with allowOutsideHours on an office create or move. Default false"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"minutes":{"type":"number"},"skills":{"type":"number"},"peopleNeeded":{"type":"number"},"days":{"type":"number"},"priceCents":{"type":"number","nullable":true},"depositCents":{"type":"number"},"currency":{"type":"string"},"active":{"type":"boolean"},"basePayCents":{"type":"number","nullable":true,"description":"Tech pay per completed job (routes/pay.ts); null: none"},"paySplit":{"type":"string","enum":["split","each"],"description":"Multi-person jobs: split (the base pay is shared equally) or each (everyone gets it)"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true},"daylightOnly":{"type":"boolean","description":"true: offered and booked only when the whole visit is in daylight at the address (start at or after local sunrise, end at or before local sunset); 409 outside_daylight otherwise"}},"required":["id","name","minutes","skills","peopleNeeded","days","priceCents","depositCents","currency","active","basePayCents","paySplit","category","daylightOnly"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/branches":{"get":{"summary":"Your branches (each one a service area)","tags":["Office"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"branches":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"lat":{"type":"number","description":"Where the branch is: the centre of its service area, and the start of the day for its people who have no home of their own"},"lng":{"type":"number"},"address":{"type":"string","nullable":true,"description":"Where it is, as one line (from address search); null: only coordinates are on file"},"located":{"type":"boolean","description":"false: no usable location (0,0): its area covers nothing and it can't be anyone's start of day until an owner sets one (PATCH lat, lng, address)"},"areaKm":{"type":"number"},"timezone":{"type":"string","description":"IANA zone of this branch's jobs, hours and days (its own, else the company's)"},"ownTimezone":{"type":"string","nullable":true,"description":"The zone set on the branch; null: it follows the company"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 (its own, else the company's)"},"ownCountry":{"type":"string","nullable":true,"description":"The country set on the branch; null: the company's"}},"required":["id","name","lat","lng","address","located","areaKm","timezone","ownTimezone","country","ownCountry"],"additionalProperties":false}}},"required":["branches"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"post":{"summary":"Add a branch","tags":["Office"],"description":"People in the branch (branchId on the team) work within areaKm of it. lat/lng (with `address`, its line from address search) is where it is: the centre of its area, and where the day starts for its people who have no home of their own. Addresses outside every branch get outsideArea on the booking page. timezone: the branch's IANA zone when it isn't the company's (a Denver branch of a Chicago company): its people's hours and days, and the jobs it serves, are in it. country: when it isn't the company's (a London branch of a US company): its phone numbers get texts and address search covers it. 422 for an unknown zone or country.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"address":{"type":"string","minLength":1,"maxLength":300,"description":"Where it is, one line (pick it from address search: lat/lng is where it is)"},"areaKm":{"type":"number","minimum":1,"maximum":500},"timezone":{"type":"string","maxLength":64,"description":"IANA zone, e.g. 'America/Denver', 'Europe/London'; null: the company's","nullable":true},"country":{"type":"string","pattern":"^[A-Za-z]{2}$","description":"ISO 3166-1 alpha-2, e.g. 'US', 'GB'; null: the company's","nullable":true}},"required":["name","lat","lng","areaKm"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"lat":{"type":"number","description":"Where the branch is: the centre of its service area, and the start of the day for its people who have no home of their own"},"lng":{"type":"number"},"address":{"type":"string","nullable":true,"description":"Where it is, as one line (from address search); null: only coordinates are on file"},"located":{"type":"boolean","description":"false: no usable location (0,0): its area covers nothing and it can't be anyone's start of day until an owner sets one (PATCH lat, lng, address)"},"areaKm":{"type":"number"},"timezone":{"type":"string","description":"IANA zone of this branch's jobs, hours and days (its own, else the company's)"},"ownTimezone":{"type":"string","nullable":true,"description":"The zone set on the branch; null: it follows the company"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 (its own, else the company's)"},"ownCountry":{"type":"string","nullable":true,"description":"The country set on the branch; null: the company's"}},"required":["id","name","lat","lng","address","located","areaKm","timezone","ownTimezone","country","ownCountry"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/branches/{id}":{"patch":{"summary":"Change a branch: name, location, area, time zone or country (owner only)","tags":["Office"],"description":"Send only what changes. A new location (lat, lng and address together) moves the centre of its area, and its people who start from the branch (added without a home of their own) move with it; booked jobs stay. A new time zone (or null: back to the company's) keeps every booked instant: its people's jobs from now on, and their time off, are re-expressed in the new zone (a 9:00 visit in Chicago time shows as 8:00 in Denver time), and the answer says how many moved. Working hours stay as typed (9 to 5 means 9 to 5 in the new zone). 403 for dispatchers, 404 for another company's branch, 422 for an unknown zone or country.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"address":{"type":"string","minLength":1,"maxLength":300,"nullable":true,"description":"Where it is, one line. Send it with lat/lng when it moves; null clears it (the coordinates stay)"},"areaKm":{"type":"number","minimum":1,"maximum":500},"timezone":{"type":"string","maxLength":64,"description":"IANA zone, e.g. 'America/Denver', 'Europe/London'; null: the company's","nullable":true},"country":{"type":"string","pattern":"^[A-Za-z]{2}$","description":"ISO 3166-1 alpha-2, e.g. 'US', 'GB'; null: the company's","nullable":true}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"lat":{"type":"number","description":"Where the branch is: the centre of its service area, and the start of the day for its people who have no home of their own"},"lng":{"type":"number"},"address":{"type":"string","nullable":true,"description":"Where it is, as one line (from address search); null: only coordinates are on file"},"located":{"type":"boolean","description":"false: no usable location (0,0): its area covers nothing and it can't be anyone's start of day until an owner sets one (PATCH lat, lng, address)"},"areaKm":{"type":"number"},"timezone":{"type":"string","description":"IANA zone of this branch's jobs, hours and days (its own, else the company's)"},"ownTimezone":{"type":"string","nullable":true,"description":"The zone set on the branch; null: it follows the company"},"country":{"type":"string","description":"ISO 3166-1 alpha-2 (its own, else the company's)"},"ownCountry":{"type":"string","nullable":true,"description":"The country set on the branch; null: the company's"},"rezoned":{"type":"object","properties":{"jobs":{"type":"number"},"blocks":{"type":"number"}},"required":["jobs","blocks"],"additionalProperties":false,"description":"Booked jobs and time-off blocks re-expressed in the new zone"}},"required":["id","name","lat","lng","address","located","areaKm","timezone","ownTimezone","country","ownCountry","rezoned"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/api-key/rotate":{"post":{"summary":"Replace the company API key (owner only)","tags":["Office"],"description":"Returns the new key once (`<company id>.<secret>`). The old key stops working at once, so update every integration that uses it. User tokens and sessions are not affected.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"apiKey":{"type":"string","description":"Shown once"}},"required":["apiKey"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/integration-key/rotate":{"post":{"summary":"Create or replace the integration key for your website (owner only)","tags":["Office"],"description":"Returns the new key once (`pk_<secret>`). Send it as `X-CrewLink-Key` on the public booking endpoints and the holds, bookings, requests and address-search limits count per key instead of per visitor IP, with higher ceilings (300 a minute per endpoint, 50 live holds). It is safe to put in your website's code: it grants no access to data, it only raises those limits and attributes the traffic to you. The old key stops working at once. There is no key until you create one. Never put the company API key in a web page.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"integrationKey":{"type":"string","description":"Shown once"}},"required":["integrationKey"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}/documents":{"get":{"summary":"Someone's documents (insurance, licence, W-9...)","tags":["Office"],"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"documents":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"personId":{"type":"number"},"kind":{"type":"string","enum":["insurance","license","w9","certification","id","other"]},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"expiresOn":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","uploaded"]},"createdAt":{"type":"string"},"url":{"type":"string","nullable":true,"description":"A link to view it for 10 minutes (uploaded documents only)"}},"required":["id","personId","kind","name","contentType","size","expiresOn","status","createdAt","url"],"additionalProperties":false}}},"required":["documents"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"post":{"summary":"Add a document: get an upload URL","tags":["Office"],"description":"PUT the file to `uploadUrl` (with the same Content-Type) within 15 minutes, then mark it uploaded. PDF, JPEG, PNG, HEIC or WebP, up to 25 MB. Link it to a credential with documentId on POST /v1/office/team/{id}/credentials.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["insurance","license","w9","certification","id","other"]},"name":{"type":"string","minLength":1,"maxLength":200},"contentType":{"type":"string","enum":["application/pdf","image/jpeg","image/png","image/heic","image/webp"]},"size":{"type":"integer","minimum":1,"maximum":26214400},"expiresOn":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"required":["kind","name","contentType","size"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"document":{"type":"object","properties":{"id":{"type":"string"},"personId":{"type":"number"},"kind":{"type":"string","enum":["insurance","license","w9","certification","id","other"]},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"expiresOn":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","uploaded"]},"createdAt":{"type":"string"},"url":{"type":"string","nullable":true,"description":"A link to view it for 10 minutes (uploaded documents only)"}},"required":["id","personId","kind","name","contentType","size","expiresOn","status","createdAt","url"],"additionalProperties":false},"uploadUrl":{"type":"string"}},"required":["document","uploadUrl"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}/documents/{docId}/uploaded":{"post":{"summary":"Mark a document uploaded","tags":["Office"],"description":"Checks the file in storage first: `409 not_uploaded` when nothing is at the upload URL yet; `422 upload_mismatch` when it is bigger than the declared `size` or 25 MB, or isn't the declared Content-Type (the file and the pending document are then removed: add it again). Records the real size. Repeating it is harmless.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"docId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"personId":{"type":"number"},"kind":{"type":"string","enum":["insurance","license","w9","certification","id","other"]},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"expiresOn":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","uploaded"]},"createdAt":{"type":"string"},"url":{"type":"string","nullable":true,"description":"A link to view it for 10 minutes (uploaded documents only)"}},"required":["id","personId","kind","name","contentType","size","expiresOn","status","createdAt","url"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/team/{id}/documents/{docId}":{"delete":{"summary":"Remove a document","tags":["Office"],"description":"The file is deleted from storage. Credentials linked to it keep their details and lose the link.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"docId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}},"required":["deleted"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}/deactivate":{"post":{"summary":"Deactivate someone (owner only)","tags":["Office"],"description":"They're no longer offered for work and their field-app sign-in is turned off (every session ends, passkeys are removed). Jobs already booked stay put: `upcoming` lists them so the office can move them. Pay already earned stays in the pay report.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"active":{"type":"boolean"},"signInsTurnedOff":{"type":"number"},"upcoming":{"type":"array","items":{"type":"number"},"description":"Booked job ids from today on"}},"required":["id","active","signInsTurnedOff","upcoming"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}/reactivate":{"post":{"summary":"Reactivate someone (owner only)","tags":["Office"],"description":"They're offered for work again. Their sign-in stays off: turn it back on with POST /v1/office/users/{id}/reactivate (`signIns` lists their user ids), so plan login limits apply as usual.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"active":{"type":"boolean"},"signIns":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string"},"active":{"type":"boolean"}},"required":["userId","active"],"additionalProperties":false}}},"required":["id","active","signIns"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}/reset-sign-in":{"post":{"summary":"Reset someone's sign-in (owner only)","tags":["Office"],"description":"For a lost phone or a shared device: signs them out everywhere, removes their passkeys and replaces their token (the old one stops working). They sign in again with a code emailed to them. There are no passwords to reset.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"signInsReset":{"type":"number"}},"required":["id","signInsReset"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/profile":{"get":{"summary":"My hours and home","tags":["Field app"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"designation":{"type":"string","enum":["w2","1099"]},"verified":{"type":"boolean"},"home":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"minItems":7,"maxItems":7,"description":"Monday first: the windows you work; [] when off"},"pendingChange":{"type":"object","properties":{"from":{"type":"string"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"nullable":true},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true}},"required":["from","availability","address"],"additionalProperties":false,"nullable":true,"description":"Your change waiting for its day"},"canEdit":{"type":"boolean","description":"You may change your own hours and home address here: your company lets techs, and no integration owns your availability"},"managedBy":{"type":"string","nullable":true,"description":"The integration that owns your hours, time off and home (e.g. acme), or null"},"managedByName":{"type":"string","nullable":true,"description":"Its name, e.g. \"Acme\": change your hours there"}},"required":["name","designation","verified","home","address","availability","pendingChange","canEdit","managedBy","managedByName"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"patch":{"summary":"Change my hours or home address (from tomorrow)","tags":["Field app"],"description":"availability: the windows you work each weekday (Monday first; [] = off). address with homeLat/homeLng: where you start and end your day. Changes take effect tomorrow, so today's schedule stays as booked; the office sees the change waiting. Sending again before then replaces it. 403 not_allowed when your company doesn't let techs change these (ask the office). 409 managed_by_integration when an integration owns your hours and home: change them there.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"integer","minimum":0,"maximum":1440},{"type":"integer","minimum":0,"maximum":1440}]},"maxItems":8},"minItems":7,"maxItems":7,"description":"Monday first: each day a list of [start, end] windows, minutes after midnight (at most 8, not overlapping); [] when off"},"address":{"type":"object","properties":{"line1":{"type":"string","minLength":1,"maxLength":200},"line2":{"type":"string","maxLength":200,"nullable":true},"city":{"type":"string","minLength":1,"maxLength":120},"region":{"type":"string","maxLength":120,"nullable":true,"description":"State, province or region"},"postalCode":{"type":"string","maxLength":20,"nullable":true},"country":{"type":"string","maxLength":60,"nullable":true}},"required":["line1","city"],"additionalProperties":false},"homeLat":{"type":"number","minimum":-90,"maximum":90},"homeLng":{"type":"number","minimum":-180,"maximum":180}},"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"designation":{"type":"string","enum":["w2","1099"]},"verified":{"type":"boolean"},"home":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"minItems":7,"maxItems":7,"description":"Monday first: the windows you work; [] when off"},"pendingChange":{"type":"object","properties":{"from":{"type":"string"},"availability":{"type":"array","items":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]}},"nullable":true},"address":{"type":"object","properties":{"line1":{"type":"string"},"line2":{"type":"string","nullable":true},"city":{"type":"string"},"region":{"type":"string","nullable":true},"postalCode":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["line1","city"],"additionalProperties":false,"nullable":true}},"required":["from","availability","address"],"additionalProperties":false,"nullable":true,"description":"Your change waiting for its day"},"canEdit":{"type":"boolean","description":"You may change your own hours and home address here: your company lets techs, and no integration owns your availability"},"managedBy":{"type":"string","nullable":true,"description":"The integration that owns your hours, time off and home (e.g. acme), or null"},"managedByName":{"type":"string","nullable":true,"description":"Its name, e.g. \"Acme\": change your hours there"}},"required":["name","designation","verified","home","address","availability","pendingChange","canEdit","managedBy","managedByName"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"managedBy":{"type":"string"},"managedByName":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/pay/settings":{"get":{"summary":"Pay settings","tags":["Pay"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"mileageRateCents":{"type":"number","description":"Cents paid per route mile (per km when the company works in metric)"},"unit":{"type":"string","enum":["mi","km"],"description":"The company's distance unit (its own choice, else its country's): the rate is per mile or per km"},"weekStartsOn":{"type":"number","description":"First day of the pay week: 0 = Monday ... 6 = Sunday"},"techsEditHours":{"type":"boolean","description":"Techs may change their own hours and home address in the field app (from their next day)"},"techsSeePay":{"type":"boolean","description":"Techs see a \"My pay\" card in the field app"}},"required":["mileageRateCents","unit","weekStartsOn","techsEditHours","techsSeePay"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"patch":{"summary":"Change pay settings (owner only)","tags":["Pay"],"description":"mileageRateCents: what a tech is paid per mile of their day's route (default 25; per km in metric). weekStartsOn: the pay week's first day (0 = Monday, the default ... 6 = Sunday); periods already started keep their dates. techsEditHours (default true): techs may change their own hours and home address in the field app, in force from their next day. techsSeePay (default true): techs see a \"My pay\" card. Returns every pay setting as it is now.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mileageRateCents":{"type":"integer","minimum":0,"maximum":10000},"weekStartsOn":{"type":"integer","minimum":0,"maximum":6},"techsEditHours":{"type":"boolean"},"techsSeePay":{"type":"boolean"}},"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"mileageRateCents":{"type":"number","description":"Cents paid per route mile (per km when the company works in metric)"},"unit":{"type":"string","enum":["mi","km"],"description":"The company's distance unit (its own choice, else its country's): the rate is per mile or per km"},"weekStartsOn":{"type":"number","description":"First day of the pay week: 0 = Monday ... 6 = Sunday"},"techsEditHours":{"type":"boolean","description":"Techs may change their own hours and home address in the field app (from their next day)"},"techsSeePay":{"type":"boolean","description":"Techs see a \"My pay\" card in the field app"}},"required":["mileageRateCents","unit","weekStartsOn","techsEditHours","techsSeePay"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/pay/rates":{"get":{"summary":"Base pay per service, and per-person rates","tags":["Pay"],"description":"services: each service's base pay per completed job (set with PATCH /v1/office/services/{id} basePayCents) and how a multi-person job is paid (split: shared equally; each: everyone gets the full amount). overrides: a person's own base pay for a service, used instead.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"basePayCents":{"type":"number","nullable":true},"paySplit":{"type":"string","enum":["split","each"]},"currency":{"type":"string"},"active":{"type":"boolean"}},"required":["id","name","basePayCents","paySplit","currency","active"],"additionalProperties":false}},"overrides":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"serviceId":{"type":"number"},"basePayCents":{"type":"number"}},"required":["personId","serviceId","basePayCents"],"additionalProperties":false}}},"required":["services","overrides"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/team/{id}/pay-rates/{serviceId}":{"put":{"summary":"Set someone's own base pay for a service (owner only)","tags":["Pay"],"description":"Used instead of the service's base pay for jobs they complete from now on. Jobs already paid keep their lines.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"basePayCents":{"type":"integer","minimum":0,"maximum":10000000}},"required":["basePayCents"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"integer"},"in":"path","name":"serviceId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"personId":{"type":"number"},"serviceId":{"type":"number"},"basePayCents":{"type":"number"}},"required":["personId","serviceId","basePayCents"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"delete":{"summary":"Remove someone's own rate (owner only): the service's base pay applies again","tags":["Pay"],"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"integer"},"in":"path","name":"serviceId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}},"required":["deleted"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/pay":{"get":{"summary":"Technician pay per period and tech","tags":["Pay"],"description":"One row per pay period and tech in the range: jobs completed, job pay, route distance and mileage pay, adjustments, total, paid and due. Open periods are live (lines accrue as jobs are done); closed and paid periods show the totals frozen at close. Filter by one period (`period` = its start date), a tech or a designation. Default range: last pay week through today.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false,"description":"First local date (default: the start of last pay week)"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false,"description":"Last local date, inclusive (default: today)"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"period","required":false,"description":"One period, by its start date"},{"schema":{"type":"integer"},"in":"query","name":"personId","required":false},{"schema":{"type":"string","enum":["w2","1099"]},"in":"query","name":"designation","required":false,"description":"w2: W-2 employee; 1099: 1099 contractor (a tax designation; scheduling treats both the same)"},{"schema":{"type":"string","enum":["true","false"],"default":"false"},"in":"query","name":"includeTest","required":false,"description":"true: include test people (left out by default unless asked for by personId)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"unit":{"type":"string","enum":["mi","km"]},"periods":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"},"status":{"type":"string","enum":["open","closed","paid"]},"closedAt":{"type":"string","nullable":true},"paidAt":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true}},"required":["start","end","status","closedAt","paidAt","reference"],"additionalProperties":false}},"rows":{"type":"array","items":{"type":"object","properties":{"periodStart":{"type":"string"},"periodEnd":{"type":"string"},"status":{"type":"string","enum":["open","closed","paid"],"description":"The period: open (still accruing), closed (frozen), paid"},"personId":{"type":"number"},"name":{"type":"string"},"designation":{"type":"string","enum":["w2","1099"],"description":"w2: W-2 employee; 1099: 1099 contractor (a tax designation; scheduling treats both the same)"},"jobs":{"type":"number","description":"Jobs completed"},"jobPayCents":{"type":"number"},"distance":{"type":"number","description":"Route distance driven, in `unit`"},"unit":{"type":"string","enum":["mi","km"]},"mileageCents":{"type":"number"},"adjustmentsCents":{"type":"number"},"totalCents":{"type":"number"},"paidCents":{"type":"number"},"dueCents":{"type":"number","description":"Total less paid (an open period is still accruing)"},"currency":{"type":"string"},"paidAt":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true}},"required":["periodStart","periodEnd","status","personId","name","designation","jobs","jobPayCents","distance","unit","mileageCents","adjustmentsCents","totalCents","paidCents","dueCents","currency","paidAt","reference"],"additionalProperties":false}},"totals":{"type":"object","properties":{"jobs":{"type":"number"},"jobPayCents":{"type":"number"},"distance":{"type":"number"},"mileageCents":{"type":"number"},"adjustmentsCents":{"type":"number"},"totalCents":{"type":"number"},"paidCents":{"type":"number"},"dueCents":{"type":"number"}},"required":["jobs","jobPayCents","distance","mileageCents","adjustmentsCents","totalCents","paidCents","dueCents"],"additionalProperties":false}},"required":["unit","periods","rows","totals"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/pay/lines":{"get":{"summary":"The lines behind a period: every job, mileage day and adjustment","tags":["Pay"],"description":"By period, or by job (jobId: every line for that job in any period, e.g. what cancelling a done job takes back). 422 without either.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"period","required":false,"description":"The period, by its start date"},{"schema":{"type":"integer"},"in":"query","name":"personId","required":false},{"schema":{"type":"integer"},"in":"query","name":"jobId","required":false,"description":"Every line for this job, whatever the period"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"lines":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"personId":{"type":"number"},"periodStart":{"type":"string"},"date":{"type":"string"},"kind":{"type":"string","enum":["job","mileage","adjustment"]},"jobId":{"type":"number","nullable":true},"service":{"type":"string","nullable":true},"customer":{"type":"string","nullable":true},"amountCents":{"type":"number"},"currency":{"type":"string"},"distance":{"type":"number","nullable":true,"description":"Mileage: the day's route, in `unit`"},"unit":{"type":"string","enum":["mi","km"],"nullable":true},"basis":{"type":"string","enum":["road","straight","mixed"],"nullable":true,"description":"Mileage: road distance (router), straight line x 1.3, or some of each"},"rateCents":{"type":"number","nullable":true},"reason":{"type":"string","nullable":true}},"required":["id","personId","periodStart","date","kind","jobId","service","customer","amountCents","currency","distance","unit","basis","rateCents","reason"],"additionalProperties":false}}},"required":["lines"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/pay/adjustments":{"post":{"summary":"Add a bonus or deduction","tags":["Pay"],"description":"amountCents is positive for a bonus, negative for a deduction. It lands in the pay period containing `date` (default today); 409 period_closed when that period is closed or paid.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"personId":{"type":"integer"},"amountCents":{"type":"integer","minimum":-10000000,"maximum":10000000},"reason":{"type":"string","minLength":1,"maxLength":300},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"currency":{"type":"string","pattern":"^[A-Z]{3}$"}},"required":["personId","amountCents","reason"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"personId":{"type":"number"},"periodStart":{"type":"string"},"date":{"type":"string"},"kind":{"type":"string","enum":["job","mileage","adjustment"]},"jobId":{"type":"number","nullable":true},"service":{"type":"string","nullable":true},"customer":{"type":"string","nullable":true},"amountCents":{"type":"number"},"currency":{"type":"string"},"distance":{"type":"number","nullable":true,"description":"Mileage: the day's route, in `unit`"},"unit":{"type":"string","enum":["mi","km"],"nullable":true},"basis":{"type":"string","enum":["road","straight","mixed"],"nullable":true,"description":"Mileage: road distance (router), straight line x 1.3, or some of each"},"rateCents":{"type":"number","nullable":true},"reason":{"type":"string","nullable":true}},"required":["id","personId","periodStart","date","kind","jobId","service","customer","amountCents","currency","distance","unit","basis","rateCents","reason"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/pay/adjustments/{id}":{"delete":{"summary":"Remove an adjustment (while its period is open)","tags":["Pay"],"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","enum":[true]}},"required":["deleted"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/pay/periods/{start}/close":{"post":{"summary":"Close a pay period (owner only)","tags":["Pay"],"description":"Freezes each tech's totals for the period (mileage redone in road distance first, when a road router is set up). Only once the period has ended (409 not_over). A job finished later counts in the next open period. A period with no lines at all is marked paid straight away.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"path","name":"start","required":true,"description":"The period, by its start date"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"period":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"},"status":{"type":"string","enum":["open","closed","paid"]},"closedAt":{"type":"string","nullable":true},"paidAt":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true}},"required":["start","end","status","closedAt","paidAt","reference"],"additionalProperties":false},"people":{"type":"number","description":"Techs with a statement"}},"required":["period","people"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/pay/periods/{start}/reopen":{"post":{"summary":"Reopen a closed period nobody has been paid for yet (owner only)","tags":["Pay"],"parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"path","name":"start","required":true,"description":"The period, by its start date"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"},"status":{"type":"string","enum":["open","closed","paid"]},"closedAt":{"type":"string","nullable":true},"paidAt":{"type":"string","nullable":true},"reference":{"type":"string","nullable":true}},"required":["start","end","status","closedAt","paidAt","reference"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/pay/periods/{start}/paid":{"post":{"summary":"Mark a closed period paid (owner only)","tags":["Pay"],"description":"Records that the techs were paid, with the payment's reference (check number, payroll run, ACH batch). Everyone in the period, or only `personIds`. When everyone is paid the period is `paid`. CrewLink doesn't move this money: pay techs through your payroll or bank and record it here.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reference":{"type":"string","minLength":1,"maxLength":200},"personIds":{"type":"array","items":{"type":"integer"},"maxItems":500}},"required":["reference"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"path","name":"start","required":true,"description":"The period, by its start date"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"marked":{"type":"array","items":{"type":"number"},"description":"Person ids marked paid now"},"status":{"type":"string","enum":["open","closed","paid"]},"unpaid":{"type":"number","description":"Techs in the period still unpaid"}},"required":["marked","status","unpaid"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/exports/pay.csv":{"get":{"summary":"Export technician pay as CSV","tags":["Exports"],"description":"The pay report (GET /v1/office/pay, same filters) as RFC 4180 CSV with a UTF-8 byte order mark, as an attachment: one row per period and tech. Money in decimal dollars; distance in the company's unit.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false,"description":"First local date (default: the start of last pay week)"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false,"description":"Last local date, inclusive (default: today)"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"period","required":false,"description":"One period, by its start date"},{"schema":{"type":"integer"},"in":"query","name":"personId","required":false},{"schema":{"type":"string","enum":["w2","1099"]},"in":"query","name":"designation","required":false,"description":"w2: W-2 employee; 1099: 1099 contractor (a tax designation; scheduling treats both the same)"},{"schema":{"type":"string","enum":["true","false"],"default":"false"},"in":"query","name":"includeTest","required":false,"description":"true: include test people (left out by default unless asked for by personId)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"text/csv; charset=utf-8","content":{"text/csv":{"schema":{"type":"string","description":"text/csv; charset=utf-8"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/pay":{"get":{"summary":"My pay","tags":["Field app"],"description":"This pay period so far: jobs completed, route distance, job pay, mileage, adjustments and the total; what closed periods still owe you; and your last payment. `visible: false` (and nothing else) when your company doesn't show pay in the app.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"visible":{"type":"boolean"},"pay":{"type":"object","properties":{"period":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"},"status":{"type":"string"}},"required":["start","end","status"],"additionalProperties":false},"jobs":{"type":"number"},"jobPayCents":{"type":"number"},"distance":{"type":"number"},"unit":{"type":"string","enum":["mi","km"]},"mileageCents":{"type":"number"},"adjustmentsCents":{"type":"number"},"totalCents":{"type":"number"},"currency":{"type":"string"},"owedCents":{"type":"number","description":"Closed periods not yet marked paid"},"lastPaid":{"type":"object","properties":{"periodStart":{"type":"string"},"totalCents":{"type":"number"},"paidAt":{"type":"string"}},"required":["periodStart","totalCents","paidAt"],"additionalProperties":false,"nullable":true}},"required":["period","jobs","jobPayCents","distance","unit","mileageCents","adjustmentsCents","totalCents","currency","owedCents","lastPaid"],"additionalProperties":false,"nullable":true}},"required":["visible","pay"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/texts":{"get":{"summary":"A job's texts to the customer, newest first","tags":["Office"],"description":"Confirmations, reminders, moves, on-the-way, review links, invoices and the rest, sent or still queued, with the customer's number masked to its last 4 digits. 404 for a job that isn't yours.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"texts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"kind":{"type":"string","description":"What the text is about: confirmed, reminder, rescheduled, cancelled, on_my_way, review, invoice, deposit, recurring_booked, bumped, request_received, request_declined, bumped_choices, request_queued"},"channel":{"type":"string","enum":["sms","email"]},"to":{"type":"string","description":"The number it went to, masked to its last 4 digits"},"body":{"type":"string","description":"Exactly what was (or will be) sent"},"status":{"type":"string","enum":["queued","sent","failed","cancelled"],"description":"queued: waiting to go out at sendAfter (e.g. a reminder); cancelled: dropped before it went (the job moved or was cancelled)"},"sendAfter":{"type":"string","description":"When it was due to go out (ISO)"},"sentAt":{"type":"string","nullable":true,"description":"When it went out (ISO), or null"}},"required":["id","kind","channel","to","body","status","sendAfter","sentAt"],"additionalProperties":false}}},"required":["jobId","texts"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/connections":{"post":{"summary":"Ask a company or contractor to take your work","tags":["CrewConnect"],"description":"Creates a pending connection from you (sender) to them (partner). They accept and choose what you can see. If your connection to them was ended, this asks again: it goes back to pending (agreed pay terms are kept) and they accept it again. partner is their CrewConnect ID (the last part of their booking-page link), or the whole link (https://.../book/<id>). kind client asks to become their client: you file service requests with them (POST /v1/connect/connections/{id}/requests) instead of sending booked jobs. 404 not_found for an unknown ID, 422 self for your own, 409 exists while it's pending or active.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"partner":{"type":"string","minLength":1,"maxLength":500,"description":"The partner's CrewConnect ID (their booking-page slug, e.g. 'lakeshore'), or their booking-page link"},"kind":{"type":"string","enum":["partner","client"],"default":"partner","description":"client: file service requests with them as their client"}},"required":["partner"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string","enum":["sender","partner"],"description":"Your side: 'sender' sends work, 'partner' does it"},"kind":{"type":"string","enum":["partner","client"],"description":"partner: CrewConnect work sharing. client: the sender is a client company filing service requests with the partner (its provider)"},"company":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"kind":{"type":"string"}},"required":["name","slug","kind"],"additionalProperties":false,"description":"The other side"},"status":{"type":"string","enum":["pending","active","ended"]},"visibility":{"type":"string","enum":["company","people"]},"branding":{"type":"string","enum":["sender","partner"]},"requiredCredentials":{"type":"array","items":{"type":"string"}},"terms":{"type":"object","properties":{"agreed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"What the sender pays the partner per finished job, once both sides agreed"},"proposed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"Terms one side proposed and the other has not accepted yet"},"proposedByYou":{"type":"boolean"}},"required":["agreed","proposed","proposedByYou"],"additionalProperties":false}},"required":["id","role","kind","company","status","visibility","branding","requiredCredentials","terms"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"Your connections, both directions","tags":["CrewConnect"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"connections":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string","enum":["sender","partner"],"description":"Your side: 'sender' sends work, 'partner' does it"},"kind":{"type":"string","enum":["partner","client"],"description":"partner: CrewConnect work sharing. client: the sender is a client company filing service requests with the partner (its provider)"},"company":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"kind":{"type":"string"}},"required":["name","slug","kind"],"additionalProperties":false,"description":"The other side"},"status":{"type":"string","enum":["pending","active","ended"]},"visibility":{"type":"string","enum":["company","people"]},"branding":{"type":"string","enum":["sender","partner"]},"requiredCredentials":{"type":"array","items":{"type":"string"}},"terms":{"type":"object","properties":{"agreed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"What the sender pays the partner per finished job, once both sides agreed"},"proposed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"Terms one side proposed and the other has not accepted yet"},"proposedByYou":{"type":"boolean"}},"required":["agreed","proposed","proposedByYou"],"additionalProperties":false}},"required":["id","role","kind","company","status","visibility","branding","requiredCredentials","terms"],"additionalProperties":false}}},"required":["connections"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/connections/{id}/accept":{"post":{"summary":"Accept a request (partner only) and choose what the sender sees","tags":["CrewConnect"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"visibility":{"type":"string","enum":["company","people"],"default":"company","description":"company: open times and capacity only. people: open times per tech, first names only"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string","enum":["sender","partner"],"description":"Your side: 'sender' sends work, 'partner' does it"},"kind":{"type":"string","enum":["partner","client"],"description":"partner: CrewConnect work sharing. client: the sender is a client company filing service requests with the partner (its provider)"},"company":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"kind":{"type":"string"}},"required":["name","slug","kind"],"additionalProperties":false,"description":"The other side"},"status":{"type":"string","enum":["pending","active","ended"]},"visibility":{"type":"string","enum":["company","people"]},"branding":{"type":"string","enum":["sender","partner"]},"requiredCredentials":{"type":"array","items":{"type":"string"}},"terms":{"type":"object","properties":{"agreed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"What the sender pays the partner per finished job, once both sides agreed"},"proposed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"Terms one side proposed and the other has not accepted yet"},"proposedByYou":{"type":"boolean"}},"required":["agreed","proposed","proposedByYou"],"additionalProperties":false}},"required":["id","role","kind","company","status","visibility","branding","requiredCredentials","terms"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/connections/{id}":{"patch":{"summary":"Change visibility (partner) or branding (sender)","tags":["CrewConnect"],"description":"Each side changes only its own setting: the partner decides what the sender sees; the sender decides whose name the homeowner sees.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"visibility":{"type":"string","enum":["company","people"]},"branding":{"type":"string","enum":["sender","partner"]},"requiredCredentials":{"type":"array","items":{"type":"string","enum":["license","insurance","certification","background_check"]},"maxItems":4,"description":"Sender only: the partner's people need these, valid on the job date, to be offered or sent work"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string","enum":["sender","partner"],"description":"Your side: 'sender' sends work, 'partner' does it"},"kind":{"type":"string","enum":["partner","client"],"description":"partner: CrewConnect work sharing. client: the sender is a client company filing service requests with the partner (its provider)"},"company":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"kind":{"type":"string"}},"required":["name","slug","kind"],"additionalProperties":false,"description":"The other side"},"status":{"type":"string","enum":["pending","active","ended"]},"visibility":{"type":"string","enum":["company","people"]},"branding":{"type":"string","enum":["sender","partner"]},"requiredCredentials":{"type":"array","items":{"type":"string"}},"terms":{"type":"object","properties":{"agreed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"What the sender pays the partner per finished job, once both sides agreed"},"proposed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"Terms one side proposed and the other has not accepted yet"},"proposedByYou":{"type":"boolean"}},"required":["agreed","proposed","proposedByYou"],"additionalProperties":false}},"required":["id","role","kind","company","status","visibility","branding","requiredCredentials","terms"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/connections/{id}/end":{"post":{"summary":"End a connection (either side)","tags":["CrewConnect"],"description":"No new work can be sent. Jobs already sent stay booked with the partner. Ending one that has already ended changes nothing (200, no second webhook). The sender can ask again later (POST /v1/connect/connections).","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string","enum":["sender","partner"],"description":"Your side: 'sender' sends work, 'partner' does it"},"kind":{"type":"string","enum":["partner","client"],"description":"partner: CrewConnect work sharing. client: the sender is a client company filing service requests with the partner (its provider)"},"company":{"type":"object","properties":{"name":{"type":"string"},"slug":{"type":"string"},"kind":{"type":"string"}},"required":["name","slug","kind"],"additionalProperties":false,"description":"The other side"},"status":{"type":"string","enum":["pending","active","ended"]},"visibility":{"type":"string","enum":["company","people"]},"branding":{"type":"string","enum":["sender","partner"]},"requiredCredentials":{"type":"array","items":{"type":"string"}},"terms":{"type":"object","properties":{"agreed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"What the sender pays the partner per finished job, once both sides agreed"},"proposed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"Terms one side proposed and the other has not accepted yet"},"proposedByYou":{"type":"boolean"}},"required":["agreed","proposed","proposedByYou"],"additionalProperties":false}},"required":["id","role","kind","company","status","visibility","branding","requiredCredentials","terms"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/connections/{id}/terms":{"post":{"summary":"Propose pay terms (either side, owner only)","tags":["CrewConnect"],"description":"What the sender pays the partner per finished job: a percent of the job's list price, or a flat amount, due net N days. The other side accepts; until then the old terms (if any) stand. Active connections only (409 not_active); owners only (403).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"agreed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"What the sender pays the partner per finished job, once both sides agreed"},"proposed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"Terms one side proposed and the other has not accepted yet"},"proposedByYou":{"type":"boolean"}},"required":["agreed","proposed","proposedByYou"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/connections/{id}/terms/accept":{"post":{"summary":"Accept the other side's proposed terms (owner only)","tags":["CrewConnect"],"description":"Send back the terms you are accepting, exactly as proposed. 409 terms_changed if they no longer match (the other side changed them meanwhile), nothing_to_accept if there are none from the other side, not_active unless the connection is active.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"agreed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"What the sender pays the partner per finished job, once both sides agreed"},"proposed":{"type":"object","properties":{"kind":{"type":"string","enum":["percent","flat"]},"value":{"type":"integer","minimum":1,"description":"percent of the job price (1-100), or flat cents"},"netDays":{"type":"integer","minimum":0,"maximum":90}},"required":["kind","value","netDays"],"additionalProperties":false,"nullable":true,"description":"Terms one side proposed and the other has not accepted yet"},"proposedByYou":{"type":"boolean"}},"required":["agreed","proposed","proposedByYou"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/connections/{id}/services":{"get":{"summary":"The partner's services (what you can send)","tags":["CrewConnect"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"minutes":{"type":"number"},"peopleNeeded":{"type":"number"},"days":{"type":"number"}},"required":["id","name","minutes","peopleNeeded","days"],"additionalProperties":false}}},"required":["services"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/connections/{id}/availability":{"post":{"summary":"The partner's open times at an address","tags":["CrewConnect"],"description":"Open times only. In 'company' visibility each time carries how many of their people could take it; in 'people' visibility, one entry per person with a first name. Never their other jobs, addresses or drives. Times are in the time zone of the partner's branch serving the address (`timezone`).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"days":{"type":"integer","minimum":1,"maximum":14,"default":7}},"required":["serviceId","lat","lng"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string"},"times":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"},"capacity":{"type":"number"},"tech":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}},"required":["start","local","capacity"],"additionalProperties":false}}},"required":["timezone","times"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/connections/{id}/jobs":{"post":{"summary":"Send a job to the partner","tags":["CrewConnect"],"description":"Booked straight into the partner's schedule under their lock, best tech at that time (or the tech you picked, in 'people' visibility). The homeowner's texts carry the brand set on the connection. A refusal carries a `reason` as well as a message: 409 too_soon (reason too_soon) under an hour ahead; 422 invalid_request (reason visibility) for techId on a 'company'-visibility connection; 409 slot_taken with reason credential, outside_hours or no_capacity when nobody can go at that time. With allowQueue: true, too_soon and slot_taken become 202: the job waits in the partner's intake queue as unscheduled until their office places it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"start":{"type":"string","format":"date-time"},"techId":{"type":"integer","description":"Only with 'people' visibility"},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40},"email":{"type":"string","format":"email"}},"required":["name","phone"],"additionalProperties":false},"address":{"type":"string","maxLength":300},"notes":{"type":"string","maxLength":2000},"allowResubcontract":{"type":"boolean","default":false,"description":"May the partner pass this job on to their own partners?"},"allowQueue":{"type":"boolean","default":false,"description":"true: when the partner can't take it at that time (or it's under an hour ahead), file it in the partner's intake queue (202, status unscheduled) instead of answering 409. The partner's office places it; the homeowner is texted that a time is coming"}},"required":["serviceId","lat","lng","start","customer"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local (the partner's branch serving the address)"},"techs":{"type":"array","items":{"type":"string"},"description":"First names, 'people' visibility only"},"manageToken":{"type":"string"}},"required":["jobId","start","local","timezone","techs","manageToken"],"additionalProperties":false}}}},"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"status":{"type":"string","enum":["unscheduled"]},"queued":{"type":"boolean","enum":[true]},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"description":"The time that was asked for"},"timezone":{"type":"string","description":"IANA zone of requested.local (the zone of the address, or of the techs sent)"},"reason":{"type":"string","description":"Why it couldn't be booked then, in words"},"siteId":{"type":"number","nullable":true,"description":"The site (property, per client company) it is at"}},"required":["jobId","status","queued","requested","timezone","reason"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"reason":{"type":"string","enum":["too_soon","visibility","credential","outside_hours","no_capacity","out_of_area"],"description":"too_soon: less than an hour ahead; visibility: techId on a 'company'-visibility connection; credential: the people who could go don't hold the credentials you require (or the one you picked doesn't); outside_hours: outside the working hours of everyone who could go; no_capacity: nobody is free then any more (another job or the drive is in the way); out_of_area: the address is outside every service area of the partner (or of the tech you picked)"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"reason":{"type":"string","enum":["too_soon","visibility","credential","outside_hours","no_capacity","out_of_area"],"description":"too_soon: less than an hour ahead; visibility: techId on a 'company'-visibility connection; credential: the people who could go don't hold the credentials you require (or the one you picked doesn't); outside_hours: outside the working hours of everyone who could go; no_capacity: nobody is free then any more (another job or the drive is in the way); out_of_area: the address is outside every service area of the partner (or of the tech you picked)"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/received/{jobId}/pass":{"post":{"summary":"Pass a job you received on to your own partner (re-subcontract)","tags":["CrewConnect"],"description":"Only when the original sender allowed it on that job. The job is booked into your partner's schedule (their service, under their lock), your visits are freed, and the homeowner keeps the same brand on their texts. The original sender still sees the job in their sent list. Passed jobs can't be passed again. The same rules as sending a job apply: your connection's required credentials, techId only with 'people' visibility (422), and at least an hour ahead (409 too_soon).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"connectionId":{"type":"string","format":"uuid"},"serviceId":{"type":"integer","description":"Your partner's service"},"start":{"type":"string","format":"date-time","description":"Default: the same time"},"techId":{"type":"integer","description":"Only with 'people' visibility"}},"required":["connectionId","serviceId"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"jobId","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string"}},"required":["jobId","start","local","timezone"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/sent":{"get":{"summary":"Jobs you sent to partners","tags":["CrewConnect"],"description":"Status and time of each job you sent. Tech first names only where the partner shares people.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"partner":{"type":"string"},"service":{"type":"string"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local (where the job is)"},"status":{"type":"string"},"techs":{"type":"array","items":{"type":"string"}}},"required":["jobId","partner","service","start","local","timezone","status","techs"],"additionalProperties":false}}},"required":["jobs"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/sent/{jobId}/cancel":{"post":{"summary":"Take back a job you sent","tags":["CrewConnect"],"description":"Frees the partner's people and texts the homeowner. A job your partner passed on to theirs (status 'passed') can be taken back too: the job that's actually booked is cancelled along with it. 409 too_late once the job is done or its start time has passed.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"jobId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"status":{"type":"string","enum":["cancelled"]}},"required":["jobId","status"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/users":{"post":{"summary":"Add a user (owner only)","tags":["Users"],"description":"Returns the user's token once: `<user id>.<secret>` (never to CrewLink staff acting as the owner: an act-as session gets no token, so the person signs in from the emailed invite). Techs must be linked to a person on the schedule (personId); they can only use /v1/tech. With an `email`, the user is also emailed an invite (see POST /v1/office/users/{id}/invite) unless `invite` is false; `invite` defaults to true when this server sends email (a mail provider is set). `invited` says whether the invite went out: false without an email or a mail provider, past the invite limit, or when the email failed. The user is created either way; invite them later.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"],"description":"owner, field_manager, dispatcher or tech (a client company has no techs)"},"personId":{"type":"integer"},"invite":{"type":"boolean","description":"Email them an invite with a sign-in code. Default: true when the server sends email"},"scope":{"type":"string","enum":["company","team","own"],"description":"Default: the role's (dispatcher: company at a provider, own at a client company; field_manager: team)"},"teamLeadId":{"type":"string","format":"uuid","description":"Dispatchers: put them on this field manager's team"}},"required":["name","role"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"personId":{"type":"number","nullable":true},"active":{"type":"boolean"},"inviteSentAt":{"type":"string","nullable":true,"description":"When the last invite email went out (ISO instant), or null if never"},"scope":{"type":"string","enum":["company","team","own"],"description":"Which jobs they see: company (all), team (their field manager's team), own (what they filed). Owners are always company"},"scopeIsDefault":{"type":"boolean","description":"true when scope is the role's default rather than set for them"},"teamLeadId":{"type":"string","nullable":true,"description":"Dispatchers: the field manager whose team they're on"},"phone":{"type":"string","nullable":true},"token":{"type":"string","description":"Left out for an act-as session (staff never hold people's sign-in tokens)"},"invited":{"type":"boolean"}},"required":["id","name","email","role","personId","active","inviteSentAt","scope","scopeIsDefault","teamLeadId","phone","invited"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"Everyone who can sign in","tags":["Users"],"description":"Field managers and dispatchers with a team or own scope see themselves and their team only.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"users":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"personId":{"type":"number","nullable":true},"active":{"type":"boolean"},"inviteSentAt":{"type":"string","nullable":true,"description":"When the last invite email went out (ISO instant), or null if never"},"scope":{"type":"string","enum":["company","team","own"],"description":"Which jobs they see: company (all), team (their field manager's team), own (what they filed). Owners are always company"},"scopeIsDefault":{"type":"boolean","description":"true when scope is the role's default rather than set for them"},"teamLeadId":{"type":"string","nullable":true,"description":"Dispatchers: the field manager whose team they're on"},"phone":{"type":"string","nullable":true}},"required":["id","name","email","role","personId","active","inviteSentAt","scope","scopeIsDefault","teamLeadId","phone"],"additionalProperties":false}}},"required":["users"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/users/{id}/invite":{"post":{"summary":"Email a user an invite (owner; dispatchers may invite techs, field managers their team)","tags":["Users"],"description":"Emails \"<you> added you to <company> on CrewLink\" with their role, what it lets them do, a link to the web app and a 6-digit sign-in code started for them (an ordinary POST /v1/auth/start code: 10 minutes, 5 tries, and it counts toward 5 codes an hour; past that the email says to ask for a code). The link opens sign-in on the code step for their email, so link plus code signs them in. Call it again to resend (a new code). Returns the user with `inviteSentAt`. 5 invites a minute per company (429 rate_limited with Retry-After). 409 inactive (sign-in turned off), 422 no_email, 502 mail_failed (nothing was sent), 503 no_mailer (no mail provider on this server).","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"personId":{"type":"number","nullable":true},"active":{"type":"boolean"},"inviteSentAt":{"type":"string","nullable":true,"description":"When the last invite email went out (ISO instant), or null if never"},"scope":{"type":"string","enum":["company","team","own"],"description":"Which jobs they see: company (all), team (their field manager's team), own (what they filed). Owners are always company"},"scopeIsDefault":{"type":"boolean","description":"true when scope is the role's default rather than set for them"},"teamLeadId":{"type":"string","nullable":true,"description":"Dispatchers: the field manager whose team they're on"},"phone":{"type":"string","nullable":true}},"required":["id","name","email","role","personId","active","inviteSentAt","scope","scopeIsDefault","teamLeadId","phone"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/team/invite-all":{"post":{"summary":"Invite every tech without a login (owner only)","tags":["Users"],"description":"For every active person on the team with an email (people.email, set on POST/PATCH /v1/office/team) and no sign-in user yet: adds a tech user linked to them and emails them an invite (as POST /v1/office/users/{id}/invite: a link and a sign-in code). Techs who already have a user are left alone (counted in `alreadyHadLogin`); people with no email are counted in `noEmail`. A user made here whose email failed is kept (`failed`; invite them again from Users). Not for platform keys (403). 503 no_mailer when this server sends no email (nothing is created); 429 when run more than twice a minute.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"created":{"type":"number","description":"Tech users added"},"invited":{"type":"number","description":"Invite emails sent"},"alreadyHadLogin":{"type":"number","description":"People who already have a user (invited or not): skipped"},"noEmail":{"type":"number","description":"Active people with no email: add one on the Team page first"},"emailInUse":{"type":"number","description":"Skipped: another user of this company already signs in with that email"},"failed":{"type":"number","description":"Users added whose invite email failed"},"users":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"personId":{"type":"number","nullable":true},"active":{"type":"boolean"},"inviteSentAt":{"type":"string","nullable":true,"description":"When the last invite email went out (ISO instant), or null if never"},"scope":{"type":"string","enum":["company","team","own"],"description":"Which jobs they see: company (all), team (their field manager's team), own (what they filed). Owners are always company"},"scopeIsDefault":{"type":"boolean","description":"true when scope is the role's default rather than set for them"},"teamLeadId":{"type":"string","nullable":true,"description":"Dispatchers: the field manager whose team they're on"},"phone":{"type":"string","nullable":true}},"required":["id","name","email","role","personId","active","inviteSentAt","scope","scopeIsDefault","teamLeadId","phone"],"additionalProperties":false},"description":"The users added"}},"required":["created","invited","alreadyHadLogin","noEmail","emailInUse","failed","users"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/users/{id}":{"patch":{"summary":"Change someone's role, scope or team (owner only)","tags":["Users"],"description":"role: owner, field_manager, dispatcher or tech (a tech must already be linked to a person on the schedule; a client company has no techs). scope: company, team or own, or null for the role's default; owners are always company. teamLeadId: the field manager whose team a dispatcher is on, or null. Turning a field manager into another role takes their dispatchers off the team. Their sign-in keeps working; what they see changes on their next request.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"scope":{"type":"string","enum":["company","team","own"],"nullable":true},"teamLeadId":{"type":"string","format":"uuid","nullable":true},"name":{"type":"string","minLength":1,"maxLength":120},"phone":{"type":"string","maxLength":40,"nullable":true}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"personId":{"type":"number","nullable":true},"active":{"type":"boolean"},"inviteSentAt":{"type":"string","nullable":true,"description":"When the last invite email went out (ISO instant), or null if never"},"scope":{"type":"string","enum":["company","team","own"],"description":"Which jobs they see: company (all), team (their field manager's team), own (what they filed). Owners are always company"},"scopeIsDefault":{"type":"boolean","description":"true when scope is the role's default rather than set for them"},"teamLeadId":{"type":"string","nullable":true,"description":"Dispatchers: the field manager whose team they're on"},"phone":{"type":"string","nullable":true}},"required":["id","name","email","role","personId","active","inviteSentAt","scope","scopeIsDefault","teamLeadId","phone"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/users/{id}/token":{"post":{"summary":"Issue a new token (the old one stops working)","tags":["Users"],"description":"Also signs the user out everywhere: every sign-in session ends and their passkeys are removed. Use it when a phone or a token may have leaked.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"}},"required":["token"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/users/{id}/deactivate":{"post":{"summary":"Turn off sign-in for a user","tags":["Users"],"description":"Their token stops working, every session they have ends and their passkeys are removed.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"personId":{"type":"number","nullable":true},"active":{"type":"boolean"},"inviteSentAt":{"type":"string","nullable":true,"description":"When the last invite email went out (ISO instant), or null if never"},"scope":{"type":"string","enum":["company","team","own"],"description":"Which jobs they see: company (all), team (their field manager's team), own (what they filed). Owners are always company"},"scopeIsDefault":{"type":"boolean","description":"true when scope is the role's default rather than set for them"},"teamLeadId":{"type":"string","nullable":true,"description":"Dispatchers: the field manager whose team they're on"},"phone":{"type":"string","nullable":true}},"required":["id","name","email","role","personId","active","inviteSentAt","scope","scopeIsDefault","teamLeadId","phone"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/users/{id}/reactivate":{"post":{"summary":"Turn sign-in back on for a user (owner only)","tags":["Users"],"description":"Their existing token works again and they can sign in by email. Sessions ended by deactivating stay ended. Issue a new token if the old one may have leaked.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"personId":{"type":"number","nullable":true},"active":{"type":"boolean"},"inviteSentAt":{"type":"string","nullable":true,"description":"When the last invite email went out (ISO instant), or null if never"},"scope":{"type":"string","enum":["company","team","own"],"description":"Which jobs they see: company (all), team (their field manager's team), own (what they filed). Owners are always company"},"scopeIsDefault":{"type":"boolean","description":"true when scope is the role's default rather than set for them"},"teamLeadId":{"type":"string","nullable":true,"description":"Dispatchers: the field manager whose team they're on"},"phone":{"type":"string","nullable":true}},"required":["id","name","email","role","personId","active","inviteSentAt","scope","scopeIsDefault","teamLeadId","phone"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/invitations/{token}":{"get":{"summary":"What an invitation link is for","tags":["Sign-in"],"description":"404 for an unknown link; 410 invitation_used, invitation_revoked or invitation_expired.","parameters":[{"schema":{"type":"string","minLength":16,"maxLength":200},"in":"path","name":"token","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"company":{"type":"string"},"provider":{"type":"string","nullable":true,"description":"For a client company: the company it files requests with"},"invitedBy":{"type":"string","nullable":true},"expiresAt":{"type":"string"}},"required":["name","email","role","company","provider","invitedBy","expiresAt"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/invitations/{token}/accept":{"post":{"summary":"Accept an invitation: creates the user and signs them in","tags":["Sign-in"],"description":"Works once. Creates their sign-in at the company with the invited role, team and contact details and answers with a session, like POST /v1/auth/verify. Then add a passkey (POST /v1/auth/passkeys/options) or sign in later with an emailed code. name may correct the name on the invitation. 409 already_member when that email already signs in there; 404 / 410 as for GET.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120}},"additionalProperties":false,"default":{}}}}},"parameters":[{"schema":{"type":"string","minLength":16,"maxLength":200},"in":"path","name":"token","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"expiresAt":{"type":"string"},"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string"},"personId":{"type":"number","nullable":true}},"required":["id","name","role","personId"],"additionalProperties":false},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"],"additionalProperties":false},"companies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"role":{"type":"string","description":"Their role there: owner, field_manager, dispatcher or tech"},"lastUsedAt":{"type":"string","nullable":true,"description":"When they last signed in there (ISO instant), or null if never"}},"required":["id","name","slug","role","lastUsedAt"],"additionalProperties":false},"description":"Every company this email can sign in at (an active user at each), the one used most recently first. More than one: ask which, and POST /v1/auth/switch to change"}},"required":["token","expiresAt","user","company","companies"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/clients":{"get":{"summary":"Your client companies","tags":["Clients"],"description":"Companies that file service requests into yours: the ones you set up and CrewLink companies that connected to you as a client. With field managers, dispatchers, open requests and job counts.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"clients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The client company id (use it as clientCompanyId)"},"connectionId":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string","description":"client (set up by you) or company (a CrewLink company that connected to you as a client)"},"managed":{"type":"boolean","description":"You set it up: you invite its people, issue its API key and can rename it"},"status":{"type":"string","enum":["active","inactive","pending"],"description":"pending: it asked to connect as a client and waits for you to accept"},"emailDomains":{"type":"array","items":{"type":"string"}},"fieldManagers":{"type":"number"},"dispatchers":{"type":"number"},"openRequests":{"type":"number","description":"Requests filed and not scheduled yet"},"jobs":{"type":"number","description":"Every request and job for this client, any status"},"sites":{"type":"number","description":"Its sites: one per property, however many visits (merged ones left out)"},"activeSites":{"type":"number","description":"Of those, the ones with a visit still to do or needing attention"},"apiKey":{"type":"boolean","description":"Whether its API key has been issued (you can see when, not the key)"},"apiKeyIssuedAt":{"type":"string","nullable":true},"test":{"type":"boolean","description":"true: a test client (PATCH /v1/office/clients/{id} { test }): its jobs only ever go to your test people (people with test: true), and no other job ever goes to them"},"createdAt":{"type":"string"}},"required":["id","connectionId","name","kind","managed","status","emailDomains","fieldManagers","dispatchers","openRequests","jobs","apiKey","apiKeyIssuedAt","test","createdAt"],"additionalProperties":false}}},"required":["clients"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"post":{"summary":"Set up a client company (owner only)","tags":["Clients"],"description":"Creates the client company (no techs, no plan of its own) already connected to yours as a client, active. Invite its people with POST /v1/office/invitations and clientCompanyId (or let emailDomains match them). It sees only the requests it files and your open times. timezone defaults to yours.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"emailDomains":{"type":"array","items":{"type":"string","minLength":3,"maxLength":253,"pattern":"^@?[a-z0-9.-]+\\.[a-z]{2,}$"},"maxItems":10,"default":[]},"timezone":{"type":"string","minLength":1,"maxLength":64}},"required":["name"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The client company id (use it as clientCompanyId)"},"connectionId":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string","description":"client (set up by you) or company (a CrewLink company that connected to you as a client)"},"managed":{"type":"boolean","description":"You set it up: you invite its people, issue its API key and can rename it"},"status":{"type":"string","enum":["active","inactive","pending"],"description":"pending: it asked to connect as a client and waits for you to accept"},"emailDomains":{"type":"array","items":{"type":"string"}},"fieldManagers":{"type":"number"},"dispatchers":{"type":"number"},"openRequests":{"type":"number","description":"Requests filed and not scheduled yet"},"jobs":{"type":"number","description":"Every request and job for this client, any status"},"sites":{"type":"number","description":"Its sites: one per property, however many visits (merged ones left out)"},"activeSites":{"type":"number","description":"Of those, the ones with a visit still to do or needing attention"},"apiKey":{"type":"boolean","description":"Whether its API key has been issued (you can see when, not the key)"},"apiKeyIssuedAt":{"type":"string","nullable":true},"test":{"type":"boolean","description":"true: a test client (PATCH /v1/office/clients/{id} { test }): its jobs only ever go to your test people (people with test: true), and no other job ever goes to them"},"createdAt":{"type":"string"}},"required":["id","connectionId","name","kind","managed","status","emailDomains","fieldManagers","dispatchers","openRequests","jobs","apiKey","apiKeyIssuedAt","test","createdAt"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/clients/{id}":{"get":{"summary":"One client company: its people, latest jobs and pending invitations","tags":["Clients"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The client company id (use it as clientCompanyId)"},"connectionId":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string","description":"client (set up by you) or company (a CrewLink company that connected to you as a client)"},"managed":{"type":"boolean","description":"You set it up: you invite its people, issue its API key and can rename it"},"status":{"type":"string","enum":["active","inactive","pending"],"description":"pending: it asked to connect as a client and waits for you to accept"},"emailDomains":{"type":"array","items":{"type":"string"}},"fieldManagers":{"type":"number"},"dispatchers":{"type":"number"},"openRequests":{"type":"number","description":"Requests filed and not scheduled yet"},"jobs":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"status":{"type":"string","enum":["pending","scheduled","done","cancelled","declined"]},"service":{"type":"string"},"customer":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"local":{"type":"string","nullable":true},"filedBy":{"type":"string","nullable":true},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false,"nullable":true,"description":"The client's rep who booked it through a share link"},"createdAt":{"type":"string"}},"required":["jobId","status","service","customer","address","local","filedBy","bookedBy","createdAt"],"additionalProperties":false},"description":"The latest 50, newest first"},"sites":{"type":"number","description":"Its sites: one per property, however many visits (merged ones left out)"},"activeSites":{"type":"number","description":"Of those, the ones with a visit still to do or needing attention"},"apiKey":{"type":"boolean","description":"Whether its API key has been issued (you can see when, not the key)"},"apiKeyIssuedAt":{"type":"string","nullable":true},"test":{"type":"boolean","description":"true: a test client (PATCH /v1/office/clients/{id} { test }): its jobs only ever go to your test people (people with test: true), and no other job ever goes to them"},"createdAt":{"type":"string"},"people":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"scope":{"type":"string"},"teamLeadId":{"type":"string","nullable":true},"active":{"type":"boolean"}},"required":["id","name","email","role","scope","teamLeadId","active"],"additionalProperties":false},"description":"Empty for a company that connected itself (its people are its own)"},"invitations":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string","nullable":true},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"description":"The company they join"},"client":{"type":"boolean","description":"They join one of your client companies"},"matchedBy":{"type":"string","enum":["email_domain"],"nullable":true,"description":"email_domain: the client company was picked from the email's domain"},"teamLeadId":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","accepted","revoked","expired"]},"invitedBy":{"type":"string","nullable":true},"emailed":{"type":"boolean","description":"The invitation email went out (false: share the link yourself)"},"expiresAt":{"type":"string"},"createdAt":{"type":"string"},"acceptedAt":{"type":"string","nullable":true}},"required":["id","role","name","email","phone","company","client","matchedBy","teamLeadId","status","invitedBy","emailed","expiresAt","createdAt","acceptedAt"],"additionalProperties":false}}},"required":["id","connectionId","name","kind","managed","status","emailDomains","fieldManagers","dispatchers","openRequests","jobs","apiKey","apiKeyIssuedAt","test","createdAt","people","invitations"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"patch":{"summary":"Rename, set email domains, mark as a test client, or turn a client on or off (owner only)","tags":["Clients"],"description":"status inactive ends the client connection: its people can still sign in and see the requests they filed, but can't file new ones or see your open times; active turns it back on (a client that asked to connect is accepted). name only for clients you set up. emailDomains replaces the list. test: true marks a test client (companies of kind client only, 422 otherwise): its jobs are only ever offered, booked or moved to your test people (PATCH /v1/office/team/{id} { test }), in availability, first-available, holds, office booking and moves, the engine's own picks and CrewConnect; jobs already booked stay put.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"emailDomains":{"type":"array","items":{"type":"string","minLength":3,"maxLength":253,"pattern":"^@?[a-z0-9.-]+\\.[a-z]{2,}$"},"maxItems":10},"status":{"type":"string","enum":["active","inactive"]},"test":{"type":"boolean","description":"true: a test client (its jobs go to test people only); false: a real one again"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The client company id (use it as clientCompanyId)"},"connectionId":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string","description":"client (set up by you) or company (a CrewLink company that connected to you as a client)"},"managed":{"type":"boolean","description":"You set it up: you invite its people, issue its API key and can rename it"},"status":{"type":"string","enum":["active","inactive","pending"],"description":"pending: it asked to connect as a client and waits for you to accept"},"emailDomains":{"type":"array","items":{"type":"string"}},"fieldManagers":{"type":"number"},"dispatchers":{"type":"number"},"openRequests":{"type":"number","description":"Requests filed and not scheduled yet"},"jobs":{"type":"number","description":"Every request and job for this client, any status"},"sites":{"type":"number","description":"Its sites: one per property, however many visits (merged ones left out)"},"activeSites":{"type":"number","description":"Of those, the ones with a visit still to do or needing attention"},"apiKey":{"type":"boolean","description":"Whether its API key has been issued (you can see when, not the key)"},"apiKeyIssuedAt":{"type":"string","nullable":true},"test":{"type":"boolean","description":"true: a test client (PATCH /v1/office/clients/{id} { test }): its jobs only ever go to your test people (people with test: true), and no other job ever goes to them"},"createdAt":{"type":"string"}},"required":["id","connectionId","name","kind","managed","status","emailDomains","fieldManagers","dispatchers","openRequests","jobs","apiKey","apiKeyIssuedAt","test","createdAt"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/clients/{id}/api-key":{"post":{"summary":"Issue a client company's API key (owner only, clients you set up)","tags":["Clients"],"description":"Returns the key once (`<client company id>.<secret>`); the previous one stops working. With it, the client's systems file requests (POST /v1/connect/connections/{connectionId}/requests, source api) and read them (GET /v1/connect/requests). It acts as the client's owner: it can't see your schedule, only the client's own requests.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"},"issuedAt":{"type":"string"}},"required":["key","issuedAt"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/invitations":{"post":{"summary":"Invite someone with a one-time link","tags":["Users"],"description":"Returns `link` (`/join/<token>`, works once, for 7 days): send it any way you like. It is also emailed when this server sends email (`emailed`; `send: false` to skip). Opening it sets the person up in one step (they add a passkey, or sign in later by email code). Owners invite any role into their company, or owner/field_manager/dispatcher into a client company they set up (`clientCompanyId`); without clientCompanyId an email whose domain is on one of your client companies goes into that client (`matchedBy: email_domain`). Field managers invite dispatchers to their own team; dispatchers invite techs (with `personId`). `teamOf` puts a dispatcher on a field manager's team. A newer invitation for the same email and company replaces a pending one. 409 already_member when they already sign in there.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","format":"email","maxLength":320},"phone":{"type":"string","minLength":7,"maxLength":40},"address":{"type":"string","maxLength":300},"clientCompanyId":{"type":"string","format":"uuid","description":"Invite them into this client company (one you set up)"},"teamOf":{"type":"string","format":"uuid","description":"Dispatchers: the field manager whose team they join (a field manager's own invites default to their team)"},"personId":{"type":"integer","description":"Techs: who they are on the schedule"},"send":{"type":"boolean","description":"Email the link (default: when this server sends email)"}},"required":["role","name","email"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string","nullable":true},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"description":"The company they join"},"client":{"type":"boolean","description":"They join one of your client companies"},"matchedBy":{"type":"string","enum":["email_domain"],"nullable":true,"description":"email_domain: the client company was picked from the email's domain"},"teamLeadId":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","accepted","revoked","expired"]},"invitedBy":{"type":"string","nullable":true},"emailed":{"type":"boolean","description":"The invitation email went out (false: share the link yourself)"},"expiresAt":{"type":"string"},"createdAt":{"type":"string"},"acceptedAt":{"type":"string","nullable":true},"link":{"type":"string","description":"Shown now and on resend only"}},"required":["id","role","name","email","phone","company","client","matchedBy","teamLeadId","status","invitedBy","emailed","expiresAt","createdAt","acceptedAt","link"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"Invitations: pending ones first","tags":["Users"],"description":"Invitations you made (into your company or your client companies) and ones made into your company. Field managers and dispatchers with a team or own scope see the ones they made. status=all includes accepted, revoked and expired ones.","parameters":[{"schema":{"type":"string","enum":["pending","all"],"default":"pending"},"in":"query","name":"status","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"invitations":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string","nullable":true},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"description":"The company they join"},"client":{"type":"boolean","description":"They join one of your client companies"},"matchedBy":{"type":"string","enum":["email_domain"],"nullable":true,"description":"email_domain: the client company was picked from the email's domain"},"teamLeadId":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","accepted","revoked","expired"]},"invitedBy":{"type":"string","nullable":true},"emailed":{"type":"boolean","description":"The invitation email went out (false: share the link yourself)"},"expiresAt":{"type":"string"},"createdAt":{"type":"string"},"acceptedAt":{"type":"string","nullable":true}},"required":["id","role","name","email","phone","company","client","matchedBy","teamLeadId","status","invitedBy","emailed","expiresAt","createdAt","acceptedAt"],"additionalProperties":false}}},"required":["invitations"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/invitations/{id}/revoke":{"post":{"summary":"Revoke a pending invitation (its link stops working)","tags":["Users"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string","nullable":true},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"description":"The company they join"},"client":{"type":"boolean","description":"They join one of your client companies"},"matchedBy":{"type":"string","enum":["email_domain"],"nullable":true,"description":"email_domain: the client company was picked from the email's domain"},"teamLeadId":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","accepted","revoked","expired"]},"invitedBy":{"type":"string","nullable":true},"emailed":{"type":"boolean","description":"The invitation email went out (false: share the link yourself)"},"expiresAt":{"type":"string"},"createdAt":{"type":"string"},"acceptedAt":{"type":"string","nullable":true}},"required":["id","role","name","email","phone","company","client","matchedBy","teamLeadId","status","invitedBy","emailed","expiresAt","createdAt","acceptedAt"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/invitations/{id}/resend":{"post":{"summary":"Make a fresh link (and email it again)","tags":["Users"],"description":"The old link stops working; the new one works for 7 days from now. Works on pending, expired and revoked invitations, not accepted ones.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"name":{"type":"string"},"email":{"type":"string"},"phone":{"type":"string","nullable":true},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"description":"The company they join"},"client":{"type":"boolean","description":"They join one of your client companies"},"matchedBy":{"type":"string","enum":["email_domain"],"nullable":true,"description":"email_domain: the client company was picked from the email's domain"},"teamLeadId":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","accepted","revoked","expired"]},"invitedBy":{"type":"string","nullable":true},"emailed":{"type":"boolean","description":"The invitation email went out (false: share the link yourself)"},"expiresAt":{"type":"string"},"createdAt":{"type":"string"},"acceptedAt":{"type":"string","nullable":true},"link":{"type":"string"}},"required":["id","role","name","email","phone","company","client","matchedBy","teamLeadId","status","invitedBy","emailed","expiresAt","createdAt","acceptedAt","link"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/connections/{id}/requests":{"post":{"summary":"File a service request with your provider (client connections)","tags":["Clients"],"description":"For a client company (or a company connected as a client). Without `start`, the request waits for the provider to schedule it (status pending), with up to 3 days that suit the customer in `requested`. With `start` (one of the provider's open times from POST /v1/connect/connections/{id}/availability, at least an hour ahead) it's booked straight away when someone can go (status scheduled, techs by first name); when nobody can, it's still filed as pending for the provider to place, with `reason` saying why (too_soon, credential, outside_hours or no_capacity). Never refused for lack of a time. The provider gets the request.created webhook (or booking.created when booked). 404 when the connection isn't an active client connection of yours.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"address":{"type":"string","maxLength":300},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40},"email":{"type":"string","format":"email"}},"required":["name","phone"],"additionalProperties":false},"notes":{"type":"string","maxLength":2000},"start":{"type":"string","format":"date-time","description":"Book this time if someone can go"},"requested":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"part":{"type":"string","enum":["morning","afternoon","any"],"default":"any"}},"required":["date"],"additionalProperties":false},"maxItems":3,"description":"Days that suit the customer, best first (pending requests)"}},"required":["serviceId","lat","lng","customer"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"number"},"status":{"type":"string","enum":["pending","scheduled"]},"start":{"type":"string","nullable":true},"local":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of `local` and of the requested days"},"techs":{"type":"array","items":{"type":"string"}},"reason":{"type":"string","enum":["too_soon","credential","outside_hours","no_capacity"],"nullable":true,"description":"Why a requested start was not booked"}},"required":["requestId","status","start","local","timezone","techs","reason"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/requests":{"get":{"summary":"Requests your company filed, newest first","tags":["Clients"],"description":"Every request your company filed with its providers, with status (pending, scheduled, done, cancelled, declined), the visit time once scheduled, the tech's first name and who filed it. Owners see all of them; field managers their team's; dispatchers at a client company their own (scope). Filter by status (one or more, comma-separated) and by the date filed (from, to: local dates, inclusive). 25 a page by default, up to 100.","parameters":[{"schema":{"type":"string","pattern":"^(pending|scheduled|done|cancelled|declined)(,(pending|scheduled|done|cancelled|declined))*$"},"in":"query","name":"status","required":false,"description":"One or more statuses, comma-separated, e.g. scheduled,done"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false},{"schema":{"type":"integer","minimum":1,"default":1},"in":"query","name":"page","required":false},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"in":"query","name":"limit","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"requests":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"number","description":"The job id on the provider"},"provider":{"type":"string"},"service":{"type":"string"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true},"address":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","scheduled","done","cancelled","declined"],"description":"pending: filed, no visit yet (an out-of-area request, in the provider's intake queue, or moved off the board; start and local are null); scheduled: a visit is on the board; done; cancelled; declined (the provider turned it down)"},"start":{"type":"string","nullable":true,"description":"The visit (UTC), once scheduled"},"local":{"type":"string","nullable":true,"description":"The visit on the wall clock where the work is (`timezone`)"},"timezone":{"type":"string","description":"IANA zone of `local`: the provider's branch serving the address"},"techs":{"type":"array","items":{"type":"string"},"description":"Always empty: a client never sees who does the work (no tech or staff names). Kept so older apps keep working"},"requested":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false}},"filedBy":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"}},"required":["userId","name"],"additionalProperties":false,"nullable":true,"description":"Who filed it: a person of your company (with their user id), or the provider's company name (userId null) when the provider's office filed it for you"},"source":{"type":"string","nullable":true},"createdAt":{"type":"string"}},"required":["requestId","provider","service","customer","address","notes","status","start","local","timezone","techs","requested","filedBy","source","createdAt"],"additionalProperties":false}},"page":{"type":"number"},"limit":{"type":"number"},"total":{"type":"number"},"totalPages":{"type":"number"}},"required":["requests","page","limit","total","totalPages"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/requests/{jobId}":{"get":{"summary":"One request your company filed","tags":["Clients"],"parameters":[{"schema":{"type":"integer"},"in":"path","name":"jobId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"number","description":"The job id on the provider"},"provider":{"type":"string"},"service":{"type":"string"},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string"}},"required":["name","phone"],"additionalProperties":false,"nullable":true},"address":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true},"status":{"type":"string","enum":["pending","scheduled","done","cancelled","declined"],"description":"pending: filed, no visit yet (an out-of-area request, in the provider's intake queue, or moved off the board; start and local are null); scheduled: a visit is on the board; done; cancelled; declined (the provider turned it down)"},"start":{"type":"string","nullable":true,"description":"The visit (UTC), once scheduled"},"local":{"type":"string","nullable":true,"description":"The visit on the wall clock where the work is (`timezone`)"},"timezone":{"type":"string","description":"IANA zone of `local`: the provider's branch serving the address"},"techs":{"type":"array","items":{"type":"string"},"description":"Always empty: a client never sees who does the work (no tech or staff names). Kept so older apps keep working"},"requested":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false}},"filedBy":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"}},"required":["userId","name"],"additionalProperties":false,"nullable":true,"description":"Who filed it: a person of your company (with their user id), or the provider's company name (userId null) when the provider's office filed it for you"},"source":{"type":"string","nullable":true},"createdAt":{"type":"string"}},"required":["requestId","provider","service","customer","address","notes","status","start","local","timezone","techs","requested","filedBy","source","createdAt"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/connect/requests/{jobId}/cancel":{"post":{"summary":"Take back a request your company filed","tags":["Clients"],"description":"A pending request is withdrawn; a scheduled one is cancelled on the provider's schedule and the homeowner is texted (unless the job's homeowner texts are off). A scheduled one can be taken back up to the provider's cancelCutoffHours before its start (default 24, the same cutoff as the homeowner's manage link); after that, 409 too_late (call the provider, who has no cutoff). 409 too_late too once it's done or its start has passed; 409 not_open when it's already cancelled or declined.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"jobId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"number"},"status":{"type":"string","enum":["cancelled"]}},"required":["requestId","status"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/share/{token}":{"get":{"summary":"A share link: the client, its services and the rep","tags":["Share links"],"description":"What the page at /r/{token} shows: the client company's name (never the provider's people or CrewLink), the services it offers, and the rep for a per-rep link (locked). 404 for a token that isn't (or no longer is, after a rotation) a link; 410 link_revoked, link_expired, link_paused or link_inactive (the client is off) with a message to show.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":64},"in":"path","name":"token","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"client":{"type":"object","properties":{"name":{"type":"string"},"logoUrl":{"type":"string","nullable":true,"description":"Not set yet: always null"}},"required":["name","logoUrl"],"additionalProperties":false},"title":{"type":"string","description":"\"Book a site survey\""},"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"minutes":{"type":"number"},"days":{"type":"number"},"daylightOnly":{"type":"boolean","description":"true: only times when the whole visit is in daylight at the address are offered and held (409 outside_daylight otherwise)"}},"required":["id","name","minutes","days","daylightOnly"],"additionalProperties":false}},"rep":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"}},"required":["name","email"],"additionalProperties":false,"nullable":true,"description":"A per-rep link: filled in and locked"},"timezone":{"type":"string"},"units":{"type":"string","enum":["mi","km"]},"holdMinutes":{"type":"number"},"dailyCapReached":{"type":"boolean"},"turnstileSiteKey":{"type":"string","nullable":true,"description":"When set, show Cloudflare Turnstile and send its token as turnstileToken"}},"required":["client","title","services","rep","timezone","units","holdMinutes","dailyCapReached","turnstileSiteKey"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/share/{token}/challenge":{"post":{"summary":"A booking check for this link (proof of work and timing)","tags":["Share links"],"description":"Signed, bound to the link, single-use. Find a nonce (a whole number) so that sha256(\"<challenge>:<nonce>\") in hex starts with `difficulty` zero bits (about 16,000 tries: well under a second in a browser), then send { challenge, nonce } as `proof` with the booking or request, no sooner than minSeconds after this answer and before expiresAt.","parameters":[{"schema":{"type":"string","minLength":1,"maxLength":64},"in":"path","name":"token","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"challenge":{"type":"string"},"difficulty":{"type":"number"},"minSeconds":{"type":"number"},"expiresAt":{"type":"string"}},"required":["challenge","difficulty","minSeconds","expiresAt"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/share/{token}/availability":{"post":{"summary":"Open times at an address, for this link's client","tags":["Share links"],"description":"The engine's open times for the provider, as for its own office with clientCompanyId set to the link's client: its service areas, the client's test flag (a test client: test people only), drive time at that hour, nothing sooner than an hour from now; a daylight-only service only at times when the whole visit is between sunrise and sunset at the address. stepMinutes as on POST /v1/office/availability (default 15). outsideArea, or no slots: offer POST .../requests (request a visit).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"from":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","description":"First local date (default today)"},"days":{"type":"integer","minimum":1,"maximum":28,"default":14},"stepMinutes":{"anyOf":[{"type":"number","enum":[1]},{"type":"number","enum":[5]},{"type":"number","enum":[15]},{"type":"number","enum":[30]},{"type":"number","enum":[60]}],"default":15,"description":"Start times on this grid of local minutes: 60 = on the hour only, 15 = :00/:15/:30/:45 (the default), 1 = any minute. Every one is still travel- and job-feasible. One of 1, 5, 15, 30, 60 (422 otherwise)"}},"required":["serviceId","lat","lng"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":64},"in":"path","name":"token","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string"},"slots":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false}},"outsideArea":{"type":"boolean"},"nearestKm":{"type":"number","nullable":true}},"required":["timezone","slots","outsideArea","nearestKm"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/share/{token}/holds":{"post":{"summary":"Hold a start time while the rep finishes","tags":["Share links"],"description":"Rechecked under a lock against the live schedule, for the link's client (test flag included), and held for 10 minutes. 409 slot_taken, too_soon or outside_daylight (a daylight-only service: the whole visit must be between sunrise and sunset at the address; the message gives that day's times); 429 too_many_holds (5 live holds per link, 3 per IP), daily_cap (the link's bookings and requests today reached its dailyCap) or rate_limited.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"start":{"type":"string","format":"date-time"}},"required":["serviceId","lat","lng","start"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":64},"in":"path","name":"token","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"holdId":{"type":"number"},"holdSecret":{"type":"string"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string"},"expiresAt":{"type":"string"}},"required":["holdId","holdSecret","start","local","timezone","expiresAt"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/share/{token}/bookings":{"post":{"summary":"Book a held time: a job for the link's client, booked by the rep","tags":["Share links"],"description":"Needs the hold's secret, the homeowner, the address (geoPrecision as on the booking page: street or locality is 422 approximate_address unless allowApproximate), the rep (name and email; a per-rep link's own are used whatever is sent) and `proof` (POST .../challenge). The job is the provider's with clientCompanyId the client, source share_link, shareLinkId and bookedBy; booking.created carries them. The homeowner gets the usual confirmation only when the existing gates allow (the company's and the link's platform grant's homeowner messaging, lib/hometexts.ts); the rep and the client's owners get an email in the client's name. Refusals: 400 rejected (the hidden field was filled), 403 proof_invalid, proof_too_fast, proof_expired, proof_used or check_failed (Turnstile), 422 invalid_phone, rep_required or approximate_address, 429 daily_cap or rate_limited, 404 no such hold, 410 hold_expired, 409 slot_taken or outside_daylight (the held time is checked again for a daylight-only service).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"holdId":{"type":"integer"},"holdSecret":{"type":"string","minLength":1,"maxLength":200},"homeowner":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40},"email":{"type":"string","format":"email","maxLength":200}},"required":["name","phone"],"additionalProperties":false},"address":{"type":"string","minLength":3,"maxLength":300},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"]},"allowApproximate":{"type":"boolean","default":false},"notes":{"type":"string","maxLength":2000},"rep":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","format":"email","maxLength":200}},"required":["name","email"],"additionalProperties":false,"description":"Required unless it's a per-rep link"},"website":{"type":"string","maxLength":200,"description":"Leave empty: a field the page hides from people (anything in it refuses the booking, 400 rejected)"},"proof":{"type":"object","properties":{"challenge":{"type":"string","minLength":10,"maxLength":300},"nonce":{"type":"string","minLength":1,"maxLength":20}},"required":["challenge","nonce"],"additionalProperties":false,"description":"From POST /v1/public/share/{token}/challenge: the challenge and a nonce whose sha256(\"<challenge>:<nonce>\") starts with `difficulty` zero bits, sent no sooner than minSeconds after it was issued"},"turnstileToken":{"type":"string","maxLength":4096,"description":"When the link answered a turnstileSiteKey: the widget's token"}},"required":["holdId","holdSecret","homeowner","address"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":64},"in":"path","name":"token","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"bookingId":{"type":"number"},"status":{"type":"string","enum":["booked"]},"service":{"type":"string"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string"},"address":{"type":"string"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false},"emailed":{"type":"object","properties":{"rep":{"type":"boolean","description":"The rep got a confirmation email"},"clientAdmins":{"type":"number","description":"The client's owners emailed"}},"required":["rep","clientAdmins"],"additionalProperties":false}},"required":["bookingId","status","service","start","local","timezone","address","bookedBy","emailed"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/share/{token}/requests":{"post":{"summary":"No time fits: request a visit for the link's client","tags":["Share links"],"description":"Files a request (status waiting) with the days that suit, for the provider's office to place (GET /v1/office/requests), attributed like a booking: clientCompanyId, source share_link, shareLinkId, bookedBy; request.created carries them. The same checks as a booking (hidden field, proof, phone, rep, address precision, daily cap). The homeowner gets the request-received text only when the gates allow; the rep and the client's owners get an email.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"address":{"type":"string","minLength":3,"maxLength":300},"geoPrecision":{"type":"string","enum":["rooftop","address","street","locality"]},"allowApproximate":{"type":"boolean","default":false},"homeowner":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40},"email":{"type":"string","format":"email","maxLength":200}},"required":["name","phone"],"additionalProperties":false},"notes":{"type":"string","maxLength":2000},"requested":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"part":{"type":"string","enum":["morning","afternoon","any"],"default":"any"}},"required":["date"],"additionalProperties":false},"minItems":1,"maxItems":3,"description":"Days that suit, best first"},"rep":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","format":"email","maxLength":200}},"required":["name","email"],"additionalProperties":false},"website":{"type":"string","maxLength":200,"description":"Leave empty: a field the page hides from people (anything in it refuses the booking, 400 rejected)"},"proof":{"type":"object","properties":{"challenge":{"type":"string","minLength":10,"maxLength":300},"nonce":{"type":"string","minLength":1,"maxLength":20}},"required":["challenge","nonce"],"additionalProperties":false,"description":"From POST /v1/public/share/{token}/challenge: the challenge and a nonce whose sha256(\"<challenge>:<nonce>\") starts with `difficulty` zero bits, sent no sooner than minSeconds after it was issued"},"turnstileToken":{"type":"string","maxLength":4096,"description":"When the link answered a turnstileSiteKey: the widget's token"}},"required":["serviceId","lat","lng","address","homeowner","requested"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":64},"in":"path","name":"token","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"number"},"status":{"type":"string","enum":["waiting"]},"timezone":{"type":"string"},"bookedBy":{"type":"object","properties":{"repName":{"type":"string"},"repEmail":{"type":"string"}},"required":["repName","repEmail"],"additionalProperties":false},"emailed":{"type":"object","properties":{"rep":{"type":"boolean","description":"The rep got a confirmation email"},"clientAdmins":{"type":"number","description":"The client's owners emailed"}},"required":["rep","clientAdmins"],"additionalProperties":false}},"required":["requestId","status","timezone","bookedBy","emailed"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"410":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/clients/{clientId}/share-links":{"get":{"summary":"A client's booking links (owner or dispatcher)","tags":["Share links"],"description":"Every link made for this client company, newest first, revoked ones included (url null), with usage counts. Platform keys need read.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"clientId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"links":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"clientCompanyId":{"type":"string"},"url":{"type":"string","nullable":true,"description":"The link to share: https://<your share host>/r/<token> when your company (or the platform) has one (shareLinkHost), else https://go.crewlink.ai/r/<token>. null once revoked"},"fallbackUrl":{"type":"string","nullable":true,"description":"The same link on go.crewlink.ai (always works). null once revoked"},"label":{"type":"string","nullable":true},"kind":{"type":"string","enum":["client","rep"],"description":"client: the client's one general link (the rep types their name and email); rep: a per-rep link (name and email filled in and locked)"},"rep":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"}},"required":["name","email"],"additionalProperties":false,"nullable":true},"serviceIds":{"type":"array","items":{"type":"number"},"description":"The services it offers; empty = your residential services (all active services when none is residential)"},"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"What the page offers now"},"expiresAt":{"type":"string","nullable":true},"dailyCap":{"type":"number","description":"Bookings and requests a day (your local day), at most"},"active":{"type":"boolean"},"status":{"type":"string","enum":["active","paused","expired","revoked"]},"usage":{"type":"object","properties":{"bookings":{"type":"number"},"requests":{"type":"number","description":"\"Request a visit\" when no time fit"},"today":{"type":"number","description":"Both, today (your local day): what dailyCap counts"},"total":{"type":"number"}},"required":["bookings","requests","today","total"],"additionalProperties":false},"createdVia":{"type":"string","enum":["office","platform","api_key"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"rotatedAt":{"type":"string","nullable":true},"revokedAt":{"type":"string","nullable":true},"lastUsedAt":{"type":"string","nullable":true}},"required":["id","clientCompanyId","url","fallbackUrl","label","kind","rep","serviceIds","services","expiresAt","dailyCap","active","status","usage","createdVia","createdAt","updatedAt","rotatedAt","revokedAt","lastUsedAt"],"additionalProperties":false}}},"required":["links"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"post":{"summary":"Make a booking link for a client (owner or dispatcher)","tags":["Share links"],"description":"Returns the link with its url (`<base>/r/<token>`, 192 random bits). Without `rep` it is the client's general link: one per client (409 link_exists with linkId while another isn't revoked); the rep types their name and email on the page. With `rep` it is that rep's own link (name and email filled in and locked; one per rep email, 409 link_exists). serviceIds: what it offers (your active services; empty = your residential ones). dailyCap (default 25): bookings and requests a day, your local day. expiresAt: it stops working then. The client must be active (409 client_inactive). Audited. Platform keys need write; the link then books like your platform (its grant's homeowner messaging applies to its jobs).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":120},"rep":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","format":"email","maxLength":200}},"required":["name","email"],"additionalProperties":false,"description":"A per-rep link: the rep's name and email, filled in and locked on the page"},"serviceIds":{"type":"array","items":{"type":"integer"},"maxItems":30},"expiresAt":{"type":"string","format":"date-time","nullable":true},"dailyCap":{"type":"integer","minimum":1,"maximum":1000,"default":25},"active":{"type":"boolean","default":true}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"clientId","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"clientCompanyId":{"type":"string"},"url":{"type":"string","nullable":true,"description":"The link to share: https://<your share host>/r/<token> when your company (or the platform) has one (shareLinkHost), else https://go.crewlink.ai/r/<token>. null once revoked"},"fallbackUrl":{"type":"string","nullable":true,"description":"The same link on go.crewlink.ai (always works). null once revoked"},"label":{"type":"string","nullable":true},"kind":{"type":"string","enum":["client","rep"],"description":"client: the client's one general link (the rep types their name and email); rep: a per-rep link (name and email filled in and locked)"},"rep":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"}},"required":["name","email"],"additionalProperties":false,"nullable":true},"serviceIds":{"type":"array","items":{"type":"number"},"description":"The services it offers; empty = your residential services (all active services when none is residential)"},"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"What the page offers now"},"expiresAt":{"type":"string","nullable":true},"dailyCap":{"type":"number","description":"Bookings and requests a day (your local day), at most"},"active":{"type":"boolean"},"status":{"type":"string","enum":["active","paused","expired","revoked"]},"usage":{"type":"object","properties":{"bookings":{"type":"number"},"requests":{"type":"number","description":"\"Request a visit\" when no time fit"},"today":{"type":"number","description":"Both, today (your local day): what dailyCap counts"},"total":{"type":"number"}},"required":["bookings","requests","today","total"],"additionalProperties":false},"createdVia":{"type":"string","enum":["office","platform","api_key"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"rotatedAt":{"type":"string","nullable":true},"revokedAt":{"type":"string","nullable":true},"lastUsedAt":{"type":"string","nullable":true}},"required":["id","clientCompanyId","url","fallbackUrl","label","kind","rep","serviceIds","services","expiresAt","dailyCap","active","status","usage","createdVia","createdAt","updatedAt","rotatedAt","revokedAt","lastUsedAt"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"linkId":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/clients/{clientId}/share-links/{linkId}":{"get":{"summary":"One booking link, with its audit trail","tags":["Share links"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"clientId","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"linkId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"clientCompanyId":{"type":"string"},"url":{"type":"string","nullable":true,"description":"The link to share: https://<your share host>/r/<token> when your company (or the platform) has one (shareLinkHost), else https://go.crewlink.ai/r/<token>. null once revoked"},"fallbackUrl":{"type":"string","nullable":true,"description":"The same link on go.crewlink.ai (always works). null once revoked"},"label":{"type":"string","nullable":true},"kind":{"type":"string","enum":["client","rep"],"description":"client: the client's one general link (the rep types their name and email); rep: a per-rep link (name and email filled in and locked)"},"rep":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"}},"required":["name","email"],"additionalProperties":false,"nullable":true},"serviceIds":{"type":"array","items":{"type":"number"},"description":"The services it offers; empty = your residential services (all active services when none is residential)"},"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"What the page offers now"},"expiresAt":{"type":"string","nullable":true},"dailyCap":{"type":"number","description":"Bookings and requests a day (your local day), at most"},"active":{"type":"boolean"},"status":{"type":"string","enum":["active","paused","expired","revoked"]},"usage":{"type":"object","properties":{"bookings":{"type":"number"},"requests":{"type":"number","description":"\"Request a visit\" when no time fit"},"today":{"type":"number","description":"Both, today (your local day): what dailyCap counts"},"total":{"type":"number"}},"required":["bookings","requests","today","total"],"additionalProperties":false},"createdVia":{"type":"string","enum":["office","platform","api_key"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"rotatedAt":{"type":"string","nullable":true},"revokedAt":{"type":"string","nullable":true},"lastUsedAt":{"type":"string","nullable":true},"history":{"type":"array","items":{"type":"object","properties":{"action":{"type":"string","enum":["created","updated","rotated","revoked"]},"at":{"type":"string"},"by":{"type":"string","description":"office, platform:<slug>, api_key"},"changes":{"type":"array","items":{"type":"string"}}},"required":["action","at","by","changes"],"additionalProperties":false},"description":"The audit trail, oldest first"}},"required":["id","clientCompanyId","url","fallbackUrl","label","kind","rep","serviceIds","services","expiresAt","dailyCap","active","status","usage","createdVia","createdAt","updatedAt","rotatedAt","revokedAt","lastUsedAt","history"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"patch":{"summary":"Change a booking link: label, rep, services, cap, expiry, on or off","tags":["Share links"],"description":"Only what you send changes. rep null turns a per-rep link into the client's general link (409 link_exists if it has one); expiresAt null: never expires; active false pauses it (the page says so) and true turns it back on. The URL stays the same (POST .../rotate for a new one). 409 link_revoked for a revoked link. Audited. Platform keys need write.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","maxLength":120,"nullable":true},"rep":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","format":"email","maxLength":200}},"required":["name","email"],"additionalProperties":false,"nullable":true},"serviceIds":{"type":"array","items":{"type":"integer"},"maxItems":30},"expiresAt":{"type":"string","format":"date-time","nullable":true},"dailyCap":{"type":"integer","minimum":1,"maximum":1000},"active":{"type":"boolean"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"clientId","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"linkId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"clientCompanyId":{"type":"string"},"url":{"type":"string","nullable":true,"description":"The link to share: https://<your share host>/r/<token> when your company (or the platform) has one (shareLinkHost), else https://go.crewlink.ai/r/<token>. null once revoked"},"fallbackUrl":{"type":"string","nullable":true,"description":"The same link on go.crewlink.ai (always works). null once revoked"},"label":{"type":"string","nullable":true},"kind":{"type":"string","enum":["client","rep"],"description":"client: the client's one general link (the rep types their name and email); rep: a per-rep link (name and email filled in and locked)"},"rep":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"}},"required":["name","email"],"additionalProperties":false,"nullable":true},"serviceIds":{"type":"array","items":{"type":"number"},"description":"The services it offers; empty = your residential services (all active services when none is residential)"},"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"What the page offers now"},"expiresAt":{"type":"string","nullable":true},"dailyCap":{"type":"number","description":"Bookings and requests a day (your local day), at most"},"active":{"type":"boolean"},"status":{"type":"string","enum":["active","paused","expired","revoked"]},"usage":{"type":"object","properties":{"bookings":{"type":"number"},"requests":{"type":"number","description":"\"Request a visit\" when no time fit"},"today":{"type":"number","description":"Both, today (your local day): what dailyCap counts"},"total":{"type":"number"}},"required":["bookings","requests","today","total"],"additionalProperties":false},"createdVia":{"type":"string","enum":["office","platform","api_key"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"rotatedAt":{"type":"string","nullable":true},"revokedAt":{"type":"string","nullable":true},"lastUsedAt":{"type":"string","nullable":true}},"required":["id","clientCompanyId","url","fallbackUrl","label","kind","rep","serviceIds","services","expiresAt","dailyCap","active","status","usage","createdVia","createdAt","updatedAt","rotatedAt","revokedAt","lastUsedAt"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"linkId":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"delete":{"summary":"Revoke a booking link","tags":["Share links"],"description":"The URL stops working at once (the page says it was turned off). The link is kept, with its bookings and history; it can't be turned back on (make a new one). Revoking again changes nothing. Audited. Platform keys need write.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"clientId","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"linkId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"clientCompanyId":{"type":"string"},"url":{"type":"string","nullable":true,"description":"The link to share: https://<your share host>/r/<token> when your company (or the platform) has one (shareLinkHost), else https://go.crewlink.ai/r/<token>. null once revoked"},"fallbackUrl":{"type":"string","nullable":true,"description":"The same link on go.crewlink.ai (always works). null once revoked"},"label":{"type":"string","nullable":true},"kind":{"type":"string","enum":["client","rep"],"description":"client: the client's one general link (the rep types their name and email); rep: a per-rep link (name and email filled in and locked)"},"rep":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"}},"required":["name","email"],"additionalProperties":false,"nullable":true},"serviceIds":{"type":"array","items":{"type":"number"},"description":"The services it offers; empty = your residential services (all active services when none is residential)"},"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"What the page offers now"},"expiresAt":{"type":"string","nullable":true},"dailyCap":{"type":"number","description":"Bookings and requests a day (your local day), at most"},"active":{"type":"boolean"},"status":{"type":"string","enum":["active","paused","expired","revoked"]},"usage":{"type":"object","properties":{"bookings":{"type":"number"},"requests":{"type":"number","description":"\"Request a visit\" when no time fit"},"today":{"type":"number","description":"Both, today (your local day): what dailyCap counts"},"total":{"type":"number"}},"required":["bookings","requests","today","total"],"additionalProperties":false},"createdVia":{"type":"string","enum":["office","platform","api_key"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"rotatedAt":{"type":"string","nullable":true},"revokedAt":{"type":"string","nullable":true},"lastUsedAt":{"type":"string","nullable":true}},"required":["id","clientCompanyId","url","fallbackUrl","label","kind","rep","serviceIds","services","expiresAt","dailyCap","active","status","usage","createdVia","createdAt","updatedAt","rotatedAt","revokedAt","lastUsedAt"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/clients/{clientId}/share-links/{linkId}/rotate":{"post":{"summary":"A new URL for a booking link (the old one stops working at once)","tags":["Share links"],"description":"Same link (settings, counts, history), new token. Anyone still holding the old URL gets 404. 409 link_revoked for a revoked link. Audited. Platform keys need write.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"clientId","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"linkId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"clientCompanyId":{"type":"string"},"url":{"type":"string","nullable":true,"description":"The link to share: https://<your share host>/r/<token> when your company (or the platform) has one (shareLinkHost), else https://go.crewlink.ai/r/<token>. null once revoked"},"fallbackUrl":{"type":"string","nullable":true,"description":"The same link on go.crewlink.ai (always works). null once revoked"},"label":{"type":"string","nullable":true},"kind":{"type":"string","enum":["client","rep"],"description":"client: the client's one general link (the rep types their name and email); rep: a per-rep link (name and email filled in and locked)"},"rep":{"type":"object","properties":{"name":{"type":"string"},"email":{"type":"string"}},"required":["name","email"],"additionalProperties":false,"nullable":true},"serviceIds":{"type":"array","items":{"type":"number"},"description":"The services it offers; empty = your residential services (all active services when none is residential)"},"services":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"description":"What the page offers now"},"expiresAt":{"type":"string","nullable":true},"dailyCap":{"type":"number","description":"Bookings and requests a day (your local day), at most"},"active":{"type":"boolean"},"status":{"type":"string","enum":["active","paused","expired","revoked"]},"usage":{"type":"object","properties":{"bookings":{"type":"number"},"requests":{"type":"number","description":"\"Request a visit\" when no time fit"},"today":{"type":"number","description":"Both, today (your local day): what dailyCap counts"},"total":{"type":"number"}},"required":["bookings","requests","today","total"],"additionalProperties":false},"createdVia":{"type":"string","enum":["office","platform","api_key"]},"createdAt":{"type":"string"},"updatedAt":{"type":"string"},"rotatedAt":{"type":"string","nullable":true},"revokedAt":{"type":"string","nullable":true},"lastUsedAt":{"type":"string","nullable":true}},"required":["id","clientCompanyId","url","fallbackUrl","label","kind","rep","serviceIds","services","expiresAt","dailyCap","active","status","usage","createdVia","createdAt","updatedAt","rotatedAt","revokedAt","lastUsedAt"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/conversations":{"get":{"summary":"Your conversations, with unread counts","tags":["Messaging"],"description":"The company channel first, then every conversation with messages, newest first. Your office line (techs) and your partner channels (office) are always listed. Page with `before` (the `nextBefore` of the last page).","parameters":[{"schema":{"type":"string","enum":["job","master","dm","office","partner"]},"in":"query","name":"kind","required":false},{"schema":{"type":"string","enum":["true","false"]},"in":"query","name":"unreadOnly","required":false},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"before","required":false},{"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"in":"query","name":"limit","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"conversations":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["job","master","dm","office","partner"],"description":"job: one per job. master: the company channel (\"Everyone\"). dm: two people. office: one tech and the whole office. partner: the two offices of a CrewConnect connection"},"title":{"type":"string","description":"What to call it, for you: the other person, the partner company, \"Everyone\", \"Office\", or \"Job #123 · Service\""},"subtitle":{"type":"string","nullable":true},"jobId":{"type":"number","nullable":true},"lastMessageAt":{"type":"string","nullable":true},"lastMessage":{"type":"object","properties":{"id":{"type":"number"},"preview":{"type":"string"},"sender":{"type":"string","nullable":true},"at":{"type":"string"}},"required":["id","preview","sender","at"],"additionalProperties":false,"nullable":true,"description":"The last message a person wrote (system messages are left out)"},"unread":{"type":"number","description":"Messages from others since you last read it"},"mentions":{"type":"number","description":"Unread messages that @mention you"},"muted":{"type":"boolean"},"canPost":{"type":"boolean"},"readOnly":{"type":"string","nullable":true,"description":"Why you can read but not post, when canPost is false"}},"required":["id","kind","title","subtitle","jobId","lastMessageAt","lastMessage","unread","mentions","muted","canPost","readOnly"],"additionalProperties":false}},"nextBefore":{"type":"string","nullable":true},"unread":{"type":"object","properties":{"total":{"type":"number","description":"Unread messages across your conversations (a muted conversation counts only its mentions)"},"mentions":{"type":"number"},"conversations":{"type":"number","description":"Conversations with something unread"}},"required":["total","mentions","conversations"],"additionalProperties":false}},"required":["conversations","nextBefore","unread"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/unread":{"get":{"summary":"Unread totals, for a badge","tags":["Messaging"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"number","description":"Unread messages across your conversations (a muted conversation counts only its mentions)"},"mentions":{"type":"number"},"conversations":{"type":"number","description":"Conversations with something unread"}},"required":["total","mentions","conversations"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/updates":{"get":{"summary":"What changed since a cursor (poll this)","tags":["Messaging"],"description":"Messages posted, edited or deleted since `since` in conversations you read, oldest first (up to 200; call again with the new `cursor` until `messages` comes back empty), the summaries of the conversations they are in, and your unread totals. Without `since`: no messages, just a cursor to start from. Poll every few seconds while a conversation is open and every 30 seconds or so otherwise.","parameters":[{"schema":{"type":"integer","minimum":0},"in":"query","name":"since","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"cursor":{"type":"number"},"messages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"conversationId":{"type":"string"},"kind":{"type":"string","enum":["text","note","system"],"description":"text: a message. note: a job note (a tech's note from the field app shows here too). system: what happened on the job (booked, moved, on the way, arrived, done, check-in, cancelled); show these quietly"},"body":{"type":"string","description":"Empty when deleted"},"deleted":{"type":"boolean"},"sender":{"type":"object","properties":{"userId":{"type":"string","nullable":true,"description":"Null for people at another company"},"name":{"type":"string"},"you":{"type":"boolean"}},"required":["userId","name","you"],"additionalProperties":false,"nullable":true,"description":"Null for system messages"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"Download link, good for 15 minutes"}},"required":["key","name","contentType","size","url"],"additionalProperties":false}},"mentions":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"}},"required":["userId","name"],"additionalProperties":false}},"mentionsYou":{"type":"boolean"},"createdAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"canEdit":{"type":"boolean","description":"Yours, not deleted, and under 15 minutes old"}},"required":["id","conversationId","kind","body","deleted","sender","attachments","mentions","mentionsYou","createdAt","editedAt","canEdit"],"additionalProperties":false}},"conversations":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["job","master","dm","office","partner"],"description":"job: one per job. master: the company channel (\"Everyone\"). dm: two people. office: one tech and the whole office. partner: the two offices of a CrewConnect connection"},"title":{"type":"string","description":"What to call it, for you: the other person, the partner company, \"Everyone\", \"Office\", or \"Job #123 · Service\""},"subtitle":{"type":"string","nullable":true},"jobId":{"type":"number","nullable":true},"lastMessageAt":{"type":"string","nullable":true},"lastMessage":{"type":"object","properties":{"id":{"type":"number"},"preview":{"type":"string"},"sender":{"type":"string","nullable":true},"at":{"type":"string"}},"required":["id","preview","sender","at"],"additionalProperties":false,"nullable":true,"description":"The last message a person wrote (system messages are left out)"},"unread":{"type":"number","description":"Messages from others since you last read it"},"mentions":{"type":"number","description":"Unread messages that @mention you"},"muted":{"type":"boolean"},"canPost":{"type":"boolean"},"readOnly":{"type":"string","nullable":true,"description":"Why you can read but not post, when canPost is false"}},"required":["id","kind","title","subtitle","jobId","lastMessageAt","lastMessage","unread","mentions","muted","canPost","readOnly"],"additionalProperties":false}},"unread":{"type":"object","properties":{"total":{"type":"number","description":"Unread messages across your conversations (a muted conversation counts only its mentions)"},"mentions":{"type":"number"},"conversations":{"type":"number","description":"Conversations with something unread"}},"required":["total","mentions","conversations"],"additionalProperties":false}},"required":["cursor","messages","conversations","unread"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/search":{"get":{"summary":"Search messages in your conversations","tags":["Messaging"],"parameters":[{"schema":{"type":"string","minLength":2,"maxLength":100},"in":"query","name":"q","required":true},{"schema":{"type":"integer","minimum":1,"maximum":50,"default":20},"in":"query","name":"limit","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"message":{"type":"object","properties":{"id":{"type":"number"},"conversationId":{"type":"string"},"kind":{"type":"string","enum":["text","note","system"],"description":"text: a message. note: a job note (a tech's note from the field app shows here too). system: what happened on the job (booked, moved, on the way, arrived, done, check-in, cancelled); show these quietly"},"body":{"type":"string","description":"Empty when deleted"},"deleted":{"type":"boolean"},"sender":{"type":"object","properties":{"userId":{"type":"string","nullable":true,"description":"Null for people at another company"},"name":{"type":"string"},"you":{"type":"boolean"}},"required":["userId","name","you"],"additionalProperties":false,"nullable":true,"description":"Null for system messages"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"Download link, good for 15 minutes"}},"required":["key","name","contentType","size","url"],"additionalProperties":false}},"mentions":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"}},"required":["userId","name"],"additionalProperties":false}},"mentionsYou":{"type":"boolean"},"createdAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"canEdit":{"type":"boolean","description":"Yours, not deleted, and under 15 minutes old"}},"required":["id","conversationId","kind","body","deleted","sender","attachments","mentions","mentionsYou","createdAt","editedAt","canEdit"],"additionalProperties":false},"conversation":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["job","master","dm","office","partner"]},"title":{"type":"string"}},"required":["id","kind","title"],"additionalProperties":false}},"required":["message","conversation"],"additionalProperties":false}}},"required":["results"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/recipients":{"get":{"summary":"Who you can start a direct message with","tags":["Messaging"],"description":"The office can message anyone in the company. A tech can message the office, and other techs only when the owner has turned that on. Techs also have their office line (GET /v1/messaging/office-line).","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"people":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string"},"name":{"type":"string"},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"personId":{"type":"number","nullable":true}},"required":["userId","name","role","personId"],"additionalProperties":false}}},"required":["people"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/conversations/{id}":{"get":{"summary":"A conversation and who is in it","tags":["Messaging"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"conversation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["job","master","dm","office","partner"],"description":"job: one per job. master: the company channel (\"Everyone\"). dm: two people. office: one tech and the whole office. partner: the two offices of a CrewConnect connection"},"title":{"type":"string","description":"What to call it, for you: the other person, the partner company, \"Everyone\", \"Office\", or \"Job #123 · Service\""},"subtitle":{"type":"string","nullable":true},"jobId":{"type":"number","nullable":true},"lastMessageAt":{"type":"string","nullable":true},"lastMessage":{"type":"object","properties":{"id":{"type":"number"},"preview":{"type":"string"},"sender":{"type":"string","nullable":true},"at":{"type":"string"}},"required":["id","preview","sender","at"],"additionalProperties":false,"nullable":true,"description":"The last message a person wrote (system messages are left out)"},"unread":{"type":"number","description":"Messages from others since you last read it"},"mentions":{"type":"number","description":"Unread messages that @mention you"},"muted":{"type":"boolean"},"canPost":{"type":"boolean"},"readOnly":{"type":"string","nullable":true,"description":"Why you can read but not post, when canPost is false"}},"required":["id","kind","title","subtitle","jobId","lastMessageAt","lastMessage","unread","mentions","muted","canPost","readOnly"],"additionalProperties":false},"members":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"},"role":{"type":"string","enum":["owner","field_manager","dispatcher","tech"]},"you":{"type":"boolean"},"company":{"type":"string","nullable":true,"description":"Set for people at the other CrewConnect company"}},"required":["userId","name","role","you","company"],"additionalProperties":false}}},"required":["conversation","members"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/conversations/{id}/messages":{"get":{"summary":"Messages in a conversation (oldest first)","tags":["Messaging"],"description":"The newest `limit` messages, or with `before` (a message id) the page before it; `nextBefore` is set while there are older ones. With `after` (a message id), the next messages after it instead.","parameters":[{"schema":{"type":"integer"},"in":"query","name":"before","required":false},{"schema":{"type":"integer"},"in":"query","name":"after","required":false},{"schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"in":"query","name":"limit","required":false},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"messages":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"conversationId":{"type":"string"},"kind":{"type":"string","enum":["text","note","system"],"description":"text: a message. note: a job note (a tech's note from the field app shows here too). system: what happened on the job (booked, moved, on the way, arrived, done, check-in, cancelled); show these quietly"},"body":{"type":"string","description":"Empty when deleted"},"deleted":{"type":"boolean"},"sender":{"type":"object","properties":{"userId":{"type":"string","nullable":true,"description":"Null for people at another company"},"name":{"type":"string"},"you":{"type":"boolean"}},"required":["userId","name","you"],"additionalProperties":false,"nullable":true,"description":"Null for system messages"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"Download link, good for 15 minutes"}},"required":["key","name","contentType","size","url"],"additionalProperties":false}},"mentions":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"}},"required":["userId","name"],"additionalProperties":false}},"mentionsYou":{"type":"boolean"},"createdAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"canEdit":{"type":"boolean","description":"Yours, not deleted, and under 15 minutes old"}},"required":["id","conversationId","kind","body","deleted","sender","attachments","mentions","mentionsYou","createdAt","editedAt","canEdit"],"additionalProperties":false}},"nextBefore":{"type":"number","nullable":true}},"required":["messages","nextBefore"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"post":{"summary":"Post a message","tags":["Messaging"],"description":"Up to 4000 characters, with up to 5 attachments. 403 with the reason when you can read but not post here (office_only, techs_off, ended). 60 posts a minute per person (429 rate_limited, with Retry-After). Send an Idempotency-Key so a retried send posts once. Each attachment is checked in storage: 422 bad_attachment when nothing was uploaded at its key, or when the file is bigger than its size or another type than its contentType (that file is then deleted).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","maxLength":4000,"default":""},"kind":{"type":"string","enum":["text","note"],"default":"text","description":"note: only in a job thread"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","maxLength":300},"name":{"type":"string","minLength":1,"maxLength":200},"contentType":{"type":"string","enum":["image/jpeg","image/png","image/heic","image/webp","application/pdf"]},"size":{"type":"integer","minimum":1,"maximum":20971520}},"required":["key","name","contentType","size"],"additionalProperties":false},"maxItems":5,"default":[],"description":"Files uploaded first with POST /v1/messaging/conversations/{id}/attachments"},"mentions":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":50,"default":[],"description":"User ids picked in the composer. \"@Full Name\" (or \"@First\" when only one member has that first name) in the body counts too. Only members of the conversation can be mentioned"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true},{"schema":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[\\w.:-]+$"},"in":"header","name":"idempotency-key","required":false,"description":"Any unique string per action (a UUID is fine). Repeats within 24 hours return the first response"}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"conversationId":{"type":"string"},"kind":{"type":"string","enum":["text","note","system"],"description":"text: a message. note: a job note (a tech's note from the field app shows here too). system: what happened on the job (booked, moved, on the way, arrived, done, check-in, cancelled); show these quietly"},"body":{"type":"string","description":"Empty when deleted"},"deleted":{"type":"boolean"},"sender":{"type":"object","properties":{"userId":{"type":"string","nullable":true,"description":"Null for people at another company"},"name":{"type":"string"},"you":{"type":"boolean"}},"required":["userId","name","you"],"additionalProperties":false,"nullable":true,"description":"Null for system messages"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"Download link, good for 15 minutes"}},"required":["key","name","contentType","size","url"],"additionalProperties":false}},"mentions":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"}},"required":["userId","name"],"additionalProperties":false}},"mentionsYou":{"type":"boolean"},"createdAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"canEdit":{"type":"boolean","description":"Yours, not deleted, and under 15 minutes old"}},"required":["id","conversationId","kind","body","deleted","sender","attachments","mentions","mentionsYou","createdAt","editedAt","canEdit"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/messaging/conversations/{id}/read":{"post":{"summary":"Mark read","tags":["Messaging"],"description":"Up to `messageId`, or everything when it is left out. Never moves backwards.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"messageId":{"type":"integer"}},"additionalProperties":false,"default":{}}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"unread":{"type":"object","properties":{"total":{"type":"number","description":"Unread messages across your conversations (a muted conversation counts only its mentions)"},"mentions":{"type":"number"},"conversations":{"type":"number","description":"Conversations with something unread"}},"required":["total","mentions","conversations"],"additionalProperties":false}},"required":["unread"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/conversations/{id}/mute":{"post":{"summary":"Mute or unmute","tags":["Messaging"],"description":"A muted conversation still counts unread, but adds only its @mentions to your badge total.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"muted":{"type":"boolean"}},"required":["muted"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["job","master","dm","office","partner"],"description":"job: one per job. master: the company channel (\"Everyone\"). dm: two people. office: one tech and the whole office. partner: the two offices of a CrewConnect connection"},"title":{"type":"string","description":"What to call it, for you: the other person, the partner company, \"Everyone\", \"Office\", or \"Job #123 · Service\""},"subtitle":{"type":"string","nullable":true},"jobId":{"type":"number","nullable":true},"lastMessageAt":{"type":"string","nullable":true},"lastMessage":{"type":"object","properties":{"id":{"type":"number"},"preview":{"type":"string"},"sender":{"type":"string","nullable":true},"at":{"type":"string"}},"required":["id","preview","sender","at"],"additionalProperties":false,"nullable":true,"description":"The last message a person wrote (system messages are left out)"},"unread":{"type":"number","description":"Messages from others since you last read it"},"mentions":{"type":"number","description":"Unread messages that @mention you"},"muted":{"type":"boolean"},"canPost":{"type":"boolean"},"readOnly":{"type":"string","nullable":true,"description":"Why you can read but not post, when canPost is false"}},"required":["id","kind","title","subtitle","jobId","lastMessageAt","lastMessage","unread","mentions","muted","canPost","readOnly"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/conversations/{id}/attachments":{"post":{"summary":"Attach a file: get an upload URL","tags":["Messaging"],"description":"PUT the bytes to `uploadUrl` (same Content-Type) within 15 minutes, then post a message with { key, name, contentType, size } in `attachments`. Photos and PDFs, up to 20 MB. Only someone who may post here gets a URL.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contentType":{"type":"string","enum":["image/jpeg","image/png","image/heic","image/webp","application/pdf"]},"name":{"type":"string","minLength":1,"maxLength":200},"size":{"type":"integer","minimum":1,"maximum":20971520}},"required":["contentType","name","size"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"},"uploadUrl":{"type":"string"},"expiresInSeconds":{"type":"number"}},"required":["key","uploadUrl","expiresInSeconds"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/messages/{id}":{"patch":{"summary":"Edit your message (within 15 minutes)","tags":["Messaging"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","minLength":1,"maxLength":4000}},"required":["body"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"conversationId":{"type":"string"},"kind":{"type":"string","enum":["text","note","system"],"description":"text: a message. note: a job note (a tech's note from the field app shows here too). system: what happened on the job (booked, moved, on the way, arrived, done, check-in, cancelled); show these quietly"},"body":{"type":"string","description":"Empty when deleted"},"deleted":{"type":"boolean"},"sender":{"type":"object","properties":{"userId":{"type":"string","nullable":true,"description":"Null for people at another company"},"name":{"type":"string"},"you":{"type":"boolean"}},"required":["userId","name","you"],"additionalProperties":false,"nullable":true,"description":"Null for system messages"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"Download link, good for 15 minutes"}},"required":["key","name","contentType","size","url"],"additionalProperties":false}},"mentions":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"}},"required":["userId","name"],"additionalProperties":false}},"mentionsYou":{"type":"boolean"},"createdAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"canEdit":{"type":"boolean","description":"Yours, not deleted, and under 15 minutes old"}},"required":["id","conversationId","kind","body","deleted","sender","attachments","mentions","mentionsYou","createdAt","editedAt","canEdit"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"delete":{"summary":"Delete your message (within 15 minutes)","tags":["Messaging"],"description":"It stays in the thread as \"deleted\", with no text or files; its files are deleted from storage.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"conversationId":{"type":"string"},"kind":{"type":"string","enum":["text","note","system"],"description":"text: a message. note: a job note (a tech's note from the field app shows here too). system: what happened on the job (booked, moved, on the way, arrived, done, check-in, cancelled); show these quietly"},"body":{"type":"string","description":"Empty when deleted"},"deleted":{"type":"boolean"},"sender":{"type":"object","properties":{"userId":{"type":"string","nullable":true,"description":"Null for people at another company"},"name":{"type":"string"},"you":{"type":"boolean"}},"required":["userId","name","you"],"additionalProperties":false,"nullable":true,"description":"Null for system messages"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"Download link, good for 15 minutes"}},"required":["key","name","contentType","size","url"],"additionalProperties":false}},"mentions":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"}},"required":["userId","name"],"additionalProperties":false}},"mentionsYou":{"type":"boolean"},"createdAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"canEdit":{"type":"boolean","description":"Yours, not deleted, and under 15 minutes old"}},"required":["id","conversationId","kind","body","deleted","sender","attachments","mentions","mentionsYou","createdAt","editedAt","canEdit"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/dms":{"post":{"summary":"Start (or reopen) a direct message","tags":["Messaging"],"description":"With someone in your company you may message (GET /v1/messaging/recipients): 403 techs_off when two techs try and your owner has not turned that on. 404 for someone not in your company.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userId":{"type":"string","format":"uuid"}},"required":["userId"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["job","master","dm","office","partner"],"description":"job: one per job. master: the company channel (\"Everyone\"). dm: two people. office: one tech and the whole office. partner: the two offices of a CrewConnect connection"},"title":{"type":"string","description":"What to call it, for you: the other person, the partner company, \"Everyone\", \"Office\", or \"Job #123 · Service\""},"subtitle":{"type":"string","nullable":true},"jobId":{"type":"number","nullable":true},"lastMessageAt":{"type":"string","nullable":true},"lastMessage":{"type":"object","properties":{"id":{"type":"number"},"preview":{"type":"string"},"sender":{"type":"string","nullable":true},"at":{"type":"string"}},"required":["id","preview","sender","at"],"additionalProperties":false,"nullable":true,"description":"The last message a person wrote (system messages are left out)"},"unread":{"type":"number","description":"Messages from others since you last read it"},"mentions":{"type":"number","description":"Unread messages that @mention you"},"muted":{"type":"boolean"},"canPost":{"type":"boolean"},"readOnly":{"type":"string","nullable":true,"description":"Why you can read but not post, when canPost is false"}},"required":["id","kind","title","subtitle","jobId","lastMessageAt","lastMessage","unread","mentions","muted","canPost","readOnly"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/jobs/{jobId}":{"get":{"summary":"A job's thread (made on first use)","tags":["Messaging"],"description":"Your company's job, or one you sent through CrewConnect. Its members are the office, the tech(s) on the job and, for CrewConnect work, the other company's office. A tech not on the job gets 403.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"jobId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["job","master","dm","office","partner"],"description":"job: one per job. master: the company channel (\"Everyone\"). dm: two people. office: one tech and the whole office. partner: the two offices of a CrewConnect connection"},"title":{"type":"string","description":"What to call it, for you: the other person, the partner company, \"Everyone\", \"Office\", or \"Job #123 · Service\""},"subtitle":{"type":"string","nullable":true},"jobId":{"type":"number","nullable":true},"lastMessageAt":{"type":"string","nullable":true},"lastMessage":{"type":"object","properties":{"id":{"type":"number"},"preview":{"type":"string"},"sender":{"type":"string","nullable":true},"at":{"type":"string"}},"required":["id","preview","sender","at"],"additionalProperties":false,"nullable":true,"description":"The last message a person wrote (system messages are left out)"},"unread":{"type":"number","description":"Messages from others since you last read it"},"mentions":{"type":"number","description":"Unread messages that @mention you"},"muted":{"type":"boolean"},"canPost":{"type":"boolean"},"readOnly":{"type":"string","nullable":true,"description":"Why you can read but not post, when canPost is false"}},"required":["id","kind","title","subtitle","jobId","lastMessageAt","lastMessage","unread","mentions","muted","canPost","readOnly"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/jobs/{jobId}/messages":{"post":{"summary":"Post in a job's thread","tags":["Messaging"],"description":"By job id, so the field app can queue it offline without knowing the conversation. Up to 4000 characters, with up to 5 attachments. 403 with the reason when you can read but not post here (office_only, techs_off, ended). 60 posts a minute per person (429 rate_limited, with Retry-After). Send an Idempotency-Key so a retried send posts once. Each attachment is checked in storage: 422 bad_attachment when nothing was uploaded at its key, or when the file is bigger than its size or another type than its contentType (that file is then deleted).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","maxLength":4000,"default":""},"kind":{"type":"string","enum":["text","note"],"default":"text","description":"note: only in a job thread"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","maxLength":300},"name":{"type":"string","minLength":1,"maxLength":200},"contentType":{"type":"string","enum":["image/jpeg","image/png","image/heic","image/webp","application/pdf"]},"size":{"type":"integer","minimum":1,"maximum":20971520}},"required":["key","name","contentType","size"],"additionalProperties":false},"maxItems":5,"default":[],"description":"Files uploaded first with POST /v1/messaging/conversations/{id}/attachments"},"mentions":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":50,"default":[],"description":"User ids picked in the composer. \"@Full Name\" (or \"@First\" when only one member has that first name) in the body counts too. Only members of the conversation can be mentioned"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"jobId","required":true},{"schema":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[\\w.:-]+$"},"in":"header","name":"idempotency-key","required":false,"description":"Any unique string per action (a UUID is fine). Repeats within 24 hours return the first response"}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"conversationId":{"type":"string"},"kind":{"type":"string","enum":["text","note","system"],"description":"text: a message. note: a job note (a tech's note from the field app shows here too). system: what happened on the job (booked, moved, on the way, arrived, done, check-in, cancelled); show these quietly"},"body":{"type":"string","description":"Empty when deleted"},"deleted":{"type":"boolean"},"sender":{"type":"object","properties":{"userId":{"type":"string","nullable":true,"description":"Null for people at another company"},"name":{"type":"string"},"you":{"type":"boolean"}},"required":["userId","name","you"],"additionalProperties":false,"nullable":true,"description":"Null for system messages"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"Download link, good for 15 minutes"}},"required":["key","name","contentType","size","url"],"additionalProperties":false}},"mentions":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"}},"required":["userId","name"],"additionalProperties":false}},"mentionsYou":{"type":"boolean"},"createdAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"canEdit":{"type":"boolean","description":"Yours, not deleted, and under 15 minutes old"}},"required":["id","conversationId","kind","body","deleted","sender","attachments","mentions","mentionsYou","createdAt","editedAt","canEdit"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/messaging/office-line":{"get":{"summary":"A tech's line to the office","tags":["Messaging"],"description":"One conversation per tech, between that tech and everyone in the office: a tech gets their own; the office passes `techUserId`.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"query","name":"techUserId","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["job","master","dm","office","partner"],"description":"job: one per job. master: the company channel (\"Everyone\"). dm: two people. office: one tech and the whole office. partner: the two offices of a CrewConnect connection"},"title":{"type":"string","description":"What to call it, for you: the other person, the partner company, \"Everyone\", \"Office\", or \"Job #123 · Service\""},"subtitle":{"type":"string","nullable":true},"jobId":{"type":"number","nullable":true},"lastMessageAt":{"type":"string","nullable":true},"lastMessage":{"type":"object","properties":{"id":{"type":"number"},"preview":{"type":"string"},"sender":{"type":"string","nullable":true},"at":{"type":"string"}},"required":["id","preview","sender","at"],"additionalProperties":false,"nullable":true,"description":"The last message a person wrote (system messages are left out)"},"unread":{"type":"number","description":"Messages from others since you last read it"},"mentions":{"type":"number","description":"Unread messages that @mention you"},"muted":{"type":"boolean"},"canPost":{"type":"boolean"},"readOnly":{"type":"string","nullable":true,"description":"Why you can read but not post, when canPost is false"}},"required":["id","kind","title","subtitle","jobId","lastMessageAt","lastMessage","unread","mentions","muted","canPost","readOnly"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/messaging/office-line/messages":{"post":{"summary":"Post to the office (techs)","tags":["Messaging"],"description":"Your office line, without knowing its id (for the field app's offline queue). The office answers with POST /v1/messaging/conversations/{id}/messages. Up to 4000 characters, with up to 5 attachments. 403 with the reason when you can read but not post here (office_only, techs_off, ended). 60 posts a minute per person (429 rate_limited, with Retry-After). Send an Idempotency-Key so a retried send posts once. Each attachment is checked in storage: 422 bad_attachment when nothing was uploaded at its key, or when the file is bigger than its size or another type than its contentType (that file is then deleted).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"type":"string","maxLength":4000,"default":""},"kind":{"type":"string","enum":["text","note"],"default":"text","description":"note: only in a job thread"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","maxLength":300},"name":{"type":"string","minLength":1,"maxLength":200},"contentType":{"type":"string","enum":["image/jpeg","image/png","image/heic","image/webp","application/pdf"]},"size":{"type":"integer","minimum":1,"maximum":20971520}},"required":["key","name","contentType","size"],"additionalProperties":false},"maxItems":5,"default":[],"description":"Files uploaded first with POST /v1/messaging/conversations/{id}/attachments"},"mentions":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":50,"default":[],"description":"User ids picked in the composer. \"@Full Name\" (or \"@First\" when only one member has that first name) in the body counts too. Only members of the conversation can be mentioned"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[\\w.:-]+$"},"in":"header","name":"idempotency-key","required":false,"description":"Any unique string per action (a UUID is fine). Repeats within 24 hours return the first response"}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"conversationId":{"type":"string"},"kind":{"type":"string","enum":["text","note","system"],"description":"text: a message. note: a job note (a tech's note from the field app shows here too). system: what happened on the job (booked, moved, on the way, arrived, done, check-in, cancelled); show these quietly"},"body":{"type":"string","description":"Empty when deleted"},"deleted":{"type":"boolean"},"sender":{"type":"object","properties":{"userId":{"type":"string","nullable":true,"description":"Null for people at another company"},"name":{"type":"string"},"you":{"type":"boolean"}},"required":["userId","name","you"],"additionalProperties":false,"nullable":true,"description":"Null for system messages"},"attachments":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"contentType":{"type":"string"},"size":{"type":"number"},"url":{"type":"string","nullable":true,"description":"Download link, good for 15 minutes"}},"required":["key","name","contentType","size","url"],"additionalProperties":false}},"mentions":{"type":"array","items":{"type":"object","properties":{"userId":{"type":"string","nullable":true},"name":{"type":"string"}},"required":["userId","name"],"additionalProperties":false}},"mentionsYou":{"type":"boolean"},"createdAt":{"type":"string"},"editedAt":{"type":"string","nullable":true},"canEdit":{"type":"boolean","description":"Yours, not deleted, and under 15 minutes old"}},"required":["id","conversationId","kind","body","deleted","sender","attachments","mentions","mentionsYou","createdAt","editedAt","canEdit"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/messaging/settings":{"get":{"summary":"Messaging rules for your company","tags":["Messaging"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"techsCanMessageTechs":{"type":"boolean","description":"Techs may message each other directly. Off: techs talk to each other only in the threads of jobs they share"},"everyoneCanPostInMaster":{"type":"boolean","description":"Everyone may post in the company channel. Off: only the office posts there; everyone reads it"}},"required":["techsCanMessageTechs","everyoneCanPostInMaster"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"put":{"summary":"Change the messaging rules (owner only)","tags":["Messaging"],"description":"Send either or both. Turning techsCanMessageTechs off makes existing tech-to-tech messages read-only.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"techsCanMessageTechs":{"type":"boolean","description":"Techs may message each other directly. Off: techs talk to each other only in the threads of jobs they share"},"everyoneCanPostInMaster":{"type":"boolean","description":"Everyone may post in the company channel. Off: only the office posts there; everyone reads it"}},"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"techsCanMessageTechs":{"type":"boolean","description":"Techs may message each other directly. Off: techs talk to each other only in the threads of jobs they share"},"everyoneCanPostInMaster":{"type":"boolean","description":"Everyone may post in the company channel. Off: only the office posts there; everyone reads it"}},"required":["techsCanMessageTechs","everyoneCanPostInMaster"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/me":{"get":{"summary":"Who I am","tags":["Field app"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"personId":{"type":"number"},"timezone":{"type":"string","description":"My zone (my branch's): `now` and my day's times are in it"},"company":{"type":"object","properties":{"name":{"type":"string"},"timezone":{"type":"string"},"slug":{"type":"string","description":"For address search: GET /v1/public/geocode?slug="}},"required":["name","timezone","slug"],"additionalProperties":false},"now":{"type":"string"}},"required":["name","personId","timezone","company","now"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/day":{"get":{"summary":"My day","tags":["Field app"],"parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of the times (my branch's)"},"stops":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"end":{"type":"string"},"stage":{"type":"string","enum":["scheduled","en_route","on_site","done"]},"service":{"type":"string"},"customer":{"type":"string","nullable":true,"description":"First name only"},"address":{"type":"string","nullable":true},"lat":{"type":"number"},"lng":{"type":"number"},"notes":{"type":"string","nullable":true},"crew":{"type":"array","items":{"type":"string"},"description":"Everyone on the job, first names"},"textsCustomer":{"type":"boolean","description":"Whether CrewLink texts this job's customer (on my way, review link). False when the job, the company or the booking platform turned homeowner messages off, or the customer has no phone: the app must not say the customer was texted"},"siteVisit":{"type":"object","properties":{"number":{"type":"number","nullable":true},"of":{"type":"number"},"revisit":{"type":"boolean"}},"required":["number","of","revisit"],"additionalProperties":false,"nullable":true,"description":"Where this visit sits at the property: \"Visit 3 at this site\" (cancelled visits not counted); null when it is the site's only visit"}},"required":["jobId","start","end","stage","service","customer","address","lat","lng","notes","crew","textsCustomer"],"additionalProperties":false}}},"required":["date","timezone","stops"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/jobs/{id}/stage":{"post":{"summary":"On the way, arrived, done","tags":["Field app"],"description":"On the way texts the customer with an ETA (from your location when you send lat/lng). Arrived starts the clock; done stops it, teaches the engine your time for this service, and texts a review link. A job only moves forward, and only on its day or the day after: 409 not_today when every visit of yours on it is on a later local day, or more than one day ago (the office or an integration can still complete it). A tap queued offline sends `at` (when it happened): arrived and done use it, and the on-the-way text counts the ETA from it (no text when you'd already be there by now). Send an Idempotency-Key so a retried tap acts once.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"stage":{"type":"string","enum":["en_route","on_site","done"]},"lat":{"type":"number"},"lng":{"type":"number"},"at":{"type":"string","format":"date-time","description":"When the tech actually tapped (ISO 8601), for taps queued offline. Up to 24 hours back (422 invalid_at otherwise); default now"}},"required":["stage"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[\\w.:-]+$"},"in":"header","name":"idempotency-key","required":false,"description":"Any unique string per action (a UUID is fine). Repeats within 24 hours return the first response"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"stage":{"type":"string"},"etaMinutes":{"type":"number","nullable":true,"description":"Minutes from now"},"minutesOnSite":{"type":"number","nullable":true},"texted":{"type":"boolean","description":"en_route: the customer's on-my-way text was queued. done: the review link was queued (it goes out 30 minutes later). False when homeowner messages are off for this job, the customer has no textable number, or (en_route) the tap was too old"}},"required":["jobId","stage","etaMinutes","minutesOnSite","texted"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/location":{"post":{"summary":"Where I am (sent every minute or so while on the clock, per the company GPS policy)","tags":["Field app"],"description":"A fix queued offline sends `at` (when it was taken); an older fix never replaces a newer one. Send accuracy, heading and speed when the phone has them (dispatch boards show them). CrewLink keeps the last fix (the live position, 30 days) and, when it was taken on the clock, adds it to the location trail (thinned: one fix per 25 m or 60 s; kept the company's locationRetentionDays, default 90, for mileage and pay disputes). Off the clock nothing goes into the trail.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"accuracy":{"type":"number","minimum":0,"maximum":100000,"description":"Horizontal accuracy in metres, as the phone reports it"},"heading":{"type":"number","minimum":0,"maximum":360,"description":"Degrees clockwise from true north (when moving)"},"speed":{"type":"number","minimum":0,"maximum":100,"description":"Metres per second"},"at":{"type":"string","format":"date-time","description":"When the tech actually tapped (ISO 8601), for taps queued offline. Up to 24 hours back (422 invalid_at otherwise); default now"}},"required":["lat","lng"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[\\w.:-]+$"},"in":"header","name":"idempotency-key","required":false,"description":"Any unique string per action (a UUID is fine). Repeats within 24 hours return the first response"}],"security":[{"officeKey":[]}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/jobs/{id}/checkin":{"post":{"summary":"Mid-job check-in: on track, or how much longer","tags":["Field app"],"description":"The app asks halfway through each job. 'on_track' just records it. Extra minutes extend the job now, while there's still time: every later job that no longer fits goes to a teammate at the same time, or to the nearest open time, and those customers are texted (unless homeowner messages are off for that job). The answer lists what changed (techs by first name) and, per job, whether its customer is texted (customerTexted). 'office_review' asks the office to look at the job (with an optional note): it changes nothing on the schedule and flags the job as needing attention on the office's lists. 409 not_on_site before you arrive; 409 not_today for a job on a later day or more than a day ago (as for stage taps); 409 no_visit_today when the job isn't on your schedule today. `at` (a check-in queued offline) is recorded as when it happened; the schedule changes from now.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["on_track","plus15","plus30","plus60","office_review"]},"note":{"type":"string","minLength":1,"maxLength":300,"description":"With office_review: what the office should look at"},"at":{"type":"string","format":"date-time","description":"When the tech actually tapped (ISO 8601), for taps queued offline. Up to 24 hours back (422 invalid_at otherwise); default now"}},"required":["status"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[\\w.:-]+$"},"in":"header","name":"idempotency-key","required":false,"description":"Any unique string per action (a UUID is fine). Repeats within 24 hours return the first response"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"newEnd":{"type":"string","nullable":true},"changes":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"action":{"type":"string","enum":["kept","handed_off","new_time","needs_call"]},"local":{"type":"string","nullable":true},"techs":{"type":"array","items":{"type":"string"}},"customerTexted":{"type":"boolean","description":"That job's customer is texted about the change. False for kept, for needs_call with a time (the office sorts it out), and when homeowner messages are off for that job"}},"required":["jobId","action","local","techs","customerTexted"],"additionalProperties":false}}},"required":["jobId","newEnd","changes"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/jobs/{id}/notes":{"post":{"summary":"Add a note to a job","tags":["Field app"],"description":"`at`: when it was written, for notes queued offline.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","minLength":1,"maxLength":4000},"at":{"type":"string","format":"date-time","description":"When the tech actually tapped (ISO 8601), for taps queued offline. Up to 24 hours back (422 invalid_at otherwise); default now"}},"required":["text"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[\\w.:-]+$"},"in":"header","name":"idempotency-key","required":false,"description":"Any unique string per action (a UUID is fine). Repeats within 24 hours return the first response"}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"}},"required":["id"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/jobs/{id}/photos":{"post":{"summary":"Add a photo: get an upload URL","tags":["Field app"],"description":"PUT the image bytes to `uploadUrl` (with the same Content-Type) within 15 minutes, then mark it uploaded. Bytes go straight to storage, not through the API. Up to 25 MB.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contentType":{"type":"string","enum":["image/jpeg","image/png","image/heic","image/webp"]},"caption":{"type":"string","maxLength":300}},"required":["contentType"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"photoId":{"type":"string"},"uploadUrl":{"type":"string"}},"required":["photoId","uploadUrl"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/jobs/{id}/photos/{photoId}/uploaded":{"post":{"summary":"Mark a photo uploaded","tags":["Field app"],"description":"Checks the photo in storage first: `409 not_uploaded` when nothing is at the upload URL yet; `422 upload_mismatch` when it is over 25 MB or isn't the Content-Type given (the file and the pending photo are then removed: add it again). Repeating it is harmless.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"string","format":"uuid"},"in":"path","name":"photoId","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"photoId":{"type":"string"},"status":{"type":"string","enum":["uploaded"]}},"required":["photoId","status"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/tech/jobs/{id}/call":{"post":{"summary":"Call or text this job's customer through a masked number","tags":["Field app"],"description":"Returns the number to dial. The customer sees the CrewLink number, not yours, and calls or texts back through it for a day. 409 if your mobile isn't on file; 503 if masked calling isn't set up.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"dial":{"type":"string"}},"required":["dial"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/tech/jobs/{id}":{"get":{"summary":"A job on my schedule, with its history","tags":["Field app"],"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"stage":{"type":"string"},"service":{"type":"string"},"address":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true},"events":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string"},"at":{"type":"string"},"text":{"type":"string","nullable":true}},"required":["kind","at","text"],"additionalProperties":false}}},"required":["jobId","stage","service","address","notes","events"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/field/next-stop":{"get":{"summary":"My next stop today","tags":["Field app"],"description":"The first of today's stops, in plan order, that isn't done (one you're on the way to or on site at counts). `jobId` is null when there is none left. Route to it with GET /v1/field/jobs/{id}/route.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number","nullable":true}},"required":["jobId"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/field/jobs/{id}/route":{"get":{"summary":"Turn-by-turn driving route to a job","tags":["Field app"],"description":"A driving route from `from` (your position, `lat,lng`) to one of your jobs, with turn-by-turn steps and an ETA from live traffic where available. Without `from` the route starts at your last reported position, else your previous stop today, else where your day starts. A job you're not on is 404. Up to 30 routes a minute per tech (429 rate_limited); 503 not_configured when this server has no routing; 502 route_unavailable when no driving route could be found (open the address in a maps app instead).","parameters":[{"schema":{"type":"string","pattern":"^-?\\d{1,3}(\\.\\d+)?,-?\\d{1,3}(\\.\\d+)?$"},"in":"query","name":"from","required":false,"description":"Where you are: `lat,lng`, e.g. 41.8902,-87.6511"},{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"to":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"},"address":{"type":"string","nullable":true}},"required":["lat","lng","address"],"additionalProperties":false},"from":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"provider":{"type":"string","enum":["mapbox","osrm"],"description":"mapbox: live traffic. osrm: free-flow road times"},"distanceMeters":{"type":"number"},"durationSeconds":{"type":"number","description":"The provider's drive time (traffic-aware with mapbox)"},"eta":{"type":"string","description":"When the tech gets there leaving now (UTC instant)"},"arriveBy":{"type":"string","nullable":true,"description":"When the job starts (UTC instant)"},"geometry":{"type":"object","properties":{"type":{"type":"string","enum":["LineString"]},"coordinates":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}],"description":"[lng, lat]"}}},"required":["type","coordinates"],"additionalProperties":false,"description":"The whole route, GeoJSON [lng, lat] pairs"},"steps":{"type":"array","items":{"type":"object","properties":{"instruction":{"type":"string","description":"What to do at this maneuver, e.g. \"Turn right onto Elm Street\""},"name":{"type":"string","description":"The road the step drives along (may be empty)"},"distanceMeters":{"type":"number","description":"Length of this step, from its maneuver to the next"},"durationSeconds":{"type":"number"},"maneuver":{"type":"object","properties":{"type":{"type":"string","description":"depart, turn, merge, on ramp, off ramp, fork, end of road, roundabout, continue, new name, arrive, ..."},"modifier":{"type":"string","nullable":true,"description":"left, right, slight left, sharp right, straight, uturn, ..."},"location":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}],"description":"Where the maneuver happens, [lng, lat]"},"bearingAfter":{"type":"number","nullable":true,"description":"Heading after the maneuver, degrees from north"}},"required":["type","modifier","location","bearingAfter"],"additionalProperties":false},"voice":{"type":"string","nullable":true,"description":"What to say as the tech reaches this maneuver, when the provider writes one"},"lanes":{"type":"array","items":{"type":"object","properties":{"indications":{"type":"array","items":{"type":"string"},"description":"The arrows painted on the lane: left, slight left, straight, slight right, right, sharp left, sharp right, uturn, none"},"valid":{"type":"boolean","description":"The route can take this maneuver from this lane"},"active":{"type":"boolean","description":"The best lane for the route (Mapbox only)"},"validIndication":{"type":"string","description":"Which of its arrows the route follows (Mapbox only)"}},"required":["indications","valid"],"additionalProperties":false},"description":"Lane guidance for this maneuver, left to right, when the provider knows the lanes; absent otherwise"}},"required":["instruction","name","distanceMeters","durationSeconds","maneuver","voice"],"additionalProperties":false},"description":"In driving order: the first is the departure, the last the arrival"},"maxspeed":{"type":"array","items":{"type":"object","properties":{"speed":{"type":"number"},"unit":{"type":"string","enum":["km/h","mph"]}},"required":["speed","unit"],"additionalProperties":false,"nullable":true},"description":"Posted speed limits: one per segment of `geometry` (coordinates.length - 1), null where unknown. Absent when the provider knows none"},"mapStyle":{"type":"object","properties":{"url":{"type":"string","description":"Mapbox's stock navigation style (kept for older apps)"},"token":{"type":"string","description":"A public map token, safe to use in a browser or app"},"style":{"type":"string","enum":["crewlink-nav"],"description":"CrewLink's own navigation style: the apps carry it (light, blue and dark) and load Mapbox's vector tiles with `token`"},"version":{"type":"number","description":"Version of that style the server expects (1)"},"themes":{"type":"array","items":{"type":"string","enum":["light","blue","dark"]},"description":"The looks the style comes in"}},"required":["url","token","style","version","themes"],"additionalProperties":false,"nullable":true,"description":"A map style to draw under the route, or null: draw the route, your position and the job without map tiles"}},"required":["jobId","to","from","provider","distanceMeters","durationSeconds","eta","arriveBy","geometry","steps","mapStyle"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/team/{id}/locations":{"get":{"summary":"A tech's location trail for one day, with road miles, stops and the order visits were reached","tags":["Office"],"description":"The fixes CrewLink kept for one tech while they were on the clock (from the field app and, for a platform's own app, POST /v1/office/team/{id}/locations), thinned to one per 25 m or 60 s, for `date` (a local day in the tech's zone; default today) or `from`/`to` (UTC instants, at most 24 hours apart). With them: road distance (map-matched where road data is set up, else straight segments), stops of 3 minutes or more matched to that day's visits, and the planned vs actual order. For mileage and pay disputes. Owners and company-scope dispatchers; platform keys need read and a grant with full people names (403 otherwise). Fixes older than the company's locationRetentionDays (default 90) are gone. Another company's person is 404.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":false,"description":"Local date in the tech's zone, YYYY-MM-DD (default today)"},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"from","required":false,"description":"Start, ISO instant (with `to`, instead of date)"},{"schema":{"type":"string","format":"date-time"},"in":"query","name":"to","required":false,"description":"End, ISO instant (exclusive; at most 24 hours after from)"},{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"personId":{"type":"number"},"displayName":{"type":"string","description":"The tech's name (by the grant's peopleNames for platforms)"},"timezone":{"type":"string","description":"The tech's zone: `date` is a local day in it"},"date":{"type":"string","nullable":true,"description":"The local day asked for (null when from/to were sent)"},"from":{"type":"string","description":"Start of the span (UTC instant, inclusive)"},"to":{"type":"string","description":"End of the span (UTC instant, exclusive)"},"retentionDays":{"type":"integer","description":"How long this company keeps trails (locationRetentionDays); older fixes are gone"},"fixes":{"type":"array","items":{"type":"object","properties":{"at":{"type":"string","description":"When the fix was taken (UTC instant)"},"lat":{"type":"number"},"lng":{"type":"number"},"accuracy":{"type":"number","nullable":true,"description":"Horizontal accuracy, metres"},"heading":{"type":"number","nullable":true,"description":"Degrees clockwise from true north"},"speed":{"type":"number","nullable":true,"description":"Metres per second"},"source":{"type":"string","description":"app (the CrewLink field app) or the slug of the platform whose tech app sent it, e.g. acme"}},"required":["at","lat","lng","accuracy","heading","speed","source"],"additionalProperties":false},"description":"On-the-clock fixes in time order, thinned (one per 25 m or 60 s)"},"summary":{"type":"object","properties":{"fixCount":{"type":"integer"},"firstFixAt":{"type":"string","nullable":true},"lastFixAt":{"type":"string","nullable":true},"distanceMeters":{"type":"number","description":"Road distance driven over the span"},"miles":{"type":"number","description":"The same in miles, 2 decimals"},"distanceMethod":{"type":"string","enum":["map_matched","segments","none"],"description":"map_matched: snapped to roads (OSRM match); segments: the sum of straight lines between fixes (no road data here); none: fewer than two fixes"},"stops":{"type":"array","items":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"minutes":{"type":"integer"},"lat":{"type":"number"},"lng":{"type":"number"},"jobId":{"type":"number","nullable":true,"description":"The visit whose address is within 150 m, or null"}},"required":["from","to","minutes","lat","lng","jobId"],"additionalProperties":false},"description":"Places the tech stayed within 75 m for 3 minutes or more"},"visits":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"plannedStart":{"type":"string","description":"Scheduled start (UTC instant)"},"plannedOrder":{"type":"integer"},"arrivedAt":{"type":"string","nullable":true,"description":"Start of the first stop at it (from the trail, not the Arrived tap)"},"departedAt":{"type":"string","nullable":true,"description":"End of the last stop at it"},"dwellMinutes":{"type":"integer"},"actualOrder":{"type":"integer","nullable":true,"description":"Order the visits were first reached in; null if never reached"}},"required":["jobId","plannedStart","plannedOrder","arrivedAt","departedAt","dwellMinutes","actualOrder"],"additionalProperties":false},"description":"The tech's visits on the span's local days, in plan order"},"plannedOrder":{"type":"array","items":{"type":"number"},"description":"Job ids in plan order"},"actualOrder":{"type":"array","items":{"type":"number"},"description":"Job ids in the order the trail reached them"}},"required":["fixCount","firstFixAt","lastFixAt","distanceMeters","miles","distanceMethod","stops","visits","plannedOrder","actualOrder"],"additionalProperties":false}},"required":["personId","displayName","timezone","date","from","to","retentionDays","fixes","summary"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"post":{"summary":"Send a batch of a tech's location fixes from a platform's own tech app","tags":["Office"],"description":"For a platform whose own app tracks its techs (with each tech's location consent, which the platform must hold): up to 500 fixes per call, each taken in the last 7 days and not in the future. CrewLink keeps only fixes taken while the tech was on the clock (30 minutes either side of that day's working windows and visits): they join the tech's location trail (source: the platform's slug, thinned to one per 25 m or 60 s, at most one per 10 s) and the newest becomes the tech's live position (the dispatch board, the live read and dispatch.position) unless a newer one is already known. A fix at an instant already stored is a duplicate and skipped, so a batch can be resent safely. Off-the-clock fixes are dropped, not stored. Platform keys with write only (403 for anyone else). Another company's person is 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fixes":{"type":"array","items":{"type":"object","properties":{"at":{"type":"string","format":"date-time","description":"When the phone took the fix (ISO instant); within the last 7 days, not in the future"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"accuracy":{"type":"number","minimum":0,"maximum":100000,"description":"Horizontal accuracy in metres"},"heading":{"type":"number","minimum":0,"maximum":360,"description":"Degrees clockwise from true north"},"speed":{"type":"number","minimum":0,"maximum":100,"description":"Metres per second"}},"required":["at","lat","lng"],"additionalProperties":false},"minItems":1,"maxItems":500,"description":"Up to 500, any order"}},"required":["fixes"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"received":{"type":"integer"},"stored":{"type":"integer","description":"Added to the trail"},"duplicates":{"type":"integer","description":"Same instant as one already stored (or twice in the batch)"},"thinned":{"type":"integer","description":"Within 25 m and 60 s of a kept fix, or within 10 s of one"},"offClock":{"type":"integer","description":"Taken off the clock: dropped, never stored"},"rejected":{"type":"array","items":{"type":"object","properties":{"index":{"type":"integer"},"reason":{"type":"string","enum":["too_old","in_future","invalid_point"]}},"required":["index","reason"],"additionalProperties":false},"description":"Fixes refused, by their index in the batch"},"positionUpdated":{"type":"boolean","description":"The newest on-the-clock fix became the live position"}},"required":["received","stored","duplicates","thinned","offClock","rejected","positionUpdated"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/route":{"post":{"summary":"A driving route with turn-by-turn steps (the field app's directions, for your own UI)","tags":["Office"],"description":"From `from` to `to` (a point) or `jobId` (one of this company's jobs), through up to 23 `waypoints` in order. The same answer as the field app's route: GeoJSON line, distance, duration, ETA, steps with instruction, maneuver and voice line, and lanes and posted speed limits when the provider knows them. Mapbox driving with traffic first (`departAt` in the future: typical traffic then), else OSRM free flow. Answers are cached for 5 minutes. No map token unless `includeMapStyle` (then only CrewLink's public pk. token). Render it in your own UI. Read scope; 60 routes a minute per company, shared by all of its users and keys (per platform for a platform key, across its companies) (429 rate_limited); 503 not_configured without a routing provider; 502 route_unavailable when no route was found.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"object","properties":{"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180}},"required":["lat","lng"],"additionalProperties":false},"to":{"type":"object","properties":{"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180}},"required":["lat","lng"],"additionalProperties":false,"description":"Where to go (or jobId)"},"jobId":{"type":"integer","description":"One of this company's jobs: route to its address"},"waypoints":{"type":"array","items":{"type":"object","properties":{"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180}},"required":["lat","lng"],"additionalProperties":false},"maxItems":23,"description":"Stops on the way, in order (up to 23)"},"departAt":{"type":"string","format":"date-time","description":"Leave at this instant (default now). In the future: Mapbox's typical traffic then; OSRM is free flow either way"},"profile":{"type":"string","enum":["driving"],"description":"driving (the only profile)"},"includeMapStyle":{"type":"boolean","description":"Also answer CrewLink's map style and its public (pk.) token"}},"required":["from"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"to":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"},"address":{"type":"string","nullable":true}},"required":["lat","lng","address"],"additionalProperties":false},"jobId":{"type":"number","nullable":true,"description":"The job routed to, when the request named one"},"waypoints":{"type":"array","items":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false}},"provider":{"type":"string","enum":["mapbox","osrm"],"description":"mapbox: driving with traffic (live, or typical for a future departAt). osrm: free-flow road times"},"profile":{"type":"string","enum":["driving"]},"distanceMeters":{"type":"number"},"durationSeconds":{"type":"number"},"departAt":{"type":"string","description":"The departure the route is for (now unless departAt was sent)"},"eta":{"type":"string","description":"departAt plus the duration (UTC instant)"},"geometry":{"type":"object","properties":{"type":{"type":"string","enum":["LineString"]},"coordinates":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}],"description":"[lng, lat]"}}},"required":["type","coordinates"],"additionalProperties":false,"description":"The whole route, GeoJSON [lng, lat] pairs"},"steps":{"type":"array","items":{"type":"object","properties":{"instruction":{"type":"string","description":"What to do at this maneuver, e.g. \"Turn right onto Elm Street\""},"name":{"type":"string","description":"The road the step drives along (may be empty)"},"distanceMeters":{"type":"number","description":"Length of this step, from its maneuver to the next"},"durationSeconds":{"type":"number"},"maneuver":{"type":"object","properties":{"type":{"type":"string","description":"depart, turn, merge, on ramp, off ramp, fork, end of road, roundabout, continue, new name, arrive, ..."},"modifier":{"type":"string","nullable":true,"description":"left, right, slight left, sharp right, straight, uturn, ..."},"location":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}],"description":"Where the maneuver happens, [lng, lat]"},"bearingAfter":{"type":"number","nullable":true,"description":"Heading after the maneuver, degrees from north"}},"required":["type","modifier","location","bearingAfter"],"additionalProperties":false},"voice":{"type":"string","nullable":true,"description":"What to say as the tech reaches this maneuver, when the provider writes one"},"lanes":{"type":"array","items":{"type":"object","properties":{"indications":{"type":"array","items":{"type":"string"},"description":"The arrows painted on the lane: left, slight left, straight, slight right, right, sharp left, sharp right, uturn, none"},"valid":{"type":"boolean","description":"The route can take this maneuver from this lane"},"active":{"type":"boolean","description":"The best lane for the route (Mapbox only)"},"validIndication":{"type":"string","description":"Which of its arrows the route follows (Mapbox only)"}},"required":["indications","valid"],"additionalProperties":false},"description":"Lane guidance for this maneuver, left to right, when the provider knows the lanes; absent otherwise"}},"required":["instruction","name","distanceMeters","durationSeconds","maneuver","voice"],"additionalProperties":false},"description":"In driving order across every leg: departure first, arrival last (an arrive step at each waypoint)"},"maxspeed":{"type":"array","items":{"type":"object","properties":{"speed":{"type":"number"},"unit":{"type":"string","enum":["km/h","mph"]}},"required":["speed","unit"],"additionalProperties":false,"nullable":true},"description":"Posted speed limits, one per segment of `geometry`, null where unknown; absent when the provider knows none"},"mapStyle":{"type":"object","properties":{"url":{"type":"string","description":"Mapbox's stock navigation style (kept for older apps)"},"token":{"type":"string","description":"A public map token, safe to use in a browser or app"},"style":{"type":"string","enum":["crewlink-nav"],"description":"CrewLink's own navigation style: the apps carry it (light, blue and dark) and load Mapbox's vector tiles with `token`"},"version":{"type":"number","description":"Version of that style the server expects (1)"},"themes":{"type":"array","items":{"type":"string","enum":["light","blue","dark"]},"description":"The looks the style comes in"}},"required":["url","token","style","version","themes"],"additionalProperties":false,"nullable":true,"description":"Only with includeMapStyle: CrewLink's style and the public (pk.) token; null otherwise or when none is set up"}},"required":["from","to","jobId","waypoints","provider","profile","distanceMeters","durationSeconds","departAt","eta","geometry","steps","mapStyle"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/route-matrix":{"post":{"summary":"Road distance and drive time between up to 25 origins and 25 destinations (for mileage and pay)","tags":["Office"],"description":"Road distance (metres) and free-flow drive time (seconds) from every origin to every destination, from CrewLink's road data (OSRM). Made for mileage and pay: distances are road distances, not straight lines. `planned: true` adds `plannedSeconds`: the drive CrewLink's planning would allow leaving at `departAt` (default now), with its time-of-day traffic model and margin (the company's buffer between jobs not included). A cell is null when there is no road route. Read scope; at most 25 x 25; 30 a minute per company, shared by all of its users and keys (per platform for a platform key) (429 rate_limited); 503 not_configured without road data, 502 matrix_unavailable when it failed or the points span two regions.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"origins":{"type":"array","items":{"type":"object","properties":{"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180}},"required":["lat","lng"],"additionalProperties":false},"minItems":1,"maxItems":25,"description":"Up to 25"},"destinations":{"type":"array","items":{"type":"object","properties":{"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180}},"required":["lat","lng"],"additionalProperties":false},"minItems":1,"maxItems":25,"description":"Up to 25"},"departAt":{"type":"string","format":"date-time","description":"For plannedSeconds: when the drives start (default now)"},"planned":{"type":"boolean","description":"Add plannedSeconds (CrewLink's planning model)"}},"required":["origins","destinations"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","enum":["osrm"]},"departAt":{"type":"string"},"origins":{"type":"array","items":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false}},"destinations":{"type":"array","items":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false}},"distancesMeters":{"type":"array","items":{"type":"array","items":{"type":"number","nullable":true}},"description":"[origin][destination], road metres"},"durationsSeconds":{"type":"array","items":{"type":"array","items":{"type":"number","nullable":true}},"description":"[origin][destination], free-flow seconds"},"miles":{"type":"array","items":{"type":"array","items":{"type":"number","nullable":true}},"description":"distancesMeters in miles, 2 decimals"},"plannedSeconds":{"type":"array","items":{"type":"array","items":{"type":"number","nullable":true}},"description":"With planned: the drive planning allows, seconds"}},"required":["provider","departAt","origins","destinations","distancesMeters","durationsSeconds","miles"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/brain/day":{"get":{"summary":"One day's plan vs what happened","tags":["Plan vs actual"],"description":"Every tech's stops for the day with the planned arrival, start and finish (the day's final plan) next to the real arrival and finish from the field app, the drive as predicted, as the traffic provider saw it and as driven, and the day's numbers (worked out now from what's recorded; `reviewedAt` says when the stored review was made, if there is one). `snapshots` lists every version of the plan recorded that day: the cutoff (the evening before) and each change after it. Without `date`, today.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":false,"description":"Local date, YYYY-MM-DD (default today)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"timezone":{"type":"string"},"provider":{"type":"string","nullable":true,"description":"The traffic provider compared against, or null when none is set up"},"reviewedAt":{"type":"string","nullable":true},"metrics":{"type":"object","properties":{"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals tapped no more than 15 minutes after the planned start (the same rule as the on_time KPI)"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true,"description":"CrewLink's ETA error: mean absolute minutes between the arrival the plan predicted (the plan in force when the tech set off) and the real arrival"},"etaMaeProvider":{"type":"number","nullable":true,"description":"The traffic provider's ETA error: mean absolute minutes between departure + the provider's drive time (looked up as the tech set off) and the real arrival"},"paired":{"type":"object","properties":{"n":{"type":"number"},"ours":{"type":"number","nullable":true},"provider":{"type":"number","nullable":true}},"required":["n","ours","provider"],"additionalProperties":false,"description":"Both ETA errors on the same drives only (drives that have both): the fair head-to-head"},"driveErrMean":{"type":"number","nullable":true,"description":"Mean of real drive minutes minus our predicted drive minutes (positive: drives took longer than planned)"},"driveMae":{"type":"number","nullable":true,"description":"Mean absolute drive-minute error, ours"},"providerDriveErrMean":{"type":"number","nullable":true,"description":"Mean of real drive minutes minus the provider's"},"onsiteErrMean":{"type":"number","nullable":true,"description":"Mean of real minutes on site minus the minutes planned (positive: jobs ran long)"},"onsiteMae":{"type":"number","nullable":true},"byTech":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Person id, hour of the day (0-23) or service id"},"name":{"type":"string"},"stops":{"type":"number"},"arrivals":{"type":"number","description":"Stops with an arrival tapped"},"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals no more than 15 minutes after the planned start"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true},"etaMaeProvider":{"type":"number","nullable":true},"driveErrMean":{"type":"number","nullable":true},"onsiteErrMean":{"type":"number","nullable":true}},"required":["key","name","stops","arrivals","onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","driveErrMean","onsiteErrMean"],"additionalProperties":false}},"byHour":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Person id, hour of the day (0-23) or service id"},"name":{"type":"string"},"stops":{"type":"number"},"arrivals":{"type":"number","description":"Stops with an arrival tapped"},"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals no more than 15 minutes after the planned start"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true},"etaMaeProvider":{"type":"number","nullable":true},"driveErrMean":{"type":"number","nullable":true},"onsiteErrMean":{"type":"number","nullable":true}},"required":["key","name","stops","arrivals","onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","driveErrMean","onsiteErrMean"],"additionalProperties":false},"description":"By the hour of the planned start"},"byService":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Person id, hour of the day (0-23) or service id"},"name":{"type":"string"},"stops":{"type":"number"},"arrivals":{"type":"number","description":"Stops with an arrival tapped"},"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals no more than 15 minutes after the planned start"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true},"etaMaeProvider":{"type":"number","nullable":true},"driveErrMean":{"type":"number","nullable":true},"onsiteErrMean":{"type":"number","nullable":true}},"required":["key","name","stops","arrivals","onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","driveErrMean","onsiteErrMean"],"additionalProperties":false}},"counts":{"type":"object","properties":{"plannedStops":{"type":"number","description":"Stops in the day's final plan"},"arrivals":{"type":"number"},"done":{"type":"number"},"drives":{"type":"number","description":"On-the-way taps recorded"},"drivesMeasured":{"type":"number","description":"Drives with both on-the-way and arrived taps"},"providerLookups":{"type":"number"},"paired":{"type":"number"},"checkins":{"type":"number","description":"Mid-job check-ins"},"replans":{"type":"number","description":"Times the plan changed after the cutoff"},"customersMoved":{"type":"number","description":"Customers handed off, moved or asked to pick a new time because a job ran long"},"snapshots":{"type":"number"}},"required":["plannedStops","arrivals","done","drives","drivesMeasured","providerLookups","paired","checkins","replans","customersMoved","snapshots"],"additionalProperties":false}},"required":["onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","paired","driveErrMean","driveMae","providerDriveErrMean","onsiteErrMean","onsiteMae","byTech","byHour","byService","counts"],"additionalProperties":false},"snapshots":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"takenAt":{"type":"string"},"reason":{"type":"string","enum":["cutoff","replan","manual"]},"engineVersion":{"type":"string"},"travelModelVersion":{"type":"string"},"stops":{"type":"number"}},"required":["id","takenAt","reason","engineVersion","travelModelVersion","stops"],"additionalProperties":false}},"techs":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"name":{"type":"string"},"stops":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"service":{"type":"string"},"customer":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"inFinalPlan":{"type":"boolean","description":"false: the stop moved off this tech's day after they started it, or was added outside a snapshot"},"plannedArrive":{"type":"string","nullable":true,"description":"Company-local wall clock the plan predicted the tech would arrive"},"plannedStart":{"type":"string","nullable":true},"plannedFinish":{"type":"string","nullable":true},"actualArrive":{"type":"string","nullable":true},"actualDone":{"type":"string","nullable":true},"lateMin":{"type":"number","nullable":true,"description":"Minutes after the planned start the tech arrived (negative: early)"},"status":{"type":"string","enum":["ok","tight","late"],"nullable":true,"description":"As the Dispatch dots: ok 5+ min early, tight up to 10 min late, late more than 10 min late; null before arrival"},"plannedOnsiteMin":{"type":"number","nullable":true},"actualOnsiteMin":{"type":"number","nullable":true},"checkin":{"type":"boolean"},"replanned":{"type":"boolean","description":"A check-in on this visit ran long and replanned the rest of the day"},"customersMoved":{"type":"number"},"drive":{"type":"object","properties":{"from":{"type":"string","enum":["home","base","job"]},"fromJobId":{"type":"number","nullable":true},"departAt":{"type":"string","description":"Company-local wall clock of the on-the-way tap"},"predictedMin":{"type":"number","nullable":true},"providerMin":{"type":"number","nullable":true},"provider":{"type":"string","nullable":true},"actualMin":{"type":"number","nullable":true},"ourEtaErrorMin":{"type":"number","nullable":true,"description":"Real arrival minus our predicted arrival"},"providerEtaErrorMin":{"type":"number","nullable":true,"description":"Real arrival minus departure + the provider's minutes"}},"required":["from","fromJobId","departAt","predictedMin","providerMin","provider","actualMin","ourEtaErrorMin","providerEtaErrorMin"],"additionalProperties":false,"nullable":true}},"required":["jobId","service","customer","address","inFinalPlan","plannedArrive","plannedStart","plannedFinish","actualArrive","actualDone","lateMin","status","plannedOnsiteMin","actualOnsiteMin","checkin","replanned","customersMoved","drive"],"additionalProperties":false}}},"required":["personId","name","stops"],"additionalProperties":false}}},"required":["date","timezone","provider","reviewedAt","metrics","snapshots","techs"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/brain/summary":{"get":{"summary":"Plan vs actual over a date range: the reviewed days as a series, and the whole range together","tags":["Plan vs actual"],"description":"`days` has one point per reviewed day (days are reviewed each night, or with POST /v1/office/brain/review). `totals` is worked out now from everything recorded in the range, with the same definitions. Days before the company setting metricsFrom (\"Count KPIs from\", PATCH /v1/office/settings) are left out. 422 if `to` is before `from` or the range is over 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true,"description":"First local date, YYYY-MM-DD"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Last local date, inclusive (at most 366 days)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"metricsFrom":{"type":"string","nullable":true,"description":"\"Count KPIs from\" (local date): days before it are left out. null: no limit"},"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"reviewedAt":{"type":"string"},"onTimeRate":{"type":"number","nullable":true},"etaMaeOurs":{"type":"number","nullable":true},"etaMaeProvider":{"type":"number","nullable":true},"paired":{"type":"object","properties":{"n":{"type":"number"},"ours":{"type":"number","nullable":true},"provider":{"type":"number","nullable":true}},"required":["n","ours","provider"],"additionalProperties":false},"driveErrMean":{"type":"number","nullable":true},"onsiteErrMean":{"type":"number","nullable":true},"counts":{"type":"object","properties":{"plannedStops":{"type":"number","description":"Stops in the day's final plan"},"arrivals":{"type":"number"},"done":{"type":"number"},"drives":{"type":"number","description":"On-the-way taps recorded"},"drivesMeasured":{"type":"number","description":"Drives with both on-the-way and arrived taps"},"providerLookups":{"type":"number"},"paired":{"type":"number"},"checkins":{"type":"number","description":"Mid-job check-ins"},"replans":{"type":"number","description":"Times the plan changed after the cutoff"},"customersMoved":{"type":"number","description":"Customers handed off, moved or asked to pick a new time because a job ran long"},"snapshots":{"type":"number"}},"required":["plannedStops","arrivals","done","drives","drivesMeasured","providerLookups","paired","checkins","replans","customersMoved","snapshots"],"additionalProperties":false}},"required":["date","reviewedAt","onTimeRate","etaMaeOurs","etaMaeProvider","paired","driveErrMean","onsiteErrMean","counts"],"additionalProperties":false}},"totals":{"type":"object","properties":{"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals tapped no more than 15 minutes after the planned start (the same rule as the on_time KPI)"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true,"description":"CrewLink's ETA error: mean absolute minutes between the arrival the plan predicted (the plan in force when the tech set off) and the real arrival"},"etaMaeProvider":{"type":"number","nullable":true,"description":"The traffic provider's ETA error: mean absolute minutes between departure + the provider's drive time (looked up as the tech set off) and the real arrival"},"paired":{"type":"object","properties":{"n":{"type":"number"},"ours":{"type":"number","nullable":true},"provider":{"type":"number","nullable":true}},"required":["n","ours","provider"],"additionalProperties":false,"description":"Both ETA errors on the same drives only (drives that have both): the fair head-to-head"},"driveErrMean":{"type":"number","nullable":true,"description":"Mean of real drive minutes minus our predicted drive minutes (positive: drives took longer than planned)"},"driveMae":{"type":"number","nullable":true,"description":"Mean absolute drive-minute error, ours"},"providerDriveErrMean":{"type":"number","nullable":true,"description":"Mean of real drive minutes minus the provider's"},"onsiteErrMean":{"type":"number","nullable":true,"description":"Mean of real minutes on site minus the minutes planned (positive: jobs ran long)"},"onsiteMae":{"type":"number","nullable":true},"byTech":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Person id, hour of the day (0-23) or service id"},"name":{"type":"string"},"stops":{"type":"number"},"arrivals":{"type":"number","description":"Stops with an arrival tapped"},"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals no more than 15 minutes after the planned start"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true},"etaMaeProvider":{"type":"number","nullable":true},"driveErrMean":{"type":"number","nullable":true},"onsiteErrMean":{"type":"number","nullable":true}},"required":["key","name","stops","arrivals","onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","driveErrMean","onsiteErrMean"],"additionalProperties":false}},"byHour":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Person id, hour of the day (0-23) or service id"},"name":{"type":"string"},"stops":{"type":"number"},"arrivals":{"type":"number","description":"Stops with an arrival tapped"},"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals no more than 15 minutes after the planned start"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true},"etaMaeProvider":{"type":"number","nullable":true},"driveErrMean":{"type":"number","nullable":true},"onsiteErrMean":{"type":"number","nullable":true}},"required":["key","name","stops","arrivals","onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","driveErrMean","onsiteErrMean"],"additionalProperties":false},"description":"By the hour of the planned start"},"byService":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Person id, hour of the day (0-23) or service id"},"name":{"type":"string"},"stops":{"type":"number"},"arrivals":{"type":"number","description":"Stops with an arrival tapped"},"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals no more than 15 minutes after the planned start"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true},"etaMaeProvider":{"type":"number","nullable":true},"driveErrMean":{"type":"number","nullable":true},"onsiteErrMean":{"type":"number","nullable":true}},"required":["key","name","stops","arrivals","onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","driveErrMean","onsiteErrMean"],"additionalProperties":false}},"counts":{"type":"object","properties":{"plannedStops":{"type":"number","description":"Stops in the day's final plan"},"arrivals":{"type":"number"},"done":{"type":"number"},"drives":{"type":"number","description":"On-the-way taps recorded"},"drivesMeasured":{"type":"number","description":"Drives with both on-the-way and arrived taps"},"providerLookups":{"type":"number"},"paired":{"type":"number"},"checkins":{"type":"number","description":"Mid-job check-ins"},"replans":{"type":"number","description":"Times the plan changed after the cutoff"},"customersMoved":{"type":"number","description":"Customers handed off, moved or asked to pick a new time because a job ran long"},"snapshots":{"type":"number"}},"required":["plannedStops","arrivals","done","drives","drivesMeasured","providerLookups","paired","checkins","replans","customersMoved","snapshots"],"additionalProperties":false}},"required":["onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","paired","driveErrMean","driveMae","providerDriveErrMean","onsiteErrMean","onsiteMae","byTech","byHour","byService","counts"],"additionalProperties":false}},"required":["from","to","metricsFrom","days","totals"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/brain/days":{"get":{"summary":"The most recent reviewed days, newest first","tags":["Plan vs actual"],"description":"Days that have a stored review (reviewed each night, or with POST /v1/office/brain/review) and had something planned or worked, newest first, up to `limit` (default 1, at most 60). Today is included once it has been reviewed. Plan vs actual opens on the first one, so it never lands on an empty weekend or a morning before anything happened.","parameters":[{"schema":{"type":"integer","minimum":1,"maximum":60,"default":1},"in":"query","name":"limit","required":false,"description":"How many days (1-60, default 1)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"days":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","description":"Local date, YYYY-MM-DD"},"reviewedAt":{"type":"string"},"plannedStops":{"type":"number"},"arrivals":{"type":"number"},"onTimeRate":{"type":"number","nullable":true}},"required":["date","reviewedAt","plannedStops","arrivals","onTimeRate"],"additionalProperties":false}}},"required":["days"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/brain/review":{"post":{"summary":"Review a day now (store its numbers and send day.reviewed)","tags":["Plan vs actual"],"description":"Days are reviewed each night at 03:00 company time. This does it now for any day up to today (today's review covers what has happened so far, and the nightly run reviews it again once the day is over). Sends the day.reviewed webhook. 422 for a day that hasn't started.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":true,"description":"Local date, YYYY-MM-DD"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"reviewedAt":{"type":"string"},"metrics":{"type":"object","properties":{"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals tapped no more than 15 minutes after the planned start (the same rule as the on_time KPI)"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true,"description":"CrewLink's ETA error: mean absolute minutes between the arrival the plan predicted (the plan in force when the tech set off) and the real arrival"},"etaMaeProvider":{"type":"number","nullable":true,"description":"The traffic provider's ETA error: mean absolute minutes between departure + the provider's drive time (looked up as the tech set off) and the real arrival"},"paired":{"type":"object","properties":{"n":{"type":"number"},"ours":{"type":"number","nullable":true},"provider":{"type":"number","nullable":true}},"required":["n","ours","provider"],"additionalProperties":false,"description":"Both ETA errors on the same drives only (drives that have both): the fair head-to-head"},"driveErrMean":{"type":"number","nullable":true,"description":"Mean of real drive minutes minus our predicted drive minutes (positive: drives took longer than planned)"},"driveMae":{"type":"number","nullable":true,"description":"Mean absolute drive-minute error, ours"},"providerDriveErrMean":{"type":"number","nullable":true,"description":"Mean of real drive minutes minus the provider's"},"onsiteErrMean":{"type":"number","nullable":true,"description":"Mean of real minutes on site minus the minutes planned (positive: jobs ran long)"},"onsiteMae":{"type":"number","nullable":true},"byTech":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Person id, hour of the day (0-23) or service id"},"name":{"type":"string"},"stops":{"type":"number"},"arrivals":{"type":"number","description":"Stops with an arrival tapped"},"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals no more than 15 minutes after the planned start"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true},"etaMaeProvider":{"type":"number","nullable":true},"driveErrMean":{"type":"number","nullable":true},"onsiteErrMean":{"type":"number","nullable":true}},"required":["key","name","stops","arrivals","onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","driveErrMean","onsiteErrMean"],"additionalProperties":false}},"byHour":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Person id, hour of the day (0-23) or service id"},"name":{"type":"string"},"stops":{"type":"number"},"arrivals":{"type":"number","description":"Stops with an arrival tapped"},"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals no more than 15 minutes after the planned start"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true},"etaMaeProvider":{"type":"number","nullable":true},"driveErrMean":{"type":"number","nullable":true},"onsiteErrMean":{"type":"number","nullable":true}},"required":["key","name","stops","arrivals","onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","driveErrMean","onsiteErrMean"],"additionalProperties":false},"description":"By the hour of the planned start"},"byService":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Person id, hour of the day (0-23) or service id"},"name":{"type":"string"},"stops":{"type":"number"},"arrivals":{"type":"number","description":"Stops with an arrival tapped"},"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals no more than 15 minutes after the planned start"},"lateMinMean":{"type":"number","nullable":true,"description":"Mean minutes after the planned start at arrival (negative: early)"},"etaMaeOurs":{"type":"number","nullable":true},"etaMaeProvider":{"type":"number","nullable":true},"driveErrMean":{"type":"number","nullable":true},"onsiteErrMean":{"type":"number","nullable":true}},"required":["key","name","stops","arrivals","onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","driveErrMean","onsiteErrMean"],"additionalProperties":false}},"counts":{"type":"object","properties":{"plannedStops":{"type":"number","description":"Stops in the day's final plan"},"arrivals":{"type":"number"},"done":{"type":"number"},"drives":{"type":"number","description":"On-the-way taps recorded"},"drivesMeasured":{"type":"number","description":"Drives with both on-the-way and arrived taps"},"providerLookups":{"type":"number"},"paired":{"type":"number"},"checkins":{"type":"number","description":"Mid-job check-ins"},"replans":{"type":"number","description":"Times the plan changed after the cutoff"},"customersMoved":{"type":"number","description":"Customers handed off, moved or asked to pick a new time because a job ran long"},"snapshots":{"type":"number"}},"required":["plannedStops","arrivals","done","drives","drivesMeasured","providerLookups","paired","checkins","replans","customersMoved","snapshots"],"additionalProperties":false}},"required":["onTimeRate","lateMinMean","etaMaeOurs","etaMaeProvider","paired","driveErrMean","driveMae","providerDriveErrMean","onsiteErrMean","onsiteMae","byTech","byHour","byService","counts"],"additionalProperties":false}},"required":["date","reviewedAt","metrics"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/brain/snapshot":{"post":{"summary":"Record the day's plan as it stands now","tags":["Plan vs actual"],"description":"Plans are recorded on their own at the cutoff (18:00 company time the evening before) and after every change from then on; this records one more now (reason manual), e.g. before a big reshuffle. Today or a later day; 422 for a past day.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":false,"description":"Local date, YYYY-MM-DD (default today)"}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"date":{"type":"string"},"takenAt":{"type":"string"},"reason":{"type":"string","enum":["manual"]},"stops":{"type":"number"}},"required":["id","date","takenAt","reason","stops"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/brain/settings":{"get":{"summary":"The traffic-lookup budget and how much of it today has used","tags":["Plan vs actual"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"travelLookupsPerDay":{"type":"number","description":"Traffic lookups the brain may spend per day on your drives (default 50; 0 turns them off)"},"lookupsToday":{"type":"number","description":"Lookups spent on today's drives"},"provider":{"type":"string","nullable":true,"description":"The traffic provider compared against (mapbox), or null when none is set up"}},"required":["travelLookupsPerDay","lookupsToday","provider"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"patch":{"summary":"Change the traffic-lookup budget (owner only)","tags":["Plan vs actual"],"description":"Each time a tech sets off, CrewLink can ask the traffic provider how long that drive should take, to compare with its own prediction. This caps those lookups per day for your company (0 turns them off). A platform-wide daily cap (2000 today) applies on top.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"travelLookupsPerDay":{"type":"integer","minimum":0,"maximum":1000}},"required":["travelLookupsPerDay"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"travelLookupsPerDay":{"type":"number","description":"Traffic lookups the brain may spend per day on your drives (default 50; 0 turns them off)"},"lookupsToday":{"type":"number","description":"Lookups spent on today's drives"},"provider":{"type":"string","nullable":true,"description":"The traffic provider compared against (mapbox), or null when none is set up"}},"required":["travelLookupsPerDay","lookupsToday","provider"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/wins":{"get":{"summary":"The week's goals, the wins feed and each tech's streak and badges","tags":["Wins"],"description":"Goals are measured from the same facts as Reports: on time by the On-time arrival KPI rule, reviews and CrewConnect jobs sent as their KPIs count them, first-time fix per finished job. `prior` is the week before. The feed lists the wins created during the week (newest first); a team win about a finished week appears in the week after it.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"week","required":false,"description":"Any day of the week (Monday to Sunday, company time); default this week"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"week":{"type":"object","properties":{"from":{"type":"string","description":"Monday"},"to":{"type":"string","description":"Sunday"}},"required":["from","to"],"additionalProperties":false},"settings":{"type":"object","properties":{"leaderboard":{"type":"boolean","description":"The company leaderboard is on (default off). Techs can still hide themselves."},"goals":{"type":"object","properties":{"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals on time (the On-time arrival KPI rule)"},"firstTimeFixRate":{"type":"number","nullable":true,"description":"Percent of finished jobs with no return visit for the same customer and service within 14 days"},"reviewsPerWeek":{"type":"number","nullable":true,"description":"Reviews customers leave in a week"},"connectSentPerWeek":{"type":"number","nullable":true,"description":"CrewConnect jobs sent to partners, by start day"}},"required":["onTimeRate","firstTimeFixRate","reviewsPerWeek","connectSentPerWeek"],"additionalProperties":false}},"required":["leaderboard","goals"],"additionalProperties":false},"goals":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","enum":["on_time","first_time_fix","reviews","connect_sent"]},"name":{"type":"string"},"unit":{"type":"string","enum":["pct","count"]},"target":{"type":"number","nullable":true},"actual":{"type":"number","nullable":true},"prior":{"type":"number","nullable":true},"progress":{"type":"number","nullable":true,"description":"Actual as a percent of the target, at most 100"},"met":{"type":"boolean","nullable":true},"measuredOn":{"type":"number","description":"Arrivals (on time), finished jobs (first-time fix), or the count itself"}},"required":["key","name","unit","target","actual","prior","progress","met","measuredOn"],"additionalProperties":false}},"feed":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"kind":{"type":"string","enum":["badge","best_day","streak","team_best_week","goal_met","team_streak"]},"at":{"type":"string"},"personId":{"type":"number","nullable":true},"person":{"type":"string","nullable":true,"description":"Full name for the office; null for team wins"},"title":{"type":"string"},"badge":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"tier":{"type":"string","enum":["bronze","silver","gold","platinum","diamond"]}},"required":["key","name","tier"],"additionalProperties":false,"nullable":true},"streak":{"type":"object","properties":{"type":{"type":"string","enum":["on_time","on_the_way","five_star","no_checkin","team_on_time"]},"length":{"type":"number"}},"required":["type","length"],"additionalProperties":false,"nullable":true,"description":"For kind streak and team_streak: which streak and the milestone"}},"required":["id","kind","at","personId","person","title","badge","streak"],"additionalProperties":false}},"companyStreak":{"type":"object","properties":{"current":{"type":"number","description":"Days in a row the company was at or above the target"},"best":{"type":"number"},"startedOn":{"type":"string","nullable":true},"target":{"type":"number","description":"The on-time goal, else 90"},"targetIsGoal":{"type":"boolean"},"atRisk":{"type":"boolean","description":"Today's on-time rate so far is under the target (today can't end the streak until it's over)"},"nextMilestone":{"type":"number"},"today":{"type":"object","properties":{"arrivals":{"type":"number"},"onTimeRate":{"type":"number","nullable":true}},"required":["arrivals","onTimeRate"],"additionalProperties":false}},"required":["current","best","startedOn","target","targetIsGoal","atRisk","nextMilestone","today"],"additionalProperties":false},"longestStreaks":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"name":{"type":"string"},"current":{"type":"number"},"startedOn":{"type":"string","nullable":true}},"required":["personId","name","current","startedOn"],"additionalProperties":false},"maxItems":3,"description":"The three longest on-time streaks running now (at least 1)"},"techs":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"name":{"type":"string"},"rookie":{"type":"object","properties":{"daysLeft":{"type":"number"}},"required":["daysLeft"],"additionalProperties":false,"nullable":true,"description":"Set during a tech's first 30 days (from their first visit)"},"streak":{"type":"object","properties":{"current":{"type":"number","description":"On-time arrivals in a row right now"},"best":{"type":"number"},"startedOn":{"type":"string","nullable":true,"description":"The local day the current streak began; null when it is 0"},"atRisk":{"type":"boolean","description":"The next stop starts within 30 minutes (or already has) and isn't arrived at yet: a late arrival would end the streak"},"paused":{"type":"boolean","description":"Today is a day off or time off: the streak waits, it does not break"}},"required":["current","best","startedOn","atRisk","paused"],"additionalProperties":false},"badges":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"tier":{"type":"string","enum":["bronze","silver","gold","platinum","diamond"]}},"required":["key","name","tier"],"additionalProperties":false}},"week":{"type":"object","properties":{"arrivals":{"type":"number"},"onTime":{"type":"number"},"onTimeRate":{"type":"number","nullable":true},"done":{"type":"number"},"fiveStar":{"type":"number"}},"required":["arrivals","onTime","onTimeRate","done","fiveStar"],"additionalProperties":false}},"required":["personId","name","rookie","streak","badges","week"],"additionalProperties":false}},"badges":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"thresholds":{"type":"object","properties":{"bronze":{"type":"number"},"silver":{"type":"number"},"gold":{"type":"number"},"platinum":{"type":"number"},"diamond":{"type":"number"}},"required":["bronze","silver","gold"],"additionalProperties":false}},"required":["key","name","description","thresholds"],"additionalProperties":false}}},"required":["week","settings","goals","feed","companyStreak","longestStreaks","techs","badges"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/wins/settings":{"patch":{"summary":"Turn the leaderboard on or off and set weekly goals (owner only)","tags":["Wins"],"description":"Send only what changes. A goal set to null is removed. The leaderboard is off by default; when on, techs see the top 3 and their own row, and each tech can hide themselves.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"leaderboard":{"type":"boolean"},"goals":{"type":"object","properties":{"onTimeRate":{"type":"number","minimum":0,"maximum":100,"nullable":true},"firstTimeFixRate":{"type":"number","minimum":0,"maximum":100,"nullable":true},"reviewsPerWeek":{"type":"integer","minimum":0,"maximum":10000,"nullable":true},"connectSentPerWeek":{"type":"integer","minimum":0,"maximum":10000,"nullable":true}},"additionalProperties":false}},"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"leaderboard":{"type":"boolean","description":"The company leaderboard is on (default off). Techs can still hide themselves."},"goals":{"type":"object","properties":{"onTimeRate":{"type":"number","nullable":true,"description":"Percent of arrivals on time (the On-time arrival KPI rule)"},"firstTimeFixRate":{"type":"number","nullable":true,"description":"Percent of finished jobs with no return visit for the same customer and service within 14 days"},"reviewsPerWeek":{"type":"number","nullable":true,"description":"Reviews customers leave in a week"},"connectSentPerWeek":{"type":"number","nullable":true,"description":"CrewConnect jobs sent to partners, by start day"}},"required":["onTimeRate","firstTimeFixRate","reviewsPerWeek","connectSentPerWeek"],"additionalProperties":false}},"required":["leaderboard","goals"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/wins/leaderboard":{"get":{"summary":"The week's leaderboard: top 3 only","tags":["Wins"],"description":"Ranked on on-time rate (at least 5 arrivals in the week) and on five-star reviews. Only the top 3 come back, never the rest. Techs who chose to hide aren't ranked. `enabled` false (and no boards) until an owner turns the leaderboard on.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"week","required":false,"description":"Any day of the week (Monday to Sunday, company time); default this week"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"week":{"type":"object","properties":{"from":{"type":"string","description":"Monday"},"to":{"type":"string","description":"Sunday"}},"required":["from","to"],"additionalProperties":false},"boards":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","enum":["on_time","five_star"]},"name":{"type":"string"},"unit":{"type":"string","enum":["pct","count"]},"top":{"type":"array","items":{"type":"object","properties":{"rank":{"type":"number","nullable":true,"description":"1-3; null for your own row when you are not in the top 3"},"personId":{"type":"number"},"name":{"type":"string"},"value":{"type":"number"},"detail":{"type":"string"},"rookie":{"type":"boolean"}},"required":["rank","personId","name","value","detail","rookie"],"additionalProperties":false},"maxItems":3,"description":"The top 3 only; the rest of the board is never returned"},"you":{"type":"object","properties":{"rank":{"type":"number","nullable":true,"description":"1-3; null for your own row when you are not in the top 3"},"personId":{"type":"number"},"name":{"type":"string"},"value":{"type":"number"},"detail":{"type":"string"},"rookie":{"type":"boolean"},"qualified":{"type":"boolean","description":"On time needs 5 arrivals in the week; five-star needs one review"},"hidden":{"type":"boolean"}},"required":["rank","personId","name","value","detail","rookie","qualified","hidden"],"additionalProperties":false,"nullable":true,"description":"The caller's own row (techs only)"}},"required":["key","name","unit","top","you"],"additionalProperties":false}}},"required":["enabled","week","boards"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/me/wins":{"get":{"summary":"My wins: streak, this week, personal best, badges, celebrations and the leaderboard","tags":["Field app","Wins"],"description":"All from your own taps and reviews. Streaks lead: `streaks` has on-time arrivals in a row (no more than 15 minutes after the start), working days in a row with an on-the-way tap before every stop, five-star reviews in a row, and jobs in a row finished inside the planned time, each with when it started, whether it is at risk right now and the next milestone. A miss starts a streak again; days off and time off pause it. `streak` is the on-time one. `celebrations` are new badges and personal bests the app hasn't shown yet: show them, then `POST /v1/tech/me/wins/celebrated`. The leaderboard (when your company has it on) shows the top 3 and your own row.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"rookie":{"type":"object","properties":{"daysLeft":{"type":"number"}},"required":["daysLeft"],"additionalProperties":false,"nullable":true,"description":"Set during a tech's first 30 days (from their first visit)"},"streak":{"type":"object","properties":{"current":{"type":"number","description":"On-time arrivals in a row right now"},"best":{"type":"number"},"startedOn":{"type":"string","nullable":true,"description":"The local day the current streak began; null when it is 0"},"atRisk":{"type":"boolean","description":"The next stop starts within 30 minutes (or already has) and isn't arrived at yet: a late arrival would end the streak"},"paused":{"type":"boolean","description":"Today is a day off or time off: the streak waits, it does not break"}},"required":["current","best","startedOn","atRisk","paused"],"additionalProperties":false},"streaks":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["on_time","on_the_way","five_star","no_checkin"]},"name":{"type":"string"},"unit":{"type":"string","enum":["arrivals","days","reviews","jobs"]},"description":{"type":"string"},"current":{"type":"number"},"best":{"type":"number"},"startedOn":{"type":"string","nullable":true},"atRisk":{"type":"boolean"},"paused":{"type":"boolean"},"nextMilestone":{"type":"number","description":"5, 10, 25, 50, 100, then every 100"}},"required":["type","name","unit","description","current","best","startedOn","atRisk","paused","nextMilestone"],"additionalProperties":false},"description":"Every streak, on time first"},"week":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"arrivals":{"type":"number"},"onTime":{"type":"number"},"onTimeRate":{"type":"number","nullable":true},"done":{"type":"number"},"fiveStar":{"type":"number"},"drivesMeasured":{"type":"number"},"driveMinutesSaved":{"type":"number","description":"Planned drive minutes minus real ones (negative: over plan)"},"bestDayJobs":{"type":"number"}},"required":["from","to","arrivals","onTime","onTimeRate","done","fiveStar","drivesMeasured","driveMinutesSaved","bestDayJobs"],"additionalProperties":false},"personalBest":{"type":"object","properties":{"jobs":{"type":"number"},"date":{"type":"string"}},"required":["jobs","date"],"additionalProperties":false,"nullable":true,"description":"Most jobs finished in one day"},"totals":{"type":"object","properties":{"arrivals":{"type":"number"},"onTimeRate":{"type":"number","nullable":true},"early":{"type":"number"},"done":{"type":"number"},"fiveStar":{"type":"number"},"reviews":{"type":"number"},"firstTimeFixRate":{"type":"number","nullable":true},"ranLong":{"type":"number"},"headsUp":{"type":"number"},"checkinHonestyRate":{"type":"number","nullable":true,"description":"Of jobs that ran long, the percent where you checked in before the planned finish"}},"required":["arrivals","onTimeRate","early","done","fiveStar","reviews","firstTimeFixRate","ranLong","headsUp","checkinHonestyRate"],"additionalProperties":false},"badges":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"tier":{"type":"string","enum":["bronze","silver","gold","platinum","diamond"],"nullable":true},"value":{"type":"number"},"next":{"type":"object","properties":{"tier":{"type":"string","enum":["bronze","silver","gold","platinum","diamond"]},"threshold":{"type":"number"}},"required":["tier","threshold"],"additionalProperties":false,"nullable":true},"earnedAt":{"type":"string","nullable":true}},"required":["key","name","description","tier","value","next","earnedAt"],"additionalProperties":false}},"celebrations":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"kind":{"type":"string","enum":["badge","best_day","streak"]},"title":{"type":"string"},"badge":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"tier":{"type":"string","enum":["bronze","silver","gold","platinum","diamond"]}},"required":["key","name","tier"],"additionalProperties":false,"nullable":true},"streak":{"type":"object","properties":{"type":{"type":"string","enum":["on_time","on_the_way","five_star","no_checkin"]},"length":{"type":"number"}},"required":["type","length"],"additionalProperties":false,"nullable":true},"at":{"type":"string"}},"required":["id","kind","title","badge","streak","at"],"additionalProperties":false}},"leaderboard":{"type":"object","properties":{"enabled":{"type":"boolean"},"hidden":{"type":"boolean"},"boards":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","enum":["on_time","five_star"]},"name":{"type":"string"},"unit":{"type":"string","enum":["pct","count"]},"top":{"type":"array","items":{"type":"object","properties":{"rank":{"type":"number","nullable":true,"description":"1-3; null for your own row when you are not in the top 3"},"personId":{"type":"number"},"name":{"type":"string"},"value":{"type":"number"},"detail":{"type":"string"},"rookie":{"type":"boolean"}},"required":["rank","personId","name","value","detail","rookie"],"additionalProperties":false},"maxItems":3,"description":"The top 3 only; the rest of the board is never returned"},"you":{"type":"object","properties":{"rank":{"type":"number","nullable":true,"description":"1-3; null for your own row when you are not in the top 3"},"personId":{"type":"number"},"name":{"type":"string"},"value":{"type":"number"},"detail":{"type":"string"},"rookie":{"type":"boolean"},"qualified":{"type":"boolean","description":"On time needs 5 arrivals in the week; five-star needs one review"},"hidden":{"type":"boolean"}},"required":["rank","personId","name","value","detail","rookie","qualified","hidden"],"additionalProperties":false,"nullable":true,"description":"The caller's own row (techs only)"}},"required":["key","name","unit","top","you"],"additionalProperties":false}}},"required":["enabled","hidden","boards"],"additionalProperties":false}},"required":["rookie","streak","streaks","week","personalBest","totals","badges","celebrations","leaderboard"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/me/wins/settings":{"patch":{"summary":"Hide me from (or show me on) the company leaderboard","tags":["Field app","Wins"],"description":"Hidden: you aren't ranked or shown to anyone else; you still see your own numbers.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"hideFromLeaderboard":{"type":"boolean"}},"required":["hideFromLeaderboard"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"hideFromLeaderboard":{"type":"boolean"}},"required":["hideFromLeaderboard"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/me/wins/celebrated":{"post":{"summary":"Mark celebrations as shown","tags":["Field app","Wins"],"description":"Send the ids from `celebrations` once the app has shown them; they are not returned again. Ids that are not yours are ignored.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ids":{"type":"array","items":{"type":"integer"},"minItems":1,"maxItems":50}},"required":["ids"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"204":{"description":"Default Response","content":{"application/json":{"schema":{"enum":["null"],"nullable":true}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/invoice":{"post":{"summary":"Draft an invoice for a job","tags":["Invoices"],"description":"Lines default to the service at its list price plus any parts the tech logged. Any paid deposit comes off the balance. Tax is in basis points (825 = 8.25%). A job has one invoice at a time: 409 invoice_exists (with invoiceId) while it has a draft, sent or paid one. Void that one first to draft a new one; GET /v1/office/jobs/{id}/invoice returns it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"lines":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":200},"qty":{"type":"number","exclusiveMinimum":true,"minimum":0,"maximum":10000},"unitCents":{"type":"integer","minimum":0}},"required":["description","qty","unitCents"],"additionalProperties":false},"minItems":1,"maxItems":100},"taxRateBp":{"type":"integer","minimum":0,"maximum":5000,"default":0}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"number":{"type":"number"},"jobId":{"type":"number"},"status":{"type":"string","enum":["draft","sent","paid","void"]},"lines":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":200},"qty":{"type":"number","exclusiveMinimum":true,"minimum":0,"maximum":10000},"unitCents":{"type":"integer","minimum":0}},"required":["description","qty","unitCents"],"additionalProperties":false}},"subtotalCents":{"type":"number"},"taxCents":{"type":"number"},"totalCents":{"type":"number"},"depositAppliedCents":{"type":"number"},"balanceCents":{"type":"number"},"currency":{"type":"string"}},"required":["id","number","jobId","status","lines","subtotalCents","taxCents","totalCents","depositAppliedCents","balanceCents","currency"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"invoiceId":{"type":"number","description":"invoice_exists: the invoice the job already has"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"A job's current invoice (draft, sent or paid)","tags":["Invoices"],"description":"404 when the job has none, or only void ones.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"number":{"type":"number"},"jobId":{"type":"number"},"status":{"type":"string","enum":["draft","sent","paid","void"]},"lines":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":200},"qty":{"type":"number","exclusiveMinimum":true,"minimum":0,"maximum":10000},"unitCents":{"type":"integer","minimum":0}},"required":["description","qty","unitCents"],"additionalProperties":false}},"subtotalCents":{"type":"number"},"taxCents":{"type":"number"},"totalCents":{"type":"number"},"depositAppliedCents":{"type":"number"},"balanceCents":{"type":"number"},"currency":{"type":"string"},"customer":{"type":"string","nullable":true,"description":"The job's customer"},"service":{"type":"string","description":"The job's service"}},"required":["id","number","jobId","status","lines","subtotalCents","taxCents","totalCents","depositAppliedCents","balanceCents","currency","customer","service"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/invoices/{id}/send":{"post":{"summary":"Send an invoice: the customer gets a text with a link to view and pay","tags":["Invoices"],"description":"The link shows the invoice and, when the company takes payments online (a connected Stripe account), a card payment for the balance. Without one the invoice is still sent (payOnline false): the link shows the balance and asks the customer to pay the office. 502 payments_unavailable only when the provider fails for a company that does take payments online.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"number":{"type":"number"},"jobId":{"type":"number"},"status":{"type":"string","enum":["draft","sent","paid","void"]},"lines":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":200},"qty":{"type":"number","exclusiveMinimum":true,"minimum":0,"maximum":10000},"unitCents":{"type":"integer","minimum":0}},"required":["description","qty","unitCents"],"additionalProperties":false}},"subtotalCents":{"type":"number"},"taxCents":{"type":"number"},"totalCents":{"type":"number"},"depositAppliedCents":{"type":"number"},"balanceCents":{"type":"number"},"currency":{"type":"string"},"link":{"type":"string"},"payOnline":{"type":"boolean","description":"The link offers an online card payment. false: no payment account is connected yet, so the customer pays the office"},"texted":{"type":"boolean","description":"The invoice text to the customer was queued. false: the job's texts are off (notify false, the company's homeownerMessaging or the booking platform's grant) or its phone can't be texted; the invoice is still sent (share the link yourself)"}},"required":["id","number","jobId","status","lines","subtotalCents","taxCents","totalCents","depositAppliedCents","balanceCents","currency","link","payOnline","texted"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/invoices":{"get":{"summary":"Invoices, newest first, with customer and service","tags":["Invoices"],"parameters":[{"schema":{"type":"string","enum":["draft","sent","paid","void"]},"in":"query","name":"status","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"invoices":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"number":{"type":"number"},"jobId":{"type":"number"},"status":{"type":"string","enum":["draft","sent","paid","void"]},"lines":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":200},"qty":{"type":"number","exclusiveMinimum":true,"minimum":0,"maximum":10000},"unitCents":{"type":"integer","minimum":0}},"required":["description","qty","unitCents"],"additionalProperties":false}},"subtotalCents":{"type":"number"},"taxCents":{"type":"number"},"totalCents":{"type":"number"},"depositAppliedCents":{"type":"number"},"balanceCents":{"type":"number"},"currency":{"type":"string"},"customer":{"type":"string","nullable":true,"description":"The job's customer"},"service":{"type":"string","description":"The job's service"}},"required":["id","number","jobId","status","lines","subtotalCents","taxCents","totalCents","depositAppliedCents","balanceCents","currency","customer","service"],"additionalProperties":false}}},"required":["invoices"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/invoices/{id}/void":{"post":{"summary":"Void an invoice","tags":["Invoices"],"description":"A draft or sent invoice. Its balance becomes 0 (the deposit applied stays as it was), the customer's link then shows it as void with nothing to pay, and its pending payment is dropped. Voiding a void invoice changes nothing; 409 not_voidable for a paid one (refund it instead). Draft a new invoice for the job if you need one.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"number":{"type":"number"},"jobId":{"type":"number"},"status":{"type":"string","enum":["draft","sent","paid","void"]},"lines":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":200},"qty":{"type":"number","exclusiveMinimum":true,"minimum":0,"maximum":10000},"unitCents":{"type":"integer","minimum":0}},"required":["description","qty","unitCents"],"additionalProperties":false}},"subtotalCents":{"type":"number"},"taxCents":{"type":"number"},"totalCents":{"type":"number"},"depositAppliedCents":{"type":"number"},"balanceCents":{"type":"number"},"currency":{"type":"string"}},"required":["id","number","jobId","status","lines","subtotalCents","taxCents","totalCents","depositAppliedCents","balanceCents","currency"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/payouts":{"get":{"summary":"What you owe partners, and what partners owe you","tags":["CrewConnect"],"description":"Created when a CrewConnect job is marked done, on the connection’s agreed terms. A standard payout is free; `instantFeeCents` is what an instant payout would cost (1%).","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"payouts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"direction":{"type":"string","enum":["owed","incoming"]},"counterparty":{"type":"string"},"jobId":{"type":"number","nullable":true},"amountCents":{"type":"number"},"currency":{"type":"string"},"status":{"type":"string"},"dueAt":{"type":"string","nullable":true},"instantFeeCents":{"type":"number"}},"required":["id","direction","counterparty","jobId","amountCents","currency","status","dueAt","instantFeeCents"],"additionalProperties":false}}},"required":["payouts"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/reviews":{"get":{"summary":"Customer reviews, newest first","tags":["Reviews"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"average":{"type":"number","nullable":true},"count":{"type":"number"},"reviews":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"rating":{"type":"number"},"comment":{"type":"string","nullable":true},"customer":{"type":"string","nullable":true},"at":{"type":"string"}},"required":["jobId","rating","comment","customer","at"],"additionalProperties":false}}},"required":["average","count","reviews"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/billing":{"get":{"summary":"Your plan and its bill: this month (or `period`), itemized","tags":["Billing"],"description":"A job counts once, in the month it was booked, on the plan of the company that originated it: a CrewConnect job counts for the sender, never for you when you receive it. Cancelled jobs aren't counted; a moved job is one job; a multi-day install is one job. Lines: the base price, overage (jobs over x the rate), the best-price cap credit (a month never costs more than the next tier's monthly price), the waived first overage month, any upgrade proration, and the free-trial credit. `fees` sums the payments through CrewLink in the period: all-in fees (card 2.9% + 30c, ACH 1% capped at $15), the processing part and CrewLink's share. `period` (a date) shows the month containing it. `usage` keeps the older shape.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"period","required":false,"description":"Any date in the month to show (default today)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"monthlyCents":{"type":"number"},"annualCents":{"type":"number"},"includedJobs":{"type":"number"},"overageCents":{"type":"number"},"perJobCents":{"type":"number","description":"Monthly price / included jobs, in cents"},"capAt":{"type":"number","nullable":true,"description":"Jobs a month at which the best-price cap starts"},"breakeven":{"type":"number","nullable":true,"description":"Jobs a month at which the next tier is cheaper"},"next":{"type":"string","nullable":true},"selfServe":{"type":"boolean","description":"false: Enterprise, book a call"},"for":{"type":"string","enum":["individual","any"]},"maxUsers":{"type":"number","nullable":true},"multiCrewBoard":{"type":"boolean"},"who":{"type":"string"}},"required":["code","name","monthlyCents","annualCents","includedJobs","overageCents","perJobCents","capAt","breakeven","next","selfServe","for","maxUsers","multiCrewBoard","who"],"additionalProperties":false},"interval":{"type":"string","enum":["month","year"]},"period":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"],"additionalProperties":false},"usage":{"type":"object","properties":{"jobs":{"type":"number"},"included":{"type":"number"},"over":{"type":"number"},"overageCents":{"type":"number"},"platformFeeCents":{"type":"number"},"subscriptionCents":{"type":"number"},"totalCents":{"type":"number"}},"required":["jobs","included","over","overageCents","platformFeeCents","subscriptionCents","totalCents"],"additionalProperties":false},"statement":{"type":"object","properties":{"plan":{"type":"string"},"interval":{"type":"string","enum":["month","year"]},"period":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"],"additionalProperties":false},"trial":{"type":"boolean","description":"The free trial window: nothing to pay"},"closed":{"type":"boolean","description":"The month ended and the bill is frozen"},"jobs":{"type":"number","description":"Jobs counted"},"cancelledCredited":{"type":"number","description":"Booked in the period and cancelled: not counted"},"receivedNotCounted":{"type":"number","description":"CrewConnect jobs you received: they count for the sender"},"allowance":{"type":"number"},"lines":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["base","overage","cap_credit","waived_credit","trial_credit","adjustment"]},"description":{"type":"string"},"qty":{"type":"number"},"unitCents":{"type":"number"},"cents":{"type":"number"}},"required":["kind","description","cents"],"additionalProperties":false}},"totalCents":{"type":"number"},"capped":{"type":"boolean"},"waived":{"type":"boolean"},"note":{"type":"string","nullable":true},"fees":{"type":"object","properties":{"card":{"type":"object","properties":{"count":{"type":"number"},"amountCents":{"type":"number"},"feeCents":{"type":"number","description":"All-in fees paid"},"processingFeeCents":{"type":"number"},"platformFeeCents":{"type":"number","description":"CrewLink's share"}},"required":["count","amountCents","feeCents","processingFeeCents","platformFeeCents"],"additionalProperties":false},"ach":{"type":"object","properties":{"count":{"type":"number"},"amountCents":{"type":"number"},"feeCents":{"type":"number","description":"All-in fees paid"},"processingFeeCents":{"type":"number"},"platformFeeCents":{"type":"number","description":"CrewLink's share"}},"required":["count","amountCents","feeCents","processingFeeCents","platformFeeCents"],"additionalProperties":false},"total":{"type":"object","properties":{"count":{"type":"number"},"amountCents":{"type":"number"},"feeCents":{"type":"number","description":"All-in fees paid"},"processingFeeCents":{"type":"number"},"platformFeeCents":{"type":"number","description":"CrewLink's share"}},"required":["count","amountCents","feeCents","processingFeeCents","platformFeeCents"],"additionalProperties":false}},"required":["card","ach","total"],"additionalProperties":false,"description":"Payments through CrewLink in the period, and their fees"},"jobsCsv":{"type":"string","description":"The counted jobs as CSV"}},"required":["plan","interval","period","trial","closed","jobs","cancelledCredited","receivedNotCounted","allowance","lines","totalCents","capped","waived","note","fees","jobsCsv"],"additionalProperties":false},"subscription":{"type":"object","properties":{"status":{"type":"string","description":"trialing | active | past_due | canceled | incomplete. Scheduling is never blocked."},"trialEndsAt":{"type":"string"},"inTrial":{"type":"boolean"},"hasCard":{"type":"boolean","description":"A Stripe customer with a card on file"},"pending":{"type":"object","properties":{"plan":{"type":"string"},"interval":{"type":"string","enum":["month","year"]},"from":{"type":"string"}},"required":["plan","interval","from"],"additionalProperties":false,"nullable":true,"description":"A downgrade or interval change waiting for the next period"},"spendAlertCents":{"type":"number","nullable":true},"overageWaivedPeriod":{"type":"string","nullable":true}},"required":["status","trialEndsAt","inTrial","hasCard","pending","spendAlertCents","overageWaivedPeriod"],"additionalProperties":false},"plans":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"monthlyCents":{"type":"number"},"annualCents":{"type":"number"},"includedJobs":{"type":"number"},"overageCents":{"type":"number"},"perJobCents":{"type":"number","description":"Monthly price / included jobs, in cents"},"capAt":{"type":"number","nullable":true,"description":"Jobs a month at which the best-price cap starts"},"breakeven":{"type":"number","nullable":true,"description":"Jobs a month at which the next tier is cheaper"},"next":{"type":"string","nullable":true},"selfServe":{"type":"boolean","description":"false: Enterprise, book a call"},"for":{"type":"string","enum":["individual","any"]},"maxUsers":{"type":"number","nullable":true},"multiCrewBoard":{"type":"boolean"},"who":{"type":"string"}},"required":["code","name","monthlyCents","annualCents","includedJobs","overageCents","perJobCents","capAt","breakeven","next","selfServe","for","maxUsers","multiCrewBoard","who"],"additionalProperties":false},"description":"The plans this account can be on"},"rates":{"type":"object","properties":{"card":{"type":"object","properties":{"rateBp":{"type":"number"},"fixedCents":{"type":"number"}},"required":["rateBp","fixedCents"],"additionalProperties":false,"description":"All-in card rate: 2.9% + 30c, CrewLink 1% inside it"},"ach":{"type":"object","properties":{"rateBp":{"type":"number"},"capCents":{"type":"number"}},"required":["rateBp","capCents"],"additionalProperties":false,"description":"All-in ACH rate: 1%, capped at $15 a payment"},"platformBp":{"type":"number"},"payouts":{"type":"object","properties":{"standardBp":{"type":"number"},"instantBp":{"type":"number"}},"required":["standardBp","instantBp"],"additionalProperties":false,"description":"CrewConnect payouts: standard free, instant 1%"}},"required":["card","ach","platformBp","payouts"],"additionalProperties":false},"enterpriseCallUrl":{"type":"string"}},"required":["plan","interval","period","usage","statement","subscription","plans","rates","enterpriseCallUrl"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/billing/usage":{"get":{"summary":"The live usage meter","tags":["Billing"],"description":"Jobs used of the included jobs (the year's pool on an annual plan), days left, the linear forecast, where the best-price cap starts, the next tier and its breakeven, and a nudge: up when 2 of the last 3 months passed the breakeven or the forecast passes 85% of it; down after 3 months under 50% of the allowance. contactSales above 15,000 jobs a month. `alerts` are the 75% / 90% / 100% / forecast / spend alerts already sent this period (each fires once).","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string"},"interval":{"type":"string","enum":["month","year"]},"status":{"type":"string"},"period":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"],"additionalProperties":false},"included":{"type":"number"},"used":{"type":"number"},"overage":{"type":"number","description":"Jobs over the allowance so far"},"daysLeft":{"type":"number"},"onPaceFor":{"type":"number"},"capAt":{"type":"number","nullable":true},"nextTier":{"type":"string","nullable":true},"breakeven":{"type":"number","nullable":true},"nudge":{"type":"string","enum":["up","down"],"nullable":true},"nudgeTo":{"type":"string","nullable":true},"contactSales":{"type":"boolean"},"trial":{"type":"object","properties":{"endsAt":{"type":"string"},"daysLeft":{"type":"number"}},"required":["endsAt","daysLeft"],"additionalProperties":false,"nullable":true},"billSoFarCents":{"type":"number"},"projectedCents":{"type":"number"},"spendAlertCents":{"type":"number","nullable":true},"text":{"type":"string","description":"\"612 of 800 jobs · 19 days left · on pace for 940\""},"alerts":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string"},"message":{"type":"string"},"channel":{"type":"string"},"at":{"type":"string"}},"required":["kind","message","channel","at"],"additionalProperties":false},"description":"Alerts sent this period (also the in-app notices)"}},"required":["plan","interval","status","period","included","used","overage","daysLeft","onPaceFor","capAt","nextTier","breakeven","nudge","nudgeTo","contactSales","trial","billSoFarCents","projectedCents","spendAlertCents","text","alerts"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/plan":{"patch":{"summary":"Choose your plan, billing interval and spend alert (owner only)","tags":["Billing"],"description":"Upgrades (a higher tier) apply now: the price difference for the days left in the period is added to this month's bill (simple daily proration) and, with a card on file, the Stripe subscription moves now. Downgrades and interval changes apply from the next period (`effective: next_period`, `effectiveFrom`). During the free trial everything applies at once. Enterprise: 422 contact_sales with `url` (book a call). Independent and Connect are for individual (1099) accounts. spendAlertCents: we tell you when the month's projected bill passes it (null turns it off).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["connect","independent","crew","shop","fleet","enterprise"]},"interval":{"type":"string","enum":["month","year"]},"spendAlertCents":{"type":"integer","minimum":0,"maximum":10000000,"nullable":true}},"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string"},"interval":{"type":"string","enum":["month","year"]},"effective":{"type":"string","enum":["now","next_period"]},"effectiveFrom":{"type":"string"},"prorationCents":{"type":"number"},"spendAlertCents":{"type":"number","nullable":true}},"required":["plan","interval","effective","effectiveFrom","prorationCents","spendAlertCents"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"url":{"type":"string","description":"contact_sales: where to book a call"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/billing/plan":{"put":{"summary":"Change plan (owner only; older form of PATCH /v1/office/plan)","tags":["Billing"],"description":"Same rules as PATCH /v1/office/plan, keeping the current interval.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string"}},"required":["plan"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string"}},"required":["plan"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"url":{"type":"string","description":"contact_sales: where to book a call"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/exports/plan-usage.csv":{"get":{"summary":"The jobs counted on your plan in a month, as CSV","tags":["Billing"],"description":"The job list behind the bill's overage line: every job counted in the month containing `period` (default this month), in booking order, marked included or over. RFC 4180 CSV with a UTF-8 byte order mark. Company-local times.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"period","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"text/csv; charset=utf-8","content":{"text/csv":{"schema":{"type":"string","description":"text/csv; charset=utf-8"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/billing/portal":{"post":{"summary":"Open the Stripe Customer Portal: card, invoices, cancel (owner only)","tags":["Billing"],"description":"409 no_card when there is no card on file yet: use POST /v1/office/billing/checkout.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/billing/checkout":{"post":{"summary":"Put a card on file and start a paid plan: a Stripe Checkout link (owner only)","tags":["Billing"],"description":"For accounts with no subscription yet (or to replace a cancelled one). Keeps what's left of the 30-day trial. Coming back, the web app calls POST /v1/office/billing/checkout/complete (the Stripe webhook also applies it). Enterprise: 422 contact_sales.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["connect","independent","crew","shop","fleet","enterprise"]},"interval":{"type":"string","enum":["month","year"],"default":"month"}},"required":["plan"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"sessionId":{"type":"string"}},"required":["url","sessionId"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"url":{"type":"string","description":"contact_sales: where to book a call"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/office/billing/checkout/complete":{"post":{"summary":"Back from Checkout: apply it now (the webhook would too)","tags":["Billing"],"description":"409 checkout_incomplete while Stripe has not confirmed the card yet; 404 for a session that is not yours.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string","minLength":5,"maxLength":255}},"required":["sessionId"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string"},"interval":{"type":"string","enum":["month","year"]},"status":{"type":"string"}},"required":["plan","interval","status"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/invoices/{id}":{"get":{"summary":"An invoice, by the link texted to the customer","tags":["Customer"],"description":"A void invoice has balanceCents 0 and payment null: nothing to pay.","parameters":[{"schema":{"type":"string","minLength":10},"in":"query","name":"token","required":true},{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"number":{"type":"number"},"jobId":{"type":"number"},"status":{"type":"string","enum":["draft","sent","paid","void"]},"lines":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string","minLength":1,"maxLength":200},"qty":{"type":"number","exclusiveMinimum":true,"minimum":0,"maximum":10000},"unitCents":{"type":"integer","minimum":0}},"required":["description","qty","unitCents"],"additionalProperties":false}},"subtotalCents":{"type":"number"},"taxCents":{"type":"number"},"totalCents":{"type":"number"},"depositAppliedCents":{"type":"number"},"balanceCents":{"type":"number"},"currency":{"type":"string"},"company":{"type":"string"},"payment":{"type":"object","properties":{"provider":{"type":"string"},"clientSecret":{"type":"string","nullable":true}},"required":["provider","clientSecret"],"additionalProperties":false,"nullable":true}},"required":["id","number","jobId","status","lines","subtotalCents","taxCents","totalCents","depositAppliedCents","balanceCents","currency","company","payment"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/reviews/{jobId}":{"post":{"summary":"Leave a review (link texted when the job is done)","tags":["Customer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":10},"rating":{"type":"integer","minimum":1,"maximum":5},"comment":{"type":"string","maxLength":2000}},"required":["token","rating"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"jobId","required":true}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/plans":{"get":{"summary":"The plans, prices and payment rates (for the sign-up and pricing pages)","tags":["Sign-up"],"description":"Monthly and annual prices (annual = 10 months for 12, jobs pooled across the year), included jobs, overage, where the best-price cap starts, the breakeven for the next tier, and the all-in payment rates. 30-day free trial with a card on file; Enterprise is book-a-call.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"plans":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"},"monthlyCents":{"type":"number"},"annualCents":{"type":"number"},"includedJobs":{"type":"number"},"overageCents":{"type":"number"},"perJobCents":{"type":"number","description":"Monthly price / included jobs, in cents"},"capAt":{"type":"number","nullable":true,"description":"Jobs a month at which the best-price cap starts"},"breakeven":{"type":"number","nullable":true,"description":"Jobs a month at which the next tier is cheaper"},"next":{"type":"string","nullable":true},"selfServe":{"type":"boolean","description":"false: Enterprise, book a call"},"for":{"type":"string","enum":["individual","any"]},"maxUsers":{"type":"number","nullable":true},"multiCrewBoard":{"type":"boolean"},"who":{"type":"string"}},"required":["code","name","monthlyCents","annualCents","includedJobs","overageCents","perJobCents","capAt","breakeven","next","selfServe","for","maxUsers","multiCrewBoard","who"],"additionalProperties":false}},"rates":{"type":"object","properties":{"card":{"type":"object","properties":{"rateBp":{"type":"number"},"fixedCents":{"type":"number"}},"required":["rateBp","fixedCents"],"additionalProperties":false,"description":"All-in card rate: 2.9% + 30c, CrewLink 1% inside it"},"ach":{"type":"object","properties":{"rateBp":{"type":"number"},"capCents":{"type":"number"}},"required":["rateBp","capCents"],"additionalProperties":false,"description":"All-in ACH rate: 1%, capped at $15 a payment"},"platformBp":{"type":"number"},"payouts":{"type":"object","properties":{"standardBp":{"type":"number"},"instantBp":{"type":"number"}},"required":["standardBp","instantBp"],"additionalProperties":false,"description":"CrewConnect payouts: standard free, instant 1%"}},"required":["card","ach","platformBp","payouts"],"additionalProperties":false},"trialDays":{"type":"number"},"contactSalesAbove":{"type":"number"},"enterpriseCallUrl":{"type":"string"}},"required":["plans","rates","trialDays","contactSalesAbove","enterpriseCallUrl"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/signup":{"post":{"summary":"Create a company and its owner, then go to Stripe Checkout","tags":["Sign-up"],"description":"Creates the company (slug from its name), its owner (who signs in with this email) and the chosen plan, then returns a Stripe Checkout link: a subscription with a 30-day free trial and the card collected up front. Stripe sends the owner back to `/welcome?session_id=...`, which calls POST /v1/signup/complete. A Connect seat (kind individual, plan connect) is free: no card, `checkoutUrl` is null and `welcomeUrl` goes straight to /welcome. Enterprise: 422 contact_sales with `url`. Independent and Connect are for individuals (kind individual). 5 a minute per IP (429).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"company":{"type":"string","minLength":2,"maxLength":120,"description":"Company (or your own) name"},"name":{"type":"string","minLength":1,"maxLength":120,"description":"The owner's name"},"email":{"type":"string","format":"email","maxLength":320,"description":"The owner signs in with this"},"timezone":{"type":"string","maxLength":64,"default":"America/Chicago","description":"IANA zone, e.g. America/Chicago"},"country":{"type":"string","pattern":"^[A-Za-z]{2}$","default":"US","description":"ISO 3166-1 alpha-2 of the head office, e.g. US, GB: which numbers get texts and the usual distance units"},"trade":{"type":"string","maxLength":60,"description":"What you do, e.g. hvac, plumbing, roofing, solar"},"kind":{"type":"string","enum":["company","individual"],"default":"company","description":"individual: a 1099 contractor"},"plan":{"type":"string","enum":["connect","independent","crew","shop","fleet","enterprise"]},"interval":{"type":"string","enum":["month","year"],"default":"month"},"website":{"type":"string","maxLength":200,"description":"Leave empty (a trap for bots)"}},"required":["company","name","email","plan"],"additionalProperties":false}}}},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"companyId":{"type":"string","nullable":true},"slug":{"type":"string","nullable":true},"checkoutUrl":{"type":"string","nullable":true},"welcomeUrl":{"type":"string","nullable":true}},"required":["companyId","slug","checkoutUrl","welcomeUrl"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"url":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"502":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/signup/complete":{"post":{"summary":"The /welcome page: confirm the checkout and sign the owner in (once)","tags":["Sign-up"],"description":"Checks the Checkout Session with Stripe (the webhook may not have arrived yet), puts the plan and card on the account, and returns a 30-day session for the owner, once per sign-up and within 24 hours. A welcome email goes to the owner too, so they can sign in later with their email. 409 checkout_incomplete while Stripe hasn't confirmed the card (try again shortly); 409 already_used after the first time (sign in with your email instead); 404 for an unknown session.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sessionId":{"type":"string","minLength":5,"maxLength":255}},"required":["sessionId"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"expiresAt":{"type":"string"},"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string"},"personId":{"type":"number","nullable":true}},"required":["id","name","role","personId"],"additionalProperties":false},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"],"additionalProperties":false},"companies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"role":{"type":"string","description":"Their role there: owner, field_manager, dispatcher or tech"},"lastUsedAt":{"type":"string","nullable":true,"description":"When they last signed in there (ISO instant), or null if never"}},"required":["id","name","slug","role","lastUsedAt"],"additionalProperties":false},"description":"Every company this email can sign in at (an active user at each), the one used most recently first. More than one: ask which, and POST /v1/auth/switch to change"},"plan":{"type":"string"},"trialEndsAt":{"type":"string"}},"required":["token","expiresAt","user","company","companies","plan","trialEndsAt"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/recurring":{"post":{"summary":"Set up recurring work or a membership","tags":["Recurring"],"description":"The scheduler books each visit up to 14 days ahead, within 3 days of its due date, at the open time that adds the least driving, and texts the customer. It keeps the same service-area rule as New job: an address outside every service area is never booked; each visit due goes to the intake queue flagged needs attention \"Out of service area\" instead, with no text to the customer.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"customer":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40},"email":{"type":"string","format":"email"}},"required":["name","phone"],"additionalProperties":false},"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"address":{"type":"string","maxLength":300},"everyMonths":{"type":"integer","minimum":1,"maximum":24},"firstDue":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"membership":{"type":"string","maxLength":100,"description":"e.g. \"Comfort Club\""},"priceCents":{"type":"integer","minimum":0,"description":"Membership price per period"}},"required":["customer","serviceId","lat","lng","everyMonths","firstDue"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"customer":{"type":"string"},"service":{"type":"string"},"everyMonths":{"type":"number"},"nextDue":{"type":"string"},"membership":{"type":"string","nullable":true},"priceCents":{"type":"number","nullable":true},"active":{"type":"boolean"},"address":{"type":"string","nullable":true}},"required":["id","customer","service","everyMonths","nextDue","membership","priceCents","active","address"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"Recurring work and memberships","tags":["Recurring"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"recurring":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"customer":{"type":"string"},"service":{"type":"string"},"everyMonths":{"type":"number"},"nextDue":{"type":"string"},"membership":{"type":"string","nullable":true},"priceCents":{"type":"number","nullable":true},"active":{"type":"boolean"},"address":{"type":"string","nullable":true}},"required":["id","customer","service","everyMonths","nextDue","membership","priceCents","active","address"],"additionalProperties":false}}},"required":["recurring"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/recurring/{id}/stop":{"post":{"summary":"Stop a recurrence (booked visits stay)","tags":["Recurring"],"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"active":{"type":"boolean","enum":[false]}},"required":["id","active"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/recurring/run":{"post":{"summary":"Book every recurring visit due in the next 14 days now","tags":["Recurring"],"description":"The server also runs this every hour. The service-area rule is New job's: a visit whose address no tech's area covers (a branch's area, or the radius of someone without a branch) is never booked. It is filed in the intake queue (status queued, source recurring, intakeReason and needsAttention \"Out of service area\"), the customer is not texted, the due date moves on, and it is listed in `outOfArea`. Place it from Intake (POST /v1/office/jobs/{id}/move with techIds) or stop the series. `unplaced`: in the area, but no open time near the due date yet (tried again next run).","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"booked":{"type":"array","items":{"type":"object","properties":{"recurrenceId":{"type":"string"},"jobId":{"type":"number"},"local":{"type":"string"}},"required":["recurrenceId","jobId","local"],"additionalProperties":false}},"unplaced":{"type":"array","items":{"type":"object","properties":{"recurrenceId":{"type":"string"},"due":{"type":"string"}},"required":["recurrenceId","due"],"additionalProperties":false}},"outOfArea":{"type":"array","items":{"type":"object","properties":{"recurrenceId":{"type":"string"},"jobId":{"type":"number","description":"The queued job, flagged needs attention"},"due":{"type":"string"}},"required":["recurrenceId","jobId","due"],"additionalProperties":false},"description":"Visits outside every service area: queued for the office, not booked, nobody texted"}},"required":["booked","unplaced","outOfArea"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/{slug}/requests":{"post":{"summary":"Ask for service outside the area","tags":["Booking page"],"description":"When availability says outsideArea, the booking page shows no times and offers this instead: the customer picks up to 3 days (morning, afternoon or any) and gets a text that the office will confirm. The office accepts it for a time (optionally with a travel fee), books it on a trip, or declines. 30 requests a minute per IP, or 300 per integration key (X-CrewLink-Key).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40},"email":{"type":"string","format":"email"}},"required":["name","phone"],"additionalProperties":false},"address":{"type":"string","maxLength":300},"notes":{"type":"string","maxLength":2000},"requested":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"part":{"type":"string","enum":["morning","afternoon","any"],"default":"any"}},"required":["date"],"additionalProperties":false},"minItems":1,"maxItems":3,"description":"Days and times that suit the customer, best first"},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200},"description":"Your ids for this job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" } (up to 10). Each (source, id) is unique per company among jobs that aren't cancelled: one already on another live job is refused with 409 duplicate_external_ref (jobId: that job); one whose every job is cancelled is free to reuse (the cancelled job keeps it). Find a job by any of them: GET /v1/office/jobs?externalSource=&externalRef= (the live job first, then the newest)"},"externalRef":{"type":"string","minLength":1,"maxLength":200,"description":"Shorthand for one externalRefs entry: { [externalSource]: externalRef }. Also the job's primary id (externalRef / externalSource on reads)"},"externalSource":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,99}$","description":"Whose id externalRef is. Default: the platform's slug for a platform key, `api` for other office keys, `booking_page` on the public endpoints (which may not use a granted platform's slug)"}},"required":["serviceId","lat","lng","customer","requested"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string"},"in":"path","name":"slug","required":true}],"security":[{},{"integrationKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"number"},"status":{"type":"string","enum":["waiting"]}},"required":["requestId","status"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"jobId":{"type":"number"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/requests":{"get":{"summary":"Requests waiting for an answer (out-of-area and client-filed), with times the engine can do","tags":["Trips"],"description":"For each request: what the customer asked for, how far it is from the nearest branch, and up to 3 workable times on the days they asked for (anyone qualified, ignoring service areas). Each time says the one-way drive to get there (driveMinutes) and the extra driving it adds to the tech's day, out and back (addedDriveMinutes). Accept one as is, or pick another time. Requests filed by a client company say which (`client`) and who filed them (`filedBy`); `client` filters to one client company's requests (its id) or to requests with no client (`none`). People with a team or own scope see only the requests in their scope.","parameters":[{"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","enum":["none"]}]},"in":"query","name":"client","required":false,"description":"A client company id, or none"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"requests":{"type":"array","items":{"type":"object","properties":{"requestId":{"type":"number"},"customer":{"type":"string","nullable":true},"service":{"type":"string"},"address":{"type":"string","nullable":true},"timezone":{"type":"string","description":"IANA zone of the requested dates and the options (the nearest branch's)"},"km":{"type":"number","description":"From the nearest branch, straight line, in km (show it in the company's units)"},"requested":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false}},"options":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}},"driveMinutes":{"type":"number","description":"One-way drive to the job from the tech's previous stop that day, or from home (or the trip base); for a crew, the longest. A rough road estimate"},"addedDriveMinutes":{"type":"number","description":"Extra driving that day: how much longer their whole day of driving gets, out and back, compared with not taking the job"}},"required":["start","local","techs","driveMinutes","addedDriveMinutes"],"additionalProperties":false}},"createdAt":{"type":"string"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company that filed it"},"filedBy":{"type":"string","nullable":true,"description":"\"Filed by Ann Lee · Globex Solar\""},"source":{"type":"string","nullable":true,"description":"booking_page, client, api, office"},"notes":{"type":"string","nullable":true}},"required":["requestId","customer","service","address","timezone","km","requested","options","createdAt","client","filedBy","source","notes"],"additionalProperties":false}}},"required":["requests"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"post":{"summary":"File a request for a visit (out-of-area or \"request a visit\"), for one of your client companies if you like","tags":["Trips"],"description":"The office's (and a platform's) version of POST /v1/public/{slug}/requests: the same body, plus clientCompanyId (one of your client companies, active: 404 otherwise, as on POST /v1/office/jobs) so the request is that client's, and your own ids for it (externalRefs, or externalRef with externalSource; 409 duplicate_external_ref when one is taken, with the jobId that has it). The request waits for the office (status waiting) with the days the customer picked; the customer gets the request-received text unless the job's homeowner texts are off (notify: false, the company's setting or the platform's grant). The client company sees it in its own requests (GET /v1/connect/requests, pending) like one it filed; request.created carries client and filedBy. Accept it with POST /v1/office/requests/{id}/accept, or place it on a trip.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40},"email":{"type":"string","format":"email"}},"required":["name","phone"],"additionalProperties":false},"address":{"type":"string","maxLength":300},"notes":{"type":"string","maxLength":2000},"requested":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"part":{"type":"string","enum":["morning","afternoon","any"],"default":"any"}},"required":["date"],"additionalProperties":false},"minItems":1,"maxItems":3,"description":"Days and times that suit the customer, best first"},"clientCompanyId":{"type":"string","format":"uuid","description":"The client company this request is for (one of yours, active): it sees it like one it filed"},"notify":{"type":"boolean","description":"false: the homeowner gets no text or email about this job, ever (stored on the job)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string","minLength":1,"maxLength":200},"description":"Your ids for this job, by source: { \"acme\": \"appt_88213\", \"crewlink-v1\": \"J-9912\" } (up to 10). Each (source, id) is unique per company among jobs that aren't cancelled: one already on another live job is refused with 409 duplicate_external_ref (jobId: that job); one whose every job is cancelled is free to reuse (the cancelled job keeps it). Find a job by any of them: GET /v1/office/jobs?externalSource=&externalRef= (the live job first, then the newest)"},"externalRef":{"type":"string","minLength":1,"maxLength":200,"description":"Shorthand for one externalRefs entry: { [externalSource]: externalRef }. Also the job's primary id (externalRef / externalSource on reads)"},"externalSource":{"type":"string","pattern":"^[A-Za-z0-9][A-Za-z0-9_.:-]{0,99}$","description":"Whose id externalRef is. Default: the platform's slug for a platform key, `api` for other office keys, `booking_page` on the public endpoints (which may not use a granted platform's slug)"}},"required":["serviceId","lat","lng","customer","requested"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"number"},"status":{"type":"string","enum":["waiting"]},"timezone":{"type":"string","description":"IANA zone of the requested days"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true},"externalRefs":{"type":"object","additionalProperties":{"type":"string"}}},"required":["requestId","status","timezone","client","externalRefs"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"jobId":{"type":"number"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/requests/{id}/accept":{"post":{"summary":"Accept an out-of-area request for a time, optionally with a travel fee","tags":["Trips"],"description":"Booked under the same locks as any job, with anyone qualified (service areas don't apply); techIds are preferred and go if they fit. The fee goes on the invoice and in the customer's confirmation text. 409 when that time can't be booked, with `earliest` (the earliest workable time that day, or null) and a message that says why: cannot_reach when no one qualified can get there by then (for example, a 3-hour drive before an 11:00 start would mean leaving before their day starts); slot_taken with a `reason` (on_call_reserve, outside_hours, time_off, no_skills) when everyone qualified is ruled out by those. GET /v1/office/requests lists workable times for each request.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"start":{"type":"string","format":"date-time","description":"The instant (ISO 8601 with offset). Send this or local"},"local":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$","description":"Wall clock where the job is, 'YYYY-MM-DDTHH:MM', read in the zone of the branch serving it (no need to know that zone). Send this or start"},"techIds":{"type":"array","items":{"type":"integer"},"minItems":1,"description":"Preferred people: they go if they fit, otherwise CrewLink picks"},"tripFeeCents":{"type":"integer","minimum":0,"maximum":1000000}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"local":{"type":"string"},"timezone":{"type":"string"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}},"tripFeeCents":{"type":"number","nullable":true}},"required":["jobId","local","timezone","techs","tripFeeCents"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"reason":{"type":"string","enum":["on_call_reserve","outside_hours","time_off","no_skills","not_on_team","doesnt_fit"],"description":"on_call_reserve: held for same-day emergencies; outside_hours: outside the tech's working hours; time_off: a block on their calendar; no_skills / not_on_team: they can't do this job (not_on_team also when they are a test tech and the job isn't for a test client, or the reverse); doesnt_fit: another job or the drive is in the way"},"personId":{"type":"number","nullable":true,"description":"The tech the reason is about, when it is about one"},"earliest":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}}},"required":["start","local","techs"],"additionalProperties":false,"nullable":true,"description":"The earliest workable time that day (anyone qualified), or null when nothing works that day"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/requests/{id}/decline":{"post":{"summary":"Decline an out-of-area request (the customer gets a polite text)","tags":["Trips"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","maxLength":300,"description":"Added to the text, e.g. a referral"}},"additionalProperties":false,"default":{}}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"requestId":{"type":"number"},"status":{"type":"string","enum":["declined"]}},"required":["requestId","status"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/trips/suggestions":{"get":{"summary":"Trips the engine suggests for waiting out-of-area requests","tags":["Trips"],"description":"Costs are per trip: hotel and per diem per night, mileage per km (cents). A job is valued at its service list price. Only whole free days are used. Each stop names the request's customer and address. Up to 92 days, to on or after from (422 otherwise).","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":false,"description":"First day a trip may start (default tomorrow)"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":false,"description":"Last day (default 13 days after from)"},{"schema":{"type":"integer","minimum":0,"default":13900},"in":"query","name":"hotelCents","required":false},{"schema":{"type":"integer","minimum":0,"default":5900},"in":"query","name":"perDiemCents","required":false},{"schema":{"type":"integer","minimum":0,"default":40},"in":"query","name":"perKmCents","required":false},{"schema":{"type":"number","minimum":5,"maximum":100,"default":30},"in":"query","name":"radiusKm","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"waiting":{"type":"number"},"trips":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"tech":{"type":"string"},"firstDay":{"type":"string"},"lastDay":{"type":"string"},"base":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"areaKm":{"type":"number"},"nights":{"type":"number"},"km":{"type":"number"},"costCents":{"type":"number"},"valueCents":{"type":"number"},"stops":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"end":{"type":"string"},"customer":{"type":"string","nullable":true},"address":{"type":"string","nullable":true}},"required":["jobId","start","end","customer","address"],"additionalProperties":false}},"handoffs":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"date":{"type":"string"},"toPersonId":{"type":"number"}},"required":["jobId","date","toPersonId"],"additionalProperties":false},"description":"In-area jobs that go to a teammate at the same time, to free the trip days"}},"required":["personId","tech","firstDay","lastDay","base","areaKm","nights","km","costCents","valueCents","stops","handoffs"],"additionalProperties":false}}},"required":["waiting","trips"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/trips":{"post":{"summary":"Book a trip (usually a suggestion, as returned)","tags":["Trips"],"description":"Locks the trip days, hands the listed in-area jobs to teammates (same times), checks the person's days are now free and every stop fits, then books each stop and texts each customer. All or nothing: 409 if anything changed.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"personId":{"type":"integer"},"firstDay":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"lastDay":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"base":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"areaKm":{"type":"number","minimum":1,"maximum":200},"costCents":{"type":"integer","minimum":0,"default":0},"stops":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"integer"},"start":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}$","description":"Local wall clock in the traveller's zone (their branch's)"}},"required":["jobId","start"],"additionalProperties":false},"minItems":1,"maxItems":40},"handoffs":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"integer"},"date":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"toPersonId":{"type":"integer"}},"required":["jobId","date","toPersonId"],"additionalProperties":false},"maxItems":30,"default":[]}},"required":["personId","firstDay","lastDay","base","areaKm","stops"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"tripId":{"type":"string"},"booked":{"type":"number"}},"required":["tripId","booked"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"Booked trips","tags":["Trips"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"trips":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"personId":{"type":"number"},"firstDay":{"type":"string"},"lastDay":{"type":"string"},"base":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"costCents":{"type":"number"}},"required":["id","personId","firstDay","lastDay","base","costCents"],"additionalProperties":false}}},"required":["trips"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/reports":{"get":{"summary":"KPIs for a date range","tags":["Reports"],"description":"Up to 92 days, to on or after from (422 otherwise). Nothing before the company's metricsFrom (\"Count KPIs from\", PATCH /v1/office/settings) counts: the range starts no earlier than it (a range entirely before it is all zeros), and it is returned as `metricsFrom`. `jobs.waiting` is the queue now, not a count over the range.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Inclusive"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"},"metricsFrom":{"type":"string","nullable":true,"description":"\"Count KPIs from\" (local date): nothing before it counts. null: no limit"},"jobs":{"type":"object","properties":{"booked":{"type":"number"},"done":{"type":"number"},"cancelled":{"type":"number"},"waiting":{"type":"number"},"sentToPartners":{"type":"number"},"fromPartners":{"type":"number"},"recurring":{"type":"number"}},"required":["booked","done","cancelled","waiting","sentToPartners","fromPartners","recurring"],"additionalProperties":false},"revenueCents":{"type":"number"},"collectedCents":{"type":"number"},"platformFeeCents":{"type":"number"},"onTime":{"type":"object","properties":{"measured":{"type":"number"},"onTime":{"type":"number"},"pct":{"type":"number","nullable":true}},"required":["measured","onTime","pct"],"additionalProperties":false},"utilizationPct":{"type":"number","nullable":true},"reviews":{"type":"object","properties":{"count":{"type":"number"},"average":{"type":"number","nullable":true}},"required":["count","average"],"additionalProperties":false},"texts":{"type":"object","properties":{"sent":{"type":"number"},"failed":{"type":"number"}},"required":["sent","failed"],"additionalProperties":false},"trips":{"type":"object","properties":{"count":{"type":"number"},"costCents":{"type":"number"}},"required":["count","costCents"],"additionalProperties":false},"byTech":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"jobs":{"type":"number"},"bookedMinutes":{"type":"number"},"utilizationPct":{"type":"number","nullable":true},"onTimePct":{"type":"number","nullable":true}},"required":["id","name","jobs","bookedMinutes","utilizationPct","onTimePct"],"additionalProperties":false}},"byService":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"jobs":{"type":"number"},"revenueCents":{"type":"number"},"defaultMinutes":{"type":"number"},"learnedMeanMinutes":{"type":"number","nullable":true}},"required":["id","name","jobs","revenueCents","defaultMinutes","learnedMeanMinutes"],"additionalProperties":false}}},"required":["from","to","metricsFrom","jobs","revenueCents","collectedCents","platformFeeCents","onTime","utilizationPct","reviews","texts","trips","byTech","byService"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/market":{"get":{"summary":"Weather ahead and fuel prices where you work, with what they mean","tags":["Reports"],"description":"Weather: NOAA forecast at your first branch (US only; null elsewhere or if NOAA is down). Fuel: EIA weekly retail gasoline and diesel for your region (needs EIA_API_KEY). Signals: e.g. a heat wave or cold snap means more emergency calls, so keep the on-call reserve on. Cached 30 minutes.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"weather":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"start":{"type":"string"},"temperatureF":{"type":"number"},"daytime":{"type":"boolean"},"forecast":{"type":"string"}},"required":["name","start","temperatureF","daytime","forecast"],"additionalProperties":false},"nullable":true},"fuel":{"type":"object","properties":{"gasolineCents":{"type":"number","nullable":true},"dieselCents":{"type":"number","nullable":true},"week":{"type":"string","nullable":true},"region":{"type":"string"}},"required":["gasolineCents","dieselCents","week","region"],"additionalProperties":false,"nullable":true},"signals":{"type":"array","items":{"type":"string"}}},"required":["weather","fuel","signals"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/kpis/catalog":{"get":{"summary":"KPI definitions: key, name, one-sentence definition, unit and which way is better","tags":["Reports"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"kpis":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"definition":{"type":"string"},"unit":{"type":"string","enum":["count","pct","cents","minutes","hours","rating","ratio"],"description":"count | pct (0 to 100) | cents (minor units) | minutes | hours | rating (1 to 5) | ratio"},"direction":{"type":"string","enum":["higher_is_better","lower_is_better"]}},"required":["key","name","definition","unit","direction"],"additionalProperties":false}}},"required":["kpis"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/kpis":{"get":{"summary":"Every KPI for a date range, with the prior period, the change and a daily series","tags":["Reports"],"description":"Days are company-local. The prior period is the same number of days immediately before `from`. Breakdowns by tech, service and branch cover the range (jobs, list-price revenue, booked minutes, on-time arrivals, hours booked). Market signals come from GET /v1/office/market. `client` limits every KPI to one client company's jobs (its id; payments, reviews and texts only when they belong to those jobs; trips, CrewConnect and payouts drop out), or to jobs with no client (`none`). Test work (test people, the jobs they work and the jobs of test clients) is left out unless includeTest=true. Nothing before the company's metricsFrom (\"Count KPIs from\", PATCH /v1/office/settings) counts; it is returned as `metricsFrom`. 422 if `to` is before `from` or the range is over 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true,"description":"First local date, YYYY-MM-DD"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Last local date, inclusive (at most 366 days)"},{"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","enum":["none"]}]},"in":"query","name":"client","required":false,"description":"A client company id, or none"},{"schema":{"type":"string","enum":["true","false"],"default":"false"},"in":"query","name":"includeTest","required":false,"description":"true: count test work too (test people, their jobs and test clients' jobs; left out by default)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"timezone":{"type":"string"}},"required":["id","name","timezone"],"additionalProperties":false},"from":{"type":"string"},"to":{"type":"string"},"metricsFrom":{"type":"string","nullable":true,"description":"The company's \"Count KPIs from\" date (PATCH /v1/office/settings): nothing before it counts, in the range, prior period, series or breakdowns; a range entirely before it is all zeros (rates null). null: no limit"},"prior":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}},"required":["from","to"],"additionalProperties":false,"description":"The same number of days, immediately before"},"days":{"type":"array","items":{"type":"string"},"description":"Each local date in the range; every series lines up with it"},"currency":{"type":"string"},"kpis":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"name":{"type":"string"},"definition":{"type":"string"},"unit":{"type":"string","enum":["count","pct","cents","minutes","hours","rating","ratio"],"description":"count | pct (0 to 100) | cents (minor units) | minutes | hours | rating (1 to 5) | ratio"},"direction":{"type":"string","enum":["higher_is_better","lower_is_better"]},"value":{"type":"number","nullable":true,"description":"null when there is nothing to measure (e.g. no arrivals tapped)"},"prior":{"type":"number","nullable":true,"description":"The same KPI over `prior`. null when there is nothing to measure, or when `prior` starts before metricsFrom (not a whole period to compare with)"},"delta":{"type":"number","nullable":true,"description":"value - prior, in the KPI unit (percentage points for pct)"},"series":{"type":"array","items":{"type":"number","nullable":true},"description":"One value per day in `days`"}},"required":["key","name","definition","unit","direction","value","prior","delta","series"],"additionalProperties":false}},"breakdowns":{"type":"object","properties":{"tech":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"jobs":{"type":"number"},"revenueCents":{"type":"number"},"bookedMinutes":{"type":"number"},"onTimePct":{"type":"number","nullable":true},"utilizationPct":{"type":"number","nullable":true,"description":"null for services"}},"required":["id","name","jobs","revenueCents","bookedMinutes","onTimePct","utilizationPct"],"additionalProperties":false}},"service":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"jobs":{"type":"number"},"revenueCents":{"type":"number"},"bookedMinutes":{"type":"number"},"onTimePct":{"type":"number","nullable":true},"utilizationPct":{"type":"number","nullable":true,"description":"null for services"}},"required":["id","name","jobs","revenueCents","bookedMinutes","onTimePct","utilizationPct"],"additionalProperties":false}},"branch":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"jobs":{"type":"number"},"revenueCents":{"type":"number"},"bookedMinutes":{"type":"number"},"onTimePct":{"type":"number","nullable":true},"utilizationPct":{"type":"number","nullable":true,"description":"null for services"}},"required":["id","name","jobs","revenueCents","bookedMinutes","onTimePct","utilizationPct"],"additionalProperties":false}}},"required":["tech","service","branch"],"additionalProperties":false},"market":{"type":"object","properties":{"weather":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"start":{"type":"string"},"temperatureF":{"type":"number"},"daytime":{"type":"boolean"},"forecast":{"type":"string"}},"required":["name","start","temperatureF","daytime","forecast"],"additionalProperties":false},"nullable":true},"fuel":{"type":"object","properties":{"gasolineCents":{"type":"number","nullable":true},"dieselCents":{"type":"number","nullable":true},"week":{"type":"string","nullable":true},"region":{"type":"string"}},"required":["gasolineCents","dieselCents","week","region"],"additionalProperties":false,"nullable":true},"signals":{"type":"array","items":{"type":"string"}}},"required":["weather","fuel","signals"],"additionalProperties":false,"nullable":true,"description":"Weather ahead and fuel prices at your first branch, with signals (null without a branch)"},"generatedAt":{"type":"string"}},"required":["company","from","to","metricsFrom","prior","days","currency","kpis","breakdowns","market","generatedAt"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/exports/jobs.csv":{"get":{"summary":"Export jobs as CSV","tags":["Exports"],"description":"Every job starting in the range (booked, cancelled or passed on): date, time, service, customer, techs, list price, travel fee, arrival and finish times. RFC 4180 CSV with a UTF-8 byte order mark, streamed, as an attachment. Company-local dates and times; money in decimal dollars. 422 if `to` is before `from` or the range is over 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true,"description":"First local date, YYYY-MM-DD"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Last local date, inclusive (at most 366 days)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"text/csv; charset=utf-8","content":{"text/csv":{"schema":{"type":"string","description":"text/csv; charset=utf-8"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/exports/invoices.csv":{"get":{"summary":"Export invoices as CSV","tags":["Exports"],"description":"Invoices created in the range, with totals, deposit applied and balance. RFC 4180 CSV with a UTF-8 byte order mark, streamed, as an attachment. Company-local dates and times; money in decimal dollars. 422 if `to` is before `from` or the range is over 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true,"description":"First local date, YYYY-MM-DD"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Last local date, inclusive (at most 366 days)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"text/csv; charset=utf-8","content":{"text/csv":{"schema":{"type":"string","description":"text/csv; charset=utf-8"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/exports/payments.csv":{"get":{"summary":"Export payments as CSV","tags":["Exports"],"description":"Money in and out dated in the range (paid date, else created): deposits, invoice payments and CrewConnect payouts both ways. RFC 4180 CSV with a UTF-8 byte order mark, streamed, as an attachment. Company-local dates and times; money in decimal dollars. 422 if `to` is before `from` or the range is over 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true,"description":"First local date, YYYY-MM-DD"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Last local date, inclusive (at most 366 days)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"text/csv; charset=utf-8","content":{"text/csv":{"schema":{"type":"string","description":"text/csv; charset=utf-8"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/exports/kpis.csv":{"get":{"summary":"Export kpis as CSV","tags":["Exports"],"description":"Every KPI for the range with the prior period and the change (money in dollars). RFC 4180 CSV with a UTF-8 byte order mark, streamed, as an attachment. Company-local dates and times; money in decimal dollars. 422 if `to` is before `from` or the range is over 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true,"description":"First local date, YYYY-MM-DD"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Last local date, inclusive (at most 366 days)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"text/csv; charset=utf-8","content":{"text/csv":{"schema":{"type":"string","description":"text/csv; charset=utf-8"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/exports/techs.csv":{"get":{"summary":"Export techs as CSV","tags":["Exports"],"description":"Per tech for the range: jobs, booked hours, hours booked %, on-time %, list-price revenue. RFC 4180 CSV with a UTF-8 byte order mark, streamed, as an attachment. Company-local dates and times; money in decimal dollars. 422 if `to` is before `from` or the range is over 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true,"description":"First local date, YYYY-MM-DD"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Last local date, inclusive (at most 366 days)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"text/csv; charset=utf-8","content":{"text/csv":{"schema":{"type":"string","description":"text/csv; charset=utf-8"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/exports/services.csv":{"get":{"summary":"Export services as CSV","tags":["Exports"],"description":"Per service for the range: jobs, booked hours, on-time %, list-price revenue. RFC 4180 CSV with a UTF-8 byte order mark, streamed, as an attachment. Company-local dates and times; money in decimal dollars. 422 if `to` is before `from` or the range is over 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true,"description":"First local date, YYYY-MM-DD"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Last local date, inclusive (at most 366 days)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"text/csv; charset=utf-8","content":{"text/csv":{"schema":{"type":"string","description":"text/csv; charset=utf-8"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/exports/plan-vs-actual.csv":{"get":{"summary":"Export plan-vs-actual as CSV","tags":["Exports"],"description":"Every stop of every day in the range, planned against worked: the day's final plan (predicted arrival, start, finish), the real arrival and finish, minutes late, minutes on site planned and real, and the drive as we predicted it, as the traffic provider saw it and as driven, with the ~5 km cells it ran between. RFC 4180 CSV with a UTF-8 byte order mark, streamed, as an attachment. Company-local dates and times; money in decimal dollars. 422 if `to` is before `from` or the range is over 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true,"description":"First local date, YYYY-MM-DD"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Last local date, inclusive (at most 366 days)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"text/csv; charset=utf-8","content":{"text/csv":{"schema":{"type":"string","description":"text/csv; charset=utf-8"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"text/csv":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/reports.pdf":{"get":{"summary":"The KPI report as a PDF","tags":["Exports"],"description":"A branded report for the range: every KPI with the change on the prior period, jobs and revenue per day as bar charts, per-tech, per-service and per-branch tables, market signals, and the generation time in the footer. The same PDF goes out in the weekly email (PATCH /v1/office/settings weeklyReportTo). 422 if `to` is before `from` or the range is over 366 days.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"from","required":true,"description":"First local date, YYYY-MM-DD"},{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"to","required":true,"description":"Last local date, inclusive (at most 366 days)"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"application/pdf","content":{"application/pdf":{"schema":{"type":"string","description":"application/pdf"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/pdf":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/pdf":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/live":{"get":{"summary":"Live board: every tech today with a green / yellow / red status and ETA","tags":["Office"],"description":"ok: on time or early (arrives 5+ min before the start) or on site. tight: arrives between 5 min before and 10 min after. late: more than 10 min after. idle: no more jobs today. Location comes from the field app, only while the tech is on the clock (now and when the fix was taken; platform keys with a short-names grant only while the tech is working a job and the fix is fresh), never off-shift.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"at":{"type":"string"},"counts":{"type":"object","properties":{"ok":{"type":"number"},"tight":{"type":"number"},"late":{"type":"number"},"idle":{"type":"number"}},"required":["ok","tight","late","idle"],"additionalProperties":false},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"status":{"type":"string","enum":["ok","tight","late","idle"]},"doing":{"type":"string","enum":["on_site","driving","before_leaving","done"]},"location":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"},"at":{"type":"string"}},"required":["lat","lng","at"],"additionalProperties":false,"nullable":true,"description":"The last fix while the tech is on the clock; null off the clock, for every caller"},"next":{"type":"object","properties":{"jobId":{"type":"number"},"customer":{"type":"string","nullable":true},"start":{"type":"string"},"eta":{"type":"string","nullable":true},"minutesLate":{"type":"number","nullable":true}},"required":["jobId","customer","start","eta","minutesLate"],"additionalProperties":false,"nullable":true}},"required":["id","name","status","doing","location","next"],"additionalProperties":false}}},"required":["at","counts","techs"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/dispatch":{"get":{"summary":"The dispatch board for one day in one read (lanes, stops with drives, live status, queue, risks, pins)","tags":["Office"],"description":"Everything the Dispatch page draws for local date `date`: a lane per person working that day (hours that weekday or a visit; each on their own clock) with their stops in order and the planned drive before each, time off, live status, position and ETA (the tech's today only), the intake queue, risks, counts and map pins. `branch` narrows lanes and queue to one branch; `client` narrows stops and queue to one client company's jobs (`none`: jobs with no client). Platform keys need `read`; they see techs as \"First L.\" (full names when the grant says so), no homeowner phone, and positions by the grant: full-names grants while a tech is on the clock, others only while the tech is working a job and the fix is fresh. Each stop carries `risk` (level, ETA, minutes late, reason), field timestamps, planned vs actual order and the actual drive. People with a team or own scope see other jobs as busy blocks and no live status. Answers carry an ETag: send it as If-None-Match and an unchanged board is a 304 with no body (poll every 30 to 60 s).","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":true,"description":"Local date, YYYY-MM-DD (422 for one that is not a real date)"},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"branch","required":false,"description":"A branch id"},{"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","enum":["none"]}]},"in":"query","name":"client","required":false,"description":"A client company id, or none"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"timezone":{"type":"string","description":"The board's zone: the branch's with `branch`, else the company's. Each lane keeps its own (`tech.timezone`)"},"generatedAt":{"type":"string"},"live":{"type":"boolean","description":"Some lane has a live status (the date is today for someone)"},"branches":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"timezone":{"type":"string"}},"required":["id","name","timezone"],"additionalProperties":false}},"lanes":{"type":"array","items":{"type":"object","properties":{"tech":{"type":"object","properties":{"id":{"type":"number"},"displayName":{"type":"string","description":"Full name for company keys and users; \"First L.\" for platform keys"},"timezone":{"type":"string","description":"IANA zone of this lane's times"},"branchId":{"type":"string","nullable":true},"home":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}},"required":["lat","lng"],"additionalProperties":false},"hours":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}]},"description":"Working windows that day, minutes after local midnight; [] when off (on the board for a visit)"}},"required":["id","displayName","timezone","branchId","home","hours"],"additionalProperties":false},"test":{"type":"boolean","description":"true: a test person (only takes jobs for test clients); the board shows a Test badge on the lane"},"status":{"type":"string","enum":["ok","tight","late","idle","on_job"],"nullable":true,"description":"Live (the tech's today only, else null): ok on track, tight within 5 min before to 10 after, late more than 10 after, on_job on site and on track, idle nothing left"},"doing":{"type":"string","enum":["on_site","driving","before_leaving","done"],"nullable":true},"fieldStatus":{"type":"string","enum":["off","idle","en_route","on_site"],"nullable":true,"description":"Live (the tech's today only, else null): on_site, en_route, idle (on the clock, not driving or on site) or off (off the clock)"},"currentJobId":{"type":"number","nullable":true,"description":"The job the tech is on now (on site, or due in its window), live only"},"nextJobId":{"type":"number","nullable":true,"description":"The next job not yet started, live only"},"location":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"},"accuracy":{"type":"number","nullable":true,"description":"Horizontal accuracy in metres, as the phone reported it"},"heading":{"type":"number","nullable":true,"description":"Degrees clockwise from true north, when moving"},"speed":{"type":"number","nullable":true,"description":"Metres per second"},"at":{"type":"string","description":"When the phone took the fix (UTC); recordedAt on the live read"}},"required":["lat","lng","accuracy","heading","speed","at"],"additionalProperties":false,"nullable":true,"description":"Last position from the field app on the tech's today. Company keys, users and platform keys whose grant says full names: while the tech is on the clock (now and when reported, from 30 min before their first window or visit to 30 min after their last). Other platform keys: only while the tech is working a job (one not done that ended at most an hour ago or starts within two hours) and the fix is at most 15 min old. Never off the clock; else null"},"stops":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Integrators' ids for the job, by source (e.g. { \"acme\": \"proj_123\" })"},"clientCompanyId":{"type":"string","nullable":true},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string","description":"Company keys and users only; never for platform keys"}},"required":["name"],"additionalProperties":false,"nullable":true},"address":{"type":"object","properties":{"line1":{"type":"string","nullable":true},"line2":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"region":{"type":"string","nullable":true,"description":"State, province or region"},"postcode":{"type":"string","nullable":true},"lat":{"type":"number"},"lng":{"type":"number"},"full":{"type":"string","nullable":true,"description":"The address as one line"}},"required":["line1","line2","city","region","postcode","lat","lng","full"],"additionalProperties":false,"description":"As sent in parts when the job was booked that way, else read from the one-line address (best effort)"},"start":{"type":"string","description":"Instant (UTC)"},"end":{"type":"string","description":"Instant (UTC)"},"local":{"type":"string","description":"Start, wall clock on the tech's clock (`tech.timezone`), YYYY-MM-DDTHH:MM"},"localEnd":{"type":"string"},"status":{"type":"string","description":"Stored status: booked or held (a booking-page hold)"},"state":{"type":"string","enum":["held","waiting","unscheduled","scheduled","in_progress","overdue","completed","cancelled","passed"],"description":"lib/jobstate.ts: held, scheduled, in_progress (on the way or on site), overdue (ended, not marked done) or completed"},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"]},"stage":{"type":"string","description":"Field progress: scheduled, en_route, on_site or done"},"overdue":{"type":"boolean"},"needsAttention":{"type":"boolean"},"attentionReason":{"type":"string","nullable":true},"homeownerTexts":{"type":"boolean","description":"false: this job sends the homeowner nothing"},"outsideHours":{"type":"boolean","description":"true: booked outside the tech's working hours (an import honouring a time the old system confirmed)"},"test":{"type":"boolean","description":"true: the job is for a test client (it only ever goes to test people); the board shows a Test badge"},"driveMinutesBefore":{"type":"number","nullable":true,"description":"Planned drive to this stop from the previous one (or home, or a base), minutes. null for holds"},"driveFrom":{"type":"string","enum":["home","base","job"],"nullable":true},"driveSource":{"type":"string","enum":["plan","predicted"],"nullable":true,"description":"plan: the day's plan snapshot the techs are working to; predicted: the engine's prediction for the day as booked now"},"eta":{"type":"string","nullable":true,"description":"Live ETA (UTC) while the tech is on the way to this stop"},"minutesLate":{"type":"number","nullable":true,"description":"ETA minus start, with eta; negative is early"},"risk":{"type":"object","properties":{"level":{"type":"string","enum":["on_time","at_risk","late"],"description":"on_time (arrives 5+ min early), at_risk (5 early to 10 late), late (more than 10 late)"},"eta":{"type":"string","nullable":true,"description":"Expected arrival (UTC); null for basis actual and for a stop due now that has not started"},"minutesLate":{"type":"number","description":"Expected (or actual) arrival minus scheduled start; negative is early"},"reason":{"type":"string","enum":["traffic","previous_overrun","not_started","tight_schedule","unknown"],"nullable":true,"description":"Why it is not on time (null when it is): traffic (on the way, drive running long), previous_overrun (an earlier job ran past its end, or this is chained behind a late one), not_started (should have left or started by now), tight_schedule (the gap is shorter than the drive), unknown (on the way with no fresh position)"},"basis":{"type":"string","enum":["live","chained","actual"],"description":"live: the tech's next stop (or the one due now); chained: a later stop, from the next one's ETA plus booked lengths and planned drives; actual: from the Arrived tap"}},"required":["level","eta","minutesLate","reason","basis"],"additionalProperties":false,"nullable":true,"description":"Per-stop risk: today's not-yet-arrived stops from the live ETA, any day's arrived stops from the arrival; null otherwise (another day, holds, people with a team or own scope)"},"enRouteAt":{"type":"string","nullable":true,"description":"When the tech tapped On the way for this job (UTC), else null"},"arrivedAt":{"type":"string","nullable":true,"description":"When the tech tapped Arrived (UTC), else null"},"completedAt":{"type":"string","nullable":true,"description":"When the job was marked done (UTC; doneAt on job reads), else null"},"plannedOrder":{"type":"number","description":"1-based position in this lane's stops, by booked start"},"actualOrder":{"type":"number","nullable":true,"description":"1-based position by arrival among the stops arrived at, else null"},"actualDriveMinutes":{"type":"number","nullable":true,"description":"The drive to this stop as it happened (On the way to Arrived, whole minutes), when both were tapped; compare driveMinutesBefore"}},"required":["jobId","externalRefs","clientCompanyId","client","service","customer","address","start","end","local","localEnd","status","state","jobStatus","stage","overdue","needsAttention","attentionReason","homeownerTexts","outsideHours","test","driveMinutesBefore","driveFrom","driveSource","eta","minutesLate","risk","enRouteAt","arrivedAt","completedAt","plannedOrder","actualOrder","actualDriveMinutes"],"additionalProperties":false}},"blocks":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"end":{"type":"string"},"local":{"type":"string"},"localEnd":{"type":"string"},"reason":{"type":"string","nullable":true},"source":{"type":"string","description":"office, google or outlook (time off), or job: another person's job outside your scope, shown as busy"}},"required":["start","end","local","localEnd","reason","source"],"additionalProperties":false}},"utilization":{"type":"object","properties":{"bookedMinutes":{"type":"number"},"hoursMinutes":{"type":"number"},"percent":{"type":"number","nullable":true}},"required":["bookedMinutes","hoursMinutes","percent"],"additionalProperties":false,"description":"Booked minutes that day (every visit, whatever the client filter) over working-window minutes"}},"required":["tech","test","status","doing","fieldStatus","currentJobId","nextJobId","location","stops","blocks","utilization"],"additionalProperties":false}},"queue":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"status":{"type":"string","description":"Stored status: queued, waiting (out-of-area request) or unscheduled (bumped)"},"jobStatus":{"type":"string","enum":["unscheduled","scheduled","en_route","on_site","done","cancelled"]},"ageDays":{"type":"number","description":"Whole days since it was filed"},"createdAt":{"type":"string"},"intakeReason":{"type":"string","nullable":true},"source":{"type":"string"},"clientCompanyId":{"type":"string","nullable":true},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true,"description":"The client company the job is for"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Integrators' ids for the job, by source (e.g. { \"acme\": \"proj_123\" })"},"service":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"},"category":{"type":"string","enum":["residential","commercial","other"],"nullable":true}},"required":["id","name","category"],"additionalProperties":false},"customer":{"type":"object","properties":{"name":{"type":"string"},"phone":{"type":"string","description":"Company keys and users only; never for platform keys"}},"required":["name"],"additionalProperties":false,"nullable":true},"address":{"type":"object","properties":{"line1":{"type":"string","nullable":true},"line2":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"region":{"type":"string","nullable":true,"description":"State, province or region"},"postcode":{"type":"string","nullable":true},"lat":{"type":"number"},"lng":{"type":"number"},"full":{"type":"string","nullable":true,"description":"The address as one line"}},"required":["line1","line2","city","region","postcode","lat","lng","full"],"additionalProperties":false,"description":"As sent in parts when the job was booked that way, else read from the one-line address (best effort)"},"timezone":{"type":"string"},"requested":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false,"nullable":true},"requestedDays":{"type":"array","items":{"type":"object","properties":{"date":{"type":"string"},"part":{"type":"string"}},"required":["date","part"],"additionalProperties":false},"nullable":true},"needsAttention":{"type":"boolean"},"attentionReason":{"type":"string","nullable":true},"test":{"type":"boolean","description":"true: for a test client (only test people can take it)"}},"required":["jobId","status","jobStatus","ageDays","createdAt","intakeReason","source","clientCompanyId","client","externalRefs","service","customer","address","timezone","requested","requestedDays","needsAttention","attentionReason","test"],"additionalProperties":false},"description":"The intake queue, oldest first (at most 100; counts.unscheduled is all of it)"},"risks":{"type":"array","items":{"type":"object","properties":{"kind":{"type":"string","enum":["late","tight","overdue","aging"]},"jobId":{"type":"number"},"techId":{"type":"number","nullable":true},"minutesLate":{"type":"number","nullable":true,"description":"late and tight from the live board: minutes after the start the tech is expected (negative = early); a tight planned leg: minutes the gap is short of the planned drive plus the buffer"},"message":{"type":"string"}},"required":["kind","jobId","techId","minutesLate","message"],"additionalProperties":false},"description":"late and tight from the live board (today); tight planned legs from today on (a gap between two booked jobs shorter than the drive planning allows plus the company's buffer: bookings made before 2026-10-12's conservative drive times, or tightened by hand); overdue stops on this day; queue jobs waiting 3+ days"},"counts":{"type":"object","properties":{"jobs":{"type":"number"},"working":{"type":"number"},"bookedPercent":{"type":"number"},"driving":{"type":"number"},"ok":{"type":"number"},"tight":{"type":"number"},"late":{"type":"number"},"onJob":{"type":"number"},"idle":{"type":"number"},"overdue":{"type":"number"},"needsAttention":{"type":"number"},"unscheduled":{"type":"number"}},"required":["jobs","working","bookedPercent","driving","ok","tight","late","onJob","idle","overdue","needsAttention","unscheduled"],"additionalProperties":false},"pins":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"lat":{"type":"number"},"lng":{"type":"number"},"state":{"type":"string","enum":["held","waiting","unscheduled","scheduled","in_progress","overdue","completed","cancelled","passed"]},"stage":{"type":"string"},"techIds":{"type":"array","items":{"type":"number"}}},"required":["jobId","lat","lng","state","stage","techIds"],"additionalProperties":false},"description":"One per job on the board, for a map"}},"required":["date","timezone","generatedAt","live","branches","lanes","queue","risks","counts","pins"],"additionalProperties":false}}}},"304":{"description":"Not modified: If-None-Match matched the ETag","content":{"application/json":{"schema":{"enum":["null"],"nullable":true,"description":"Not modified: If-None-Match matched the ETag"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/dispatch/live":{"get":{"summary":"Live positions, field statuses and per-job risk for one day (light; poll every 30 to 60 s)","tags":["Office"],"description":"The live part of the dispatch board for local date `date` (default: today in the company's zone): one row per person working that day with their field status (off, idle, en_route, on_site), current and next job and position, and one row per visible stop with its scheduled start, ETA, minutes late, risk level and reason and the field timestamps. Same filters, scoping and privacy as GET /v1/office/dispatch (`branch`, `client`; platform keys need read; positions by the grant's people-names rule). Answers carry an ETag: send it as If-None-Match and an unchanged answer is a 304 with no body. Push instead: webhooks dispatch.position (subscribe by name) and job.risk_changed.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":false,"description":"Local date, YYYY-MM-DD; default today in the company's zone"},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"branch","required":false,"description":"A branch id"},{"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","enum":["none"]}]},"in":"query","name":"client","required":false,"description":"A client company id, or none"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"timezone":{"type":"string"},"generatedAt":{"type":"string"},"live":{"type":"boolean","description":"Some tech has a live status (the date is today for someone)"},"techs":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"displayName":{"type":"string"},"timezone":{"type":"string"},"branchId":{"type":"string","nullable":true},"test":{"type":"boolean"},"status":{"type":"string","enum":["off","idle","en_route","on_site"],"nullable":true,"description":"off, idle, en_route or on_site; null on another day or for a narrowed scope"},"laneStatus":{"type":"string","enum":["ok","tight","late","idle","on_job"],"nullable":true,"description":"The board's dot (lanes[].status)"},"currentJobId":{"type":"number","nullable":true},"nextJobId":{"type":"number","nullable":true},"position":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"},"accuracy":{"type":"number","nullable":true,"description":"Horizontal accuracy in metres, as the phone reported it"},"heading":{"type":"number","nullable":true,"description":"Degrees clockwise from true north, when moving"},"speed":{"type":"number","nullable":true,"description":"Metres per second"},"recordedAt":{"type":"string","description":"When the phone took the fix (UTC)"}},"required":["lat","lng","accuracy","heading","speed","recordedAt"],"additionalProperties":false,"nullable":true,"description":"Last position from the field app on the tech's today. Company keys, users and platform keys whose grant says full names: while the tech is on the clock (now and when reported, from 30 min before their first window or visit to 30 min after their last). Other platform keys: only while the tech is working a job (one not done that ended at most an hour ago or starts within two hours) and the fix is at most 15 min old. Never off the clock; else null"}},"required":["personId","displayName","timezone","branchId","test","status","laneStatus","currentJobId","nextJobId","position"],"additionalProperties":false}},"jobs":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"personId":{"type":"number","description":"The tech this row is for (a crew job has one row per tech)"},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Integrators' ids for the job, by source (e.g. { \"acme\": \"proj_123\" })"},"clientCompanyId":{"type":"string","nullable":true},"stage":{"type":"string"},"start":{"type":"string","description":"Scheduled start (UTC)"},"end":{"type":"string"},"local":{"type":"string"},"eta":{"type":"string","nullable":true},"minutesLate":{"type":"number","nullable":true,"description":"Signed: negative is early"},"risk":{"type":"string","enum":["on_time","at_risk","late"],"nullable":true},"reason":{"type":"string","enum":["traffic","previous_overrun","not_started","tight_schedule","unknown"],"nullable":true},"basis":{"type":"string","enum":["live","chained","actual"],"nullable":true},"enRouteAt":{"type":"string","nullable":true},"arrivedAt":{"type":"string","nullable":true},"completedAt":{"type":"string","nullable":true},"plannedOrder":{"type":"number"},"actualOrder":{"type":"number","nullable":true}},"required":["jobId","personId","externalRefs","clientCompanyId","stage","start","end","local","eta","minutesLate","risk","reason","basis","enRouteAt","arrivedAt","completedAt","plannedOrder","actualOrder"],"additionalProperties":false},"description":"Every stop the caller may see that day, lane by lane in start order (the same rows as lanes[].stops on GET /v1/office/dispatch)"}},"required":["date","timezone","generatedAt","live","techs","jobs"],"additionalProperties":false}}}},"304":{"description":"Not modified: If-None-Match matched the ETag","content":{"application/json":{"schema":{"enum":["null"],"nullable":true,"description":"Not modified: If-None-Match matched the ETag"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/dispatch/routes":{"get":{"summary":"Where each tech is and the route they are on (live map; poll every 15 to 30 s)","tags":["Office"],"description":"Per working tech for local date `date` (default today in the company's zone): position and heading (on the clock only), field status, whether they are navigating in the field app, and the driving route they are on now as a GeoJSON LineString with legs (distance, drive time, ETA) through their remaining stops in order, plus their stops with times and status. Navigating: the route their phone follows, then the planned legs. Else: the planned route from their position (or their current or last stop, or where the day starts) through what is left. Routes come from the route provider (Mapbox driving-traffic, else OSRM) and are cached: a leg between fixed points 30 min, the leg from a moving tech 3 min while they stay on it; at most 6 provider calls per read and 60 a minute, the rest drawn straight until a later read. Filters: `branch`, `client` (techs with a stop for that client; their whole route shows), `techIds`. Owners, dispatchers and field managers; people with a team or own scope see only techs with a stop of theirs, only those stops, and the route up to the first stop they can't see. Platform keys need read; names and positions by the grant (full names: on the clock; short: only while working a job with a fresh fix). Never a position off the clock. ETag: send If-None-Match and an unchanged answer is a 304.","parameters":[{"schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"in":"query","name":"date","required":false,"description":"Local date, YYYY-MM-DD; default today in the company's zone"},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"branch","required":false,"description":"A branch id"},{"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","enum":["none"]}]},"in":"query","name":"client","required":false,"description":"A client company id, or none"},{"schema":{"type":"string","pattern":"^\\d+(,\\d+)*$"},"in":"query","name":"techIds","required":false,"description":"Only these techs: person ids, comma separated"}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"timezone":{"type":"string"},"generatedAt":{"type":"string"},"live":{"type":"boolean","description":"Some tech has a live status (the date is today for someone)"},"branches":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"timezone":{"type":"string"}},"required":["id","name","timezone"],"additionalProperties":false}},"mapStyle":{"type":"object","properties":{"url":{"type":"string"},"token":{"type":"string","description":"A public map token, safe in a browser"},"style":{"type":"string","enum":["crewlink-nav"]},"version":{"type":"number"},"themes":{"type":"array","items":{"type":"string","enum":["light","blue","dark"]}}},"required":["url","token","style","version","themes"],"additionalProperties":false,"nullable":true,"description":"CrewLink's map style for the office's own map (as on GET /v1/field/jobs/{id}/route); always null for platform keys"},"techs":{"type":"array","items":{"type":"object","properties":{"personId":{"type":"number"},"displayName":{"type":"string","description":"Full name for company keys and users; \"First L.\" for platform keys unless the grant says full names"},"timezone":{"type":"string"},"branchId":{"type":"string","nullable":true},"test":{"type":"boolean"},"status":{"type":"string","enum":["off","idle","en_route","on_site"],"nullable":true,"description":"off, idle, en_route or on_site; null on another day"},"laneStatus":{"type":"string","enum":["ok","tight","late","idle","on_job"],"nullable":true,"description":"The board's risk dot: ok, tight (cutting it close), late, on_job, idle"},"currentJobId":{"type":"number","nullable":true},"nextJobId":{"type":"number","nullable":true},"position":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"},"accuracy":{"type":"number","nullable":true,"description":"Horizontal accuracy in metres, as the phone reported it"},"heading":{"type":"number","nullable":true,"description":"Degrees clockwise from true north, when moving"},"speed":{"type":"number","nullable":true,"description":"Metres per second"},"recordedAt":{"type":"string","description":"When the phone took the fix (UTC)"}},"required":["lat","lng","accuracy","heading","speed","recordedAt"],"additionalProperties":false,"nullable":true,"description":"The last fix, only while the tech is on the clock (now and when it was taken, from 30 min before their first window or visit to 30 min after their last), for every caller. Platform keys with a short-names grant: only while the tech is working a job and the fix is at most 15 min old. Scoped users: only while the tech's current or next job is one of theirs"},"heading":{"type":"number","nullable":true,"description":"The fix's heading, degrees clockwise from north (null when unknown or no position)"},"positionState":{"type":"string","enum":["live","stale","none","off_clock","hidden"],"nullable":true,"description":"Why there is (no) position: live (fresh fix), stale (older than 15 min), none (location off: no fix since they came on the clock), off_clock, hidden (this caller may not see it now); null on another day"},"navigating":{"type":"boolean","description":"The tech is following in-app navigation to their next job (they fetched its route in the field app in the last 90 min and tapped On my way)"},"route":{"type":"object","properties":{"type":{"type":"string","enum":["LineString"]},"coordinates":{"type":"array","items":{"type":"array","minItems":2,"maxItems":2,"items":[{"type":"number"},{"type":"number"}],"description":"[lng, lat]"}}},"required":["type","coordinates"],"additionalProperties":false,"nullable":true,"description":"The driving line the tech is on now, GeoJSON [lng, lat]: navigating, the route their phone is following then the planned legs after it; else the planned route from their position (or the stop they're at or last left, or where the day starts) through the remaining stops in order. null: nothing left"},"routeSource":{"type":"string","enum":["navigation","planned"],"nullable":true},"progressMeters":{"type":"number","nullable":true,"description":"Metres along `route` where the tech is now (draw what is behind it as driven)"},"legs":{"type":"array","items":{"type":"object","properties":{"from":{"type":"string","enum":["position","origin","job"],"description":"position: where the tech is; origin: where the day starts; job: the stop before"},"fromJobId":{"type":"number","nullable":true},"toJobId":{"type":"number"},"distanceMeters":{"type":"number","description":"What is left of the leg (from a position: from where they are)"},"durationSeconds":{"type":"number"},"eta":{"type":"string","description":"Expected arrival (UTC, whole minutes): live legs from the drive left, later ones chained (the later of arriving and the start, plus the booked length, plus the drive). Another day: the booked start"},"minutesLate":{"type":"number","description":"eta minus the scheduled start; negative is early"},"provider":{"type":"string","enum":["mapbox","osrm","straight"],"description":"straight: no road route yet (no routing, it failed, or over the per-read budget): a straight line with the planned drive time"},"endIndex":{"type":"number","description":"Index in route.coordinates where this leg ends"}},"required":["from","fromJobId","toJobId","distanceMeters","durationSeconds","eta","minutesLate","provider","endIndex"],"additionalProperties":false}},"stops":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"order":{"type":"number","description":"1-based position by booked start"},"start":{"type":"string"},"end":{"type":"string"},"local":{"type":"string"},"stage":{"type":"string"},"state":{"type":"string","enum":["held","waiting","unscheduled","scheduled","in_progress","overdue","completed","cancelled","passed"]},"reached":{"type":"boolean","description":"Done, on site or arrived: behind the tech"},"eta":{"type":"string","nullable":true},"risk":{"type":"object","properties":{"level":{"type":"string","enum":["on_time","at_risk","late"]},"minutesLate":{"type":"number"}},"required":["level","minutesLate"],"additionalProperties":false,"nullable":true},"customer":{"type":"string","nullable":true},"service":{"type":"string"},"client":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false,"nullable":true},"address":{"type":"object","properties":{"full":{"type":"string","nullable":true},"lat":{"type":"number"},"lng":{"type":"number"}},"required":["full","lat","lng"],"additionalProperties":false},"externalRefs":{"type":"object","additionalProperties":{"type":"string"},"description":"Integrators' ids for the job, by source (e.g. { \"acme\": \"proj_123\" })"}},"required":["jobId","order","start","end","local","stage","state","reached","eta","risk","customer","service","client","address","externalRefs"],"additionalProperties":false},"description":"The tech's booked stops that day the caller may see, in order"}},"required":["personId","displayName","timezone","branchId","test","status","laneStatus","currentJobId","nextJobId","position","heading","positionState","navigating","route","routeSource","progressMeters","legs","stops"],"additionalProperties":false}}},"required":["date","timezone","generatedAt","live","branches","mapStyle","techs"],"additionalProperties":false}}}},"304":{"description":"Not modified: If-None-Match matched the ETag","content":{"application/json":{"schema":{"enum":["null"],"nullable":true,"description":"Not modified: If-None-Match matched the ETag"}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/settings":{"patch":{"summary":"Company rules (owner only)","tags":["Office"],"description":"onCall: keep one tech per branch free on weekdays in this window (minutes after midnight, each branch's own time zone) for same-day emergencies; null turns it off. timezone: the company's default IANA zone (a branch can have its own: PATCH /v1/office/branches/{id}); changing it moves nothing already booked (their instants stay; jobs keep the zone they were booked in); people without a branch keep their home's zone. defaultTechRadiusMiles: how far from home people without a branch are offered work (each person's maxTravelMiles wins; null: 40 km); for a national company whose techs work from home. bufferMinutes: \"Buffer between jobs\", minutes kept clear before every visit after the drive (0-60, default 10). Every way a job is placed (open times, the booking page and holds, office booking, moves and drag-drop, recurring visits, intake, CrewConnect, emergencies and trips) plans with it, on top of the drive planned at a conservative time-of-day estimate; it applies to new bookings and moves only, never re-times what is booked, and the dispatch board flags booked legs that no longer fit as tight. country: ISO 3166-1 alpha-2 of the head office; it sets which phone numbers get texts (with every branch's country), how numbers without a +country code are read, address search and the default distance units. units: 'mi' or 'km' for every distance shown to people (null: the country's usual one). cancelCutoffHours: how close to the start customers (their manage link) and client companies (POST /v1/connect/requests/{id}/cancel) can still cancel or move (default 24; the office and platforms on the company's own grant have no cutoff). homeownerMessaging: off stops every text and email to homeowners for all of this company's jobs (confirmations, reminders, on-the-way and ETA, running-late moves, reschedules, cancellations, review links, invoices); anything already queued is dropped when it comes due. on (the default) sends them, subject to each job's own notify flag and the grant of the platform that booked it. weeklyReportTo: an email that gets last week's KPI report as a PDF every Monday at 07:00 company time; null turns it off. metricsFrom: \"Count KPIs from\", a local date (YYYY-MM-DD, company time); every KPI and metric (KPIs and their exports, PDF and weekly email, GET /v1/office/reports, plan vs actual, Wins goals, streaks, badges, leaderboards and tech stats) counts nothing before it, and badges and wins from before it are hidden (not deleted); null (the default) is no limit. Pay, invoices, billing and job lists are not affected. locationRetentionDays: how long techs' location trails (on-the-clock fixes from the field app or a platform's tech app, for mileage and pay disputes) are kept, 7-365 days (default 90); older fixes are deleted by the hourly retention job. shareLinkHost: the host your clients' share links are served on (your own domain, e.g. book.example.com: it must be one this server serves, SHARE_LINK_HOSTS, else 422; null: the platform's, else this server's own address), and shareLinkMailFrom the From address of their emails (a domain verified with the mail provider; null: the platform's, else the server's). [Booking page guide](/docs/guides/booking-page#your-own-domain). allowedOrigins: the websites (https origins such as `https://www.example.com`, at most 20) whose pages may call your public booking endpoints straight from the browser (CORS); the list replaces the old one, [] allows none. 422 when one isn't an https origin. The response is every setting as it is now (including integrationKey: whether one exists), so a client can refresh from it.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"onCall":{"type":"object","properties":{"start":{"type":"integer","minimum":0,"maximum":1440},"end":{"type":"integer","minimum":0,"maximum":1440}},"required":["start","end"],"additionalProperties":false,"nullable":true},"cancelCutoffHours":{"type":"integer","minimum":0,"maximum":168},"weeklyReportTo":{"type":"string","format":"email","maxLength":254,"nullable":true},"allowedOrigins":{"type":"array","items":{"type":"string","maxLength":300},"maxItems":20,"description":"https://host[:port], no path; e.g. 'https://www.example.com'"},"timezone":{"type":"string","maxLength":64,"description":"IANA zone, e.g. 'America/Chicago', 'Europe/London'"},"country":{"type":"string","pattern":"^[A-Za-z]{2}$","description":"ISO 3166-1 alpha-2, e.g. 'US', 'GB'"},"units":{"type":"string","enum":["mi","km"],"nullable":true,"description":"null: the country's usual units"},"homeownerMessaging":{"type":"string","enum":["on","off"],"description":"off: no texts or emails to homeowners for any job; on (default)"},"defaultTechRadiusMiles":{"type":"number","minimum":1,"maximum":3000,"nullable":true,"description":"How far from home (miles, straight line) people without a branch are offered work when they have no maxTravelMiles of their own; null: the default 40 km (about 25 miles)"},"bufferMinutes":{"type":"integer","minimum":0,"maximum":60,"description":"Buffer between jobs, minutes (0-60; default 10)"},"productAnalytics":{"type":"string","enum":["on","off"],"description":"off: CrewLink's own product analytics (PostHog) records nothing about this company, from the server or from its people's browsers and phones; on (default). CrewLink's own notes"},"metricsFrom":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$","nullable":true,"description":"\"Count KPIs from\": a local date, YYYY-MM-DD; nothing before it counts in any KPI or metric. null: no limit"},"locationRetentionDays":{"type":"integer","minimum":7,"maximum":365,"description":"How long techs' location trails (on-the-clock fixes, for mileage and pay disputes) are kept, days: 7-365, default 90. Shortening it deletes older fixes within the hour"},"shareLinkHost":{"type":"string","maxLength":253,"nullable":true,"description":"Where your clients' share links are served: a host this server serves (SHARE_LINK_HOSTS), e.g. book.example.com; null: the default"},"shareLinkMailFrom":{"type":"string","format":"email","maxLength":254,"nullable":true,"description":"The From address of share-link emails (a domain verified with the mail provider); null: the default"}},"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Every setting as it is now, after the change (the same values as company in GET /v1/office/me)","content":{"application/json":{"schema":{"type":"object","properties":{"onCall":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"}},"required":["start","end"],"additionalProperties":false,"nullable":true},"cancelCutoffHours":{"type":"number"},"weeklyReportTo":{"type":"string","nullable":true},"allowedOrigins":{"type":"array","items":{"type":"string"}},"timezone":{"type":"string"},"country":{"type":"string"},"units":{"type":"string","enum":["mi","km"]},"integrationKey":{"type":"boolean","description":"Whether an integration key exists (rotate it with POST /v1/office/integration-key/rotate)"},"homeownerMessaging":{"type":"string","enum":["on","off"]},"defaultTechRadiusMiles":{"type":"number","nullable":true,"description":"Service radius around home of people without a branch (and no maxTravelMiles); null: 40 km"},"bufferMinutes":{"type":"integer","description":"Buffer between jobs, minutes"},"metricsFrom":{"type":"string","nullable":true,"description":"\"Count KPIs from\" (local date), or null"},"productAnalytics":{"type":"string","enum":["on","off"],"description":"CrewLink's product analytics for this company"},"locationRetentionDays":{"type":"integer","description":"Days location trails are kept (7-365, default 90)"},"shareLinkHost":{"type":"string","nullable":true},"shareLinkMailFrom":{"type":"string","nullable":true}},"required":["onCall","cancelCutoffHours","weeklyReportTo","allowedOrigins","timezone","country","units","integrationKey","homeownerMessaging","defaultTechRadiusMiles","bufferMinutes","metricsFrom","productAnalytics","locationRetentionDays","shareLinkHost","shareLinkMailFrom"],"additionalProperties":false,"description":"Every setting as it is now, after the change (the same values as company in GET /v1/office/me)"}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/emergencies":{"post":{"summary":"Book a same-day emergency","tags":["Office"],"description":"Earliest open time today from 30 minutes from now, including the on-call reserve. If the day is full, one flexible job is moved: the engine tries the best few options in full and keeps the one that gets the emergency in soon while moving the other customer least (one-person jobs first). That customer is texted their new time, or told we'll call when nothing fits within 14 days. 409 no_one_today if no one qualified can be there today, with a reason: outside_hours (no one who can do it is working long enough today), no_skills, or doesnt_fit (the drive and today's jobs are in the way).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"serviceId":{"type":"integer"},"lat":{"type":"number","minimum":-90,"maximum":90},"lng":{"type":"number","minimum":-180,"maximum":180},"customer":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"phone":{"type":"string","minLength":7,"maxLength":40},"email":{"type":"string","format":"email"}},"required":["name","phone"],"additionalProperties":false},"address":{"type":"string","maxLength":300},"notes":{"type":"string","maxLength":2000}},"required":["serviceId","lat","lng","customer"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"start":{"type":"string"},"local":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of local (the serving branch's)"},"techs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}},"manageToken":{"type":"string","description":"The customer's manage link token (also in their confirmation text)"},"moved":{"type":"array","items":{"type":"object","properties":{"jobId":{"type":"number"},"from":{"type":"string"},"to":{"type":"string","nullable":true},"customer":{"type":"string","nullable":true}},"required":["jobId","from","to","customer"],"additionalProperties":false},"description":"Jobs moved to make room (to = null: the office needs to call them)"}},"required":["jobId","start","local","timezone","techs","manageToken","moved"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"},"reason":{"type":"string","enum":["outside_hours","no_skills","doesnt_fit"],"description":"outside_hours: no one who can do it is working long enough today; no_skills: not enough people with the skills; doesnt_fit: the drive and today's jobs are in the way"}},"required":["error","message","reason"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/calendar/callback/{provider}":{"get":{"summary":"OAuth callback from Google or Microsoft","tags":["Field app"],"description":"Google or Microsoft sends the browser here after consent; nobody calls it directly. Always a 302 to the web app: /tech?calendar=connected, or /tech?calendar=failed (bad or reused state, a provider error, or consent refused).","parameters":[{"schema":{"type":"string"},"in":"query","name":"code","required":false},{"schema":{"type":"string"},"in":"query","name":"state","required":true},{"schema":{"type":"string"},"in":"query","name":"error","required":false},{"schema":{"type":"string","enum":["google","outlook"]},"in":"path","name":"provider","required":true}],"responses":{"302":{"description":"Redirect (Location header) to {PUBLIC_URL}/tech?calendar=connected|failed","content":{"application/json":{"schema":{"enum":["null"],"nullable":true,"description":"Redirect (Location header) to {PUBLIC_URL}/tech?calendar=connected|failed"}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/calendar/connect":{"post":{"summary":"Link my Google or Outlook calendar","tags":["Field app"],"description":"Returns the sign-in URL to open. After consent, busy times in that calendar block bookings (next 21 days), and my CrewLink jobs appear in it as free time (so they never block themselves). 503 if the provider isn't set up on this server.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","enum":["google","outlook"]}},"required":["provider"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/v1/tech/calendar":{"get":{"summary":"My linked calendars","tags":["Field app"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"calendars":{"type":"array","items":{"type":"object","properties":{"provider":{"type":"string","enum":["google","outlook"]},"account":{"type":"string","nullable":true},"status":{"type":"string"},"lastSyncAt":{"type":"string","nullable":true},"lastError":{"type":"string","nullable":true}},"required":["provider","account","status","lastSyncAt","lastError"],"additionalProperties":false}}},"required":["calendars"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/calendar/sync":{"post":{"summary":"Sync my calendars now","tags":["Field app"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"calendars":{"type":"array","items":{"type":"object","properties":{"provider":{"type":"string","enum":["google","outlook"]},"account":{"type":"string","nullable":true},"status":{"type":"string"},"lastSyncAt":{"type":"string","nullable":true},"lastError":{"type":"string","nullable":true}},"required":["provider","account","status","lastSyncAt","lastError"],"additionalProperties":false}}},"required":["calendars"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/calendar/{provider}":{"delete":{"summary":"Unlink a calendar","tags":["Field app"],"description":"Busy times imported from that calendar stop blocking bookings right away (from now on; past ones stay for the record).","parameters":[{"schema":{"type":"string","enum":["google","outlook"]},"in":"path","name":"provider","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"removed":{"type":"boolean"}},"required":["removed"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/calendars":{"get":{"summary":"Who has linked a calendar, and whether it's syncing","tags":["Office"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"calendars":{"type":"array","items":{"type":"object","properties":{"provider":{"type":"string","enum":["google","outlook"]},"account":{"type":"string","nullable":true},"status":{"type":"string"},"lastSyncAt":{"type":"string","nullable":true},"lastError":{"type":"string","nullable":true},"personId":{"type":"number"}},"required":["provider","account","status","lastSyncAt","lastError","personId"],"additionalProperties":false}}},"required":["calendars"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/auth/start":{"post":{"summary":"Email me a sign-in code","tags":["Sign-in"],"description":"Always 202, whether or not the email belongs to a user (no account probing): the reply is sent before the account is looked up, so timing gives nothing away either. At most 5 codes an hour per user, and 5 requests a minute per IP (429 with Retry-After beyond that).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","maxLength":320}},"required":["email"],"additionalProperties":false}}}},"responses":{"202":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"sent":{"type":"boolean","enum":[true]}},"required":["sent"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/auth/verify":{"post":{"summary":"Trade the code for a session","tags":["Sign-in"],"description":"Returns a session token (`Bearer <session id>.<secret>`, 30 days). If the email belongs to users at more than one company, pass companyId to choose. 401 for a wrong, used or expired code; each code allows 5 tries. After 10 wrong codes in 24 hours the user is locked out of code sign-in until the oldest failure is a day old (still 401, so the lockout reveals nothing). 10 requests a minute per IP (429 with Retry-After beyond that).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","maxLength":320},"code":{"type":"string","pattern":"^\\d{6}$"},"companyId":{"type":"string","format":"uuid"}},"required":["email","code"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"expiresAt":{"type":"string"},"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string"},"personId":{"type":"number","nullable":true}},"required":["id","name","role","personId"],"additionalProperties":false},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"],"additionalProperties":false},"companies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"role":{"type":"string","description":"Their role there: owner, field_manager, dispatcher or tech"},"lastUsedAt":{"type":"string","nullable":true,"description":"When they last signed in there (ISO instant), or null if never"}},"required":["id","name","slug","role","lastUsedAt"],"additionalProperties":false},"description":"Every company this email can sign in at (an active user at each), the one used most recently first. More than one: ask which, and POST /v1/auth/switch to change"}},"required":["token","expiresAt","user","company","companies"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/auth/companies":{"get":{"summary":"The companies I can sign in at","tags":["Sign-in"],"description":"Every company the signed-in person's email has an active user at, the one used most recently first, and which one this sign-in is. Only a session that signed in with an emailed code (or switched from one) lists the others; any other sign-in (a user token, passkey, welcome link or invitation) lists this company only. 403 for a company API key or an act-as session (no person of their own); a platform key is not a sign-in (401).","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"current":{"type":"string","description":"Company id of this sign-in"},"companies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"role":{"type":"string","description":"Their role there: owner, field_manager, dispatcher or tech"},"lastUsedAt":{"type":"string","nullable":true,"description":"When they last signed in there (ISO instant), or null if never"}},"required":["id","name","slug","role","lastUsedAt"],"additionalProperties":false}}},"required":["current","companies"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/auth/switch":{"post":{"summary":"Switch to my sign-in at another company","tags":["Sign-in"],"description":"For a person whose email signs in at several companies (GET /v1/auth/companies): a new 30-day session as their user at `companyId`, only where the same email has an active user. Needs a session that signed in with an emailed code (POST /v1/auth/verify) or switched from one: anything else (a user token, passkey, welcome link or invitation sign-in) is 403 email_unproven. A session it was called with ends. 404 when that email has no active user there; 403 for a company API key or an act-as session; 401 for a platform key.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"companyId":{"type":"string","format":"uuid"}},"required":["companyId"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"expiresAt":{"type":"string"},"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string"},"personId":{"type":"number","nullable":true}},"required":["id","name","role","personId"],"additionalProperties":false},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"],"additionalProperties":false},"companies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"role":{"type":"string","description":"Their role there: owner, field_manager, dispatcher or tech"},"lastUsedAt":{"type":"string","nullable":true,"description":"When they last signed in there (ISO instant), or null if never"}},"required":["id","name","slug","role","lastUsedAt"],"additionalProperties":false},"description":"Every company this email can sign in at (an active user at each), the one used most recently first. More than one: ask which, and POST /v1/auth/switch to change"}},"required":["token","expiresAt","user","company","companies"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/auth/logout":{"post":{"summary":"End this session","tags":["Sign-in"],"description":"Also how a super admin leaves an act-as session (POST /v1/admin/companies/{id}/act-as): that's audited as act_as_end.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]}},"required":["ok"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/auth/passkeys/login/options":{"post":{"summary":"Start signing in with a passkey","tags":["Sign-in"],"description":"No key needed. Returns options for a discoverable passkey (the device offers the passkeys it has for this site) and a `loginId` to send back with the answer within 5 minutes. The options are the same whoever asks: `email` is optional, never looked up here, and only narrows the sign-in (the passkey must then belong to a user with that email). 10 requests a minute per IP (429 with Retry-After beyond that).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","maxLength":320}},"additionalProperties":false,"default":{}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"loginId":{"type":"string"},"options":{"type":"object","properties":{"challenge":{"type":"string"},"rpId":{"type":"string"},"timeout":{"type":"number"},"allowCredentials":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"transports":{"type":"array","items":{"type":"string"}}},"required":["id","type"],"additionalProperties":false}},"userVerification":{"type":"string"}},"required":["challenge"],"additionalProperties":true,"description":"PublicKeyCredentialRequestOptionsJSON: pass it to startAuthentication({ optionsJSON }) or PublicKeyCredential.parseRequestOptionsFromJSON()"}},"required":["loginId","options"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/auth/passkeys/login":{"post":{"summary":"Trade a passkey for a session","tags":["Sign-in"],"description":"Verifies the signature against the saved public key (and the signature counter, for authenticators that keep one: a counter that doesn't go up means a cloned key and is refused), then opens a 30-day session exactly like POST /v1/auth/verify. The loginId works once. 401 `challenge_expired` (no such loginId, already used, or older than 5 minutes: start again) or `bad_passkey` (unknown or removed passkey, user turned off, email mismatch, bad signature or counter), the same answer for all of those. 10 requests a minute per IP (429 with Retry-After beyond that).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"loginId":{"type":"string","minLength":1,"maxLength":64},"response":{"type":"object","properties":{"id":{"type":"string","maxLength":1024,"pattern":"^[A-Za-z0-9_-]*$","minLength":1},"rawId":{"type":"string","maxLength":1024,"pattern":"^[A-Za-z0-9_-]*$","minLength":1},"type":{"type":"string","enum":["public-key"]},"response":{"type":"object","properties":{"clientDataJSON":{"type":"string","maxLength":16384,"pattern":"^[A-Za-z0-9_-]*$"},"authenticatorData":{"type":"string","maxLength":16384,"pattern":"^[A-Za-z0-9_-]*$"},"signature":{"type":"string","maxLength":16384,"pattern":"^[A-Za-z0-9_-]*$"},"userHandle":{"type":"string","maxLength":1024,"pattern":"^[A-Za-z0-9_-]*$"}},"required":["clientDataJSON","authenticatorData","signature"],"additionalProperties":false},"authenticatorAttachment":{"type":"string","enum":["platform","cross-platform"]},"clientExtensionResults":{"type":"object","additionalProperties":{},"default":{}}},"required":["id","rawId","type","response"],"additionalProperties":false,"description":"What @simplewebauthn/browser startAuthentication() (or PublicKeyCredential.toJSON()) returns"}},"required":["loginId","response"],"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"expiresAt":{"type":"string"},"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string"},"personId":{"type":"number","nullable":true}},"required":["id","name","role","personId"],"additionalProperties":false},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"],"additionalProperties":false},"companies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"},"role":{"type":"string","description":"Their role there: owner, field_manager, dispatcher or tech"},"lastUsedAt":{"type":"string","nullable":true,"description":"When they last signed in there (ISO instant), or null if never"}},"required":["id","name","slug","role","lastUsedAt"],"additionalProperties":false},"description":"Every company this email can sign in at (an active user at each), the one used most recently first. More than one: ask which, and POST /v1/auth/switch to change"}},"required":["token","expiresAt","user","company","companies"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/auth/passkeys/options":{"post":{"summary":"Start adding a passkey","tags":["Sign-in"],"description":"Registration options for the signed-in user (session or user token): RP \"CrewLink\" on this site, the user id as the user handle, their existing passkeys excluded, resident key and user verification preferred, no attestation. Pass `options` to the browser, then send what it returns to POST /v1/auth/passkeys within 5 minutes. 403 `needs_user` with the company API key.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"options":{"type":"object","properties":{"challenge":{"type":"string"},"rp":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["name"],"additionalProperties":false},"user":{"type":"object","properties":{"id":{"type":"string","description":"base64url of the user id"},"name":{"type":"string"},"displayName":{"type":"string"}},"required":["id","name","displayName"],"additionalProperties":false},"pubKeyCredParams":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"alg":{"type":"number"}},"required":["type","alg"],"additionalProperties":false}},"timeout":{"type":"number"},"excludeCredentials":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"transports":{"type":"array","items":{"type":"string"}}},"required":["id","type"],"additionalProperties":false}},"authenticatorSelection":{"type":"object","properties":{"authenticatorAttachment":{"type":"string"},"residentKey":{"type":"string"},"requireResidentKey":{"type":"boolean"},"userVerification":{"type":"string"}},"additionalProperties":false},"attestation":{"type":"string"}},"required":["challenge","rp","user","pubKeyCredParams"],"additionalProperties":true,"description":"PublicKeyCredentialCreationOptionsJSON: pass it to startRegistration({ optionsJSON }) or PublicKeyCredential.parseCreationOptionsFromJSON()"}},"required":["options"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/auth/passkeys":{"post":{"summary":"Save the new passkey","tags":["Sign-in"],"description":"Verifies what the browser returned for the options from POST /v1/auth/passkeys/options (the challenge works once, for 5 minutes) and keeps the public key. `name` defaults to the browser and device (\"Chrome on Mac\"). 400 `challenge_expired` (start again) or `bad_passkey` (didn't verify); 409 `already_added` or `too_many` (20 per person).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"response":{"type":"object","properties":{"id":{"type":"string","maxLength":1024,"pattern":"^[A-Za-z0-9_-]*$","minLength":1},"rawId":{"type":"string","maxLength":1024,"pattern":"^[A-Za-z0-9_-]*$","minLength":1},"type":{"type":"string","enum":["public-key"]},"response":{"type":"object","properties":{"clientDataJSON":{"type":"string","maxLength":16384,"pattern":"^[A-Za-z0-9_-]*$"},"attestationObject":{"type":"string","maxLength":65536,"pattern":"^[A-Za-z0-9_-]*$"},"authenticatorData":{"type":"string","maxLength":65536,"pattern":"^[A-Za-z0-9_-]*$"},"transports":{"type":"array","items":{"type":"string","maxLength":32},"maxItems":10},"publicKeyAlgorithm":{"type":"number"},"publicKey":{"type":"string","maxLength":16384,"pattern":"^[A-Za-z0-9_-]*$"}},"required":["clientDataJSON","attestationObject"],"additionalProperties":false},"authenticatorAttachment":{"type":"string","enum":["platform","cross-platform"]},"clientExtensionResults":{"type":"object","additionalProperties":{},"default":{}}},"required":["id","rawId","type","response"],"additionalProperties":false,"description":"What @simplewebauthn/browser startRegistration() (or PublicKeyCredential.toJSON()) returns"},"name":{"type":"string","minLength":1,"maxLength":60}},"required":["response"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"passkey":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"transports":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string"},"lastUsedAt":{"type":"string","nullable":true}},"required":["id","name","transports","createdAt","lastUsedAt"],"additionalProperties":false}},"required":["passkey"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"My passkeys","tags":["Sign-in"],"description":"Only your own, oldest first.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"passkeys":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"transports":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string"},"lastUsedAt":{"type":"string","nullable":true}},"required":["id","name","transports","createdAt","lastUsedAt"],"additionalProperties":false}}},"required":["passkeys"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/auth/passkeys/{id}":{"delete":{"summary":"Remove a passkey","tags":["Sign-in"],"description":"It stops working for sign-in at once (the copy on the device stays until it's deleted there). Sessions it opened keep going until they end. 404 unless it's one of yours.","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"removed":{"type":"boolean","enum":[true]}},"required":["removed"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/me/tour":{"get":{"summary":"Where you are in the in-app tours (any role)","tags":["Users"],"description":"The signed-in person's progress through each walkthrough: the step they were on, and whether they finished or skipped it. Any user or session token (owner, dispatcher or tech) sees only their own. The company API key gets an empty list and `stored: false`.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"tours":{"type":"object","additionalProperties":{"type":"object","properties":{"step":{"type":"integer","description":"The step they were on (0 is the first). Resume here."},"completedAt":{"type":"string","nullable":true,"description":"When they finished it (ISO instant), or null"},"skippedAt":{"type":"string","nullable":true,"description":"When they skipped it (ISO instant), or null"},"updatedAt":{"type":"string","description":"Last change (ISO instant)"}},"required":["step","completedAt","skippedAt","updatedAt"],"additionalProperties":false},"description":"One entry per tour they have seen, by tour id. A tour with no entry has never been shown: the apps start it on first sign-in."},"stored":{"type":"boolean","description":"false for the company API key, which has no person to remember it for: reads are empty and writes are not kept"}},"required":["tours","stored"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"patch":{"summary":"Save your place in a tour (any role)","tags":["Users"],"description":"Records one tour's progress for the signed-in person. `step` saves the step they are on. `status`: `active` (started, restarted or resumed: clears completedAt and skippedAt), `completed` or `skipped`. Returns every tour, as GET does. With the company API key nothing is kept and the answer has `stored: false`. 422 when a person already has state for 20 other tours.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tour":{"type":"string","pattern":"^[a-z][a-z0-9-]{0,39}$","description":"Which tour, e.g. office, tech, partner"},"step":{"type":"integer","minimum":0,"maximum":200},"status":{"type":"string","enum":["active","completed","skipped"]}},"required":["tour"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"tours":{"type":"object","additionalProperties":{"type":"object","properties":{"step":{"type":"integer","description":"The step they were on (0 is the first). Resume here."},"completedAt":{"type":"string","nullable":true,"description":"When they finished it (ISO instant), or null"},"skippedAt":{"type":"string","nullable":true,"description":"When they skipped it (ISO instant), or null"},"updatedAt":{"type":"string","description":"Last change (ISO instant)"}},"required":["step","completedAt","skippedAt","updatedAt"],"additionalProperties":false},"description":"One entry per tour they have seen, by tour id. A tour with no entry has never been shown: the apps start it on first sign-in."},"stored":{"type":"boolean","description":"false for the company API key, which has no person to remember it for: reads are empty and writes are not kept"}},"required":["tours","stored"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/parts":{"post":{"summary":"Add a part","tags":["Inventory"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sku":{"type":"string","minLength":1,"maxLength":60},"name":{"type":"string","minLength":1,"maxLength":200},"unitCostCents":{"type":"integer","minimum":0},"priceCents":{"type":"integer","minimum":0},"reorderAt":{"type":"integer","minimum":0,"default":0}},"required":["sku","name","unitCostCents","priceCents"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"sku":{"type":"string"},"name":{"type":"string"},"unitCostCents":{"type":"number"},"priceCents":{"type":"number"},"reorderAt":{"type":"number"},"onHand":{"type":"number"},"low":{"type":"boolean"}},"required":["id","sku","name","unitCostCents","priceCents","reorderAt","onHand","low"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"get":{"summary":"Parts with total on hand; low = at or under the reorder point","tags":["Inventory"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"parts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"sku":{"type":"string"},"name":{"type":"string"},"unitCostCents":{"type":"number"},"priceCents":{"type":"number"},"reorderAt":{"type":"number"},"onHand":{"type":"number"},"low":{"type":"boolean"}},"required":["id","sku","name","unitCostCents","priceCents","reorderAt","onHand","low"],"additionalProperties":false}}},"required":["parts"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/stock":{"put":{"summary":"Set a count (after a stock take)","tags":["Inventory"],"description":"holder: 0 for the warehouse, or a person id for their truck.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"partId":{"type":"integer"},"holder":{"type":"integer","minimum":0},"qty":{"type":"integer","minimum":0}},"required":["partId","holder","qty"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"partId":{"type":"number"},"holder":{"type":"number"},"qty":{"type":"number"}},"required":["partId","holder","qty"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/stock/transfer":{"post":{"summary":"Move stock (warehouse to truck, truck to truck)","tags":["Inventory"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"partId":{"type":"integer"},"from":{"type":"integer","minimum":0},"to":{"type":"integer","minimum":0},"qty":{"type":"integer","minimum":1}},"required":["partId","from","to","qty"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"200":{"description":"Quantities after the move","content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"number"},"to":{"type":"number"}},"required":["from","to"],"additionalProperties":false,"description":"Quantities after the move"}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/jobs/{id}/pnl":{"get":{"summary":"Profit and loss for one job","tags":["Reports"],"description":"Revenue: the invoice total if there is one, else the service list price plus parts at price. Costs: parts at cost; labor at each person's loaded hourly cost for time on site (as tapped, else as booked) plus the drive in; vehicle cost per km of the drive in; the 1% platform fee on money collected.","parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"revenueCents":{"type":"number"},"costs":{"type":"object","properties":{"partsCents":{"type":"number"},"laborCents":{"type":"number"},"vehicleCents":{"type":"number"},"platformFeeCents":{"type":"number"}},"required":["partsCents","laborCents","vehicleCents","platformFeeCents"],"additionalProperties":false},"profitCents":{"type":"number"},"marginPct":{"type":"number","nullable":true},"basis":{"type":"object","properties":{"revenue":{"type":"string","enum":["invoice","list_price"]},"laborMinutes":{"type":"number"},"driveKm":{"type":"number"}},"required":["revenue","laborMinutes","driveKm"],"additionalProperties":false}},"required":["jobId","revenueCents","costs","profitCents","marginPct","basis"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/vehicles":{"post":{"summary":"Add a vehicle","tags":["Fleet"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"plate":{"type":"string","maxLength":20},"personId":{"type":"integer"},"costPerKmCents":{"type":"integer","minimum":0,"default":40},"odometerKm":{"type":"integer","minimum":0}},"required":["name"],"additionalProperties":false}}}},"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON; company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/office/fleet":{"get":{"summary":"Vehicles, who has them, and where they are (from the driver’s phone)","tags":["Fleet"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"vehicles":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"plate":{"type":"string","nullable":true},"personId":{"type":"number","nullable":true},"tech":{"type":"string","nullable":true},"costPerKmCents":{"type":"number"},"location":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"},"at":{"type":"string"}},"required":["lat","lng","at"],"additionalProperties":false,"nullable":true}},"required":["id","name","plate","personId","tech","costPerKmCents","location"],"additionalProperties":false}}},"required":["vehicles"],"additionalProperties":false}}}},"400":{"description":"company_required: a platform key without X-CrewLink-Company","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"forbidden: your role (or a platform key) cannot call this; scope: the platform grant lacks the scope it needs","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"not_found: nothing with that id in your company, or a company that has not granted this platform access","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/ops/travel":{"get":{"summary":"The travel model: road router(s), cache, calibration budget, regional profiles (owner or super admin)","tags":["Operations"],"description":"How drive times are worked out right now. `router.kind` is `osrm` (road minutes from the self-hosted OpenStreetMap router) or `straight_line` (distance-based estimate, when no router is set up). `cache` counts road-minute lookups answered from memory. `calibration` is today's (UTC) use of the paid traffic lookups that teach the rush-hour model, against the daily cap. `router.regions` lists each road dataset (one per region on a multi-region server, e.g. `illinois` and `colorado`, with its bounding box); a drive no dataset covers both ends of is priced by the straight-line estimate. `metros` lists each region's latest congestion profile version: a region is one of ~390 listed metros worldwide (`us-chicago`, `gb-london`, `de-berlin`) or, outside them, a ~50 km grid cell (`cell-<row>-<col>`). `yourMetro` is your main branch's region and `yourRegions` every branch's (a branch plans with its own region's profile, else the built-in rush-hour curve). Platform-wide numbers, no other company's data.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"router":{"type":"object","properties":{"kind":{"type":"string","enum":["osrm","straight_line"]},"regions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"dataVersion":{"type":"string","nullable":true},"bbox":{"type":"array","minItems":4,"maxItems":4,"items":[{"type":"number"},{"type":"number"},{"type":"number"},{"type":"number"}],"nullable":true,"description":"[minLng, minLat, maxLng, maxLat]; null: everywhere"}},"required":["key","dataVersion","bbox"],"additionalProperties":false},"description":"Road datasets, smallest area first"},"dataVersion":{"type":"string","nullable":true,"description":"OSRM dataset, e.g. 'illinois-20261011' (comma-separated with several regions)"},"datasetDate":{"type":"string","nullable":true,"description":"YYYY-MM-DD the map data was built"},"lastOkAt":{"type":"string","nullable":true},"lastError":{"type":"string","nullable":true},"resting":{"type":"boolean","description":"Paused for 30 s after repeated failures (straight line meanwhile)"}},"required":["kind","regions","dataVersion","datasetDate","lastOkAt","lastError","resting"],"additionalProperties":false},"cache":{"type":"object","properties":{"hits":{"type":"number"},"misses":{"type":"number"},"hitRate":{"type":"number","nullable":true},"size":{"type":"number"},"capacity":{"type":"number"},"requests":{"type":"number"},"errors":{"type":"number"}},"required":["hits","misses","hitRate","size","capacity","requests","errors"],"additionalProperties":false},"lastAttach":{"type":"object","properties":{"at":{"type":"string"},"ms":{"type":"number"},"points":{"type":"number"},"pairs":{"type":"number"},"missing":{"type":"number"}},"required":["at","ms","points","pairs","missing"],"additionalProperties":false,"nullable":true,"description":"The latest schedule search: how long fetching road minutes took and how many pairs it covered"},"calibration":{"type":"object","properties":{"enabled":{"type":"boolean"},"reason":{"type":"string","nullable":true},"usedToday":{"type":"number"},"cap":{"type":"number"},"lastRunAt":{"type":"string","nullable":true},"lastRunLookups":{"type":"number","nullable":true}},"required":["enabled","reason","usedToday","cap","lastRunAt","lastRunLookups"],"additionalProperties":false},"yourMetro":{"type":"string"},"yourRegions":{"type":"array","items":{"type":"object","properties":{"branch":{"type":"string"},"region":{"type":"string"},"name":{"type":"string"}},"required":["branch","region","name"],"additionalProperties":false},"description":"Each branch's congestion region"},"metros":{"type":"array","items":{"type":"object","properties":{"metro":{"type":"string"},"name":{"type":"string"},"version":{"type":"number"},"observations":{"type":"number"},"updatedAt":{"type":"string"}},"required":["metro","name","version","observations","updatedAt"],"additionalProperties":false}}},"required":["router","cache","lastAttach","calibration","yourMetro","yourRegions","metros"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/ops/brain":{"get":{"summary":"The brain: training runs, tunables in use per metro, real days exported (owner or super admin)","tags":["Operations"],"description":"Phase 3 of the brain: a simulator on AWS replays days in each metro (real building locations, road minutes and rush hours, plus real finished days from the plan-vs-actual ledger, anonymised) and searches the scheduling engine's tunables (buffer before each visit, drive-time margin, how full a day may get, running-late hand-offs). A metro's new set is published only if it beats the one in use on the same simulated days and breaks none of the engine's pass targets. `runs`: the last 10 training runs, newest first, with each metro's score before and after (on-time jobs per tech-day, net of driving and broken promises) and on-time rate. `params`: the set each metro plans with right now (`version` null = the engine's defaults); `yourMetro` is yours. `realDays`: finished days exported for training so far. Platform-wide numbers, no other company's data.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"store":{"type":"object","properties":{"kind":{"type":"string","enum":["s3","file"],"nullable":true,"description":"null: training data not set up (TRAIN_STORE), everyone plans with the defaults"},"error":{"type":"string","nullable":true}},"required":["kind","error"],"additionalProperties":false},"yourMetro":{"type":"string"},"runs":{"type":"array","items":{"type":"object","properties":{"runId":{"type":"string"},"version":{"type":"string"},"sha":{"type":"string"},"publishedAt":{"type":"string","nullable":true},"seconds":{"type":"number"},"metros":{"type":"array","items":{"type":"object","properties":{"metro":{"type":"string"},"name":{"type":"string"},"version":{"type":"string"},"changed":{"type":"boolean"},"scoreBefore":{"type":"number"},"scoreAfter":{"type":"number"},"delta":{"type":"number"},"onTimeBefore":{"type":"number"},"onTimeAfter":{"type":"number"},"realDays":{"type":"number"}},"required":["metro","name","version","changed","scoreBefore","scoreAfter","delta","onTimeBefore","onTimeAfter","realDays"],"additionalProperties":false}}},"required":["runId","version","sha","publishedAt","seconds","metros"],"additionalProperties":false}},"params":{"type":"array","items":{"type":"object","properties":{"metro":{"type":"string"},"name":{"type":"string"},"version":{"type":"string","nullable":true},"changed":{"type":"boolean"},"params":{"type":"object","additionalProperties":{"type":"number"},"description":"The engine tunables (cl-core Params; fields left out are the defaults)","nullable":true},"checkedAt":{"type":"string"},"error":{"type":"string","nullable":true}},"required":["metro","name","version","changed","params","checkedAt","error"],"additionalProperties":false}},"realDays":{"type":"object","properties":{"total":{"type":"number"},"byMetro":{"type":"object","additionalProperties":{"type":"number"}},"lastAt":{"type":"string","nullable":true}},"required":["total","byMetro","lastAt"],"additionalProperties":false}},"required":["store","yourMetro","runs","params","realDays"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/stock":{"get":{"summary":"What's on my truck","tags":["Field app"],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"stock":{"type":"array","items":{"type":"object","properties":{"partId":{"type":"number"},"sku":{"type":"string"},"name":{"type":"string"},"qty":{"type":"number"}},"required":["partId","sku","name","qty"],"additionalProperties":false}}},"required":["stock"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/tech/jobs/{id}/parts":{"post":{"summary":"Log parts used on a job (taken off my truck)","tags":["Field app"],"description":"They're added to the job's invoice and P&L. 409 if your truck doesn't have that many (count it, or ask the office to fix the count). `at`: when they were used, for entries queued offline; send an Idempotency-Key so a retry takes them off the truck once.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"partId":{"type":"integer"},"qty":{"type":"integer","minimum":1,"maximum":1000},"at":{"type":"string","format":"date-time","description":"When the tech actually tapped (ISO 8601), for taps queued offline. Up to 24 hours back (422 invalid_at otherwise); default now"}},"required":["partId","qty"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"id","required":true},{"schema":{"type":"string","minLength":1,"maxLength":200,"pattern":"^[\\w.:-]+$"},"in":"header","name":"idempotency-key","required":false,"description":"Any unique string per action (a UUID is fine). Repeats within 24 hours return the first response"}],"security":[{"officeKey":[]}],"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"number"},"leftOnTruck":{"type":"number"}},"required":["id","leftOnTruck"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/admin/companies":{"get":{"summary":"Every company, newest activity first (super admin)","tags":["Admin"],"description":"Search with `q` (name or slug, case-insensitive) and narrow with `kind`. Each row: plan and its status, whether it's a demo account, active users and techs, jobs (all time and created in the last 30 days), created and last activity (a job created or a sign-in session started). Up to `limit` rows (default 100, at most 500). Counts only: no customer or job details.","parameters":[{"schema":{"type":"string","maxLength":100},"in":"query","name":"q","required":false},{"schema":{"type":"string","enum":["company","individual","client","platform"]},"in":"query","name":"kind","required":false},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"in":"query","name":"limit","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"companies":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"kind":{"type":"string","description":"company | individual | client | platform (CrewLink itself)"},"plan":{"type":"string","nullable":true,"description":"CrewLink plan code (lib/billing.ts), null when it has no subscription row yet"},"planStatus":{"type":"string","nullable":true},"demo":{"type":"boolean","description":"A demo account (demo-fill filled it)"},"managedBy":{"type":"string","nullable":true,"description":"Client companies: the provider that set it up (company id)"},"counts":{"type":"object","properties":{"users":{"type":"number","description":"Active sign-ins"},"techs":{"type":"number","description":"Active people on the schedule"},"jobs":{"type":"number"},"jobs30d":{"type":"number","description":"Jobs created in the last 30 days"}},"required":["users","techs","jobs","jobs30d"],"additionalProperties":false},"createdAt":{"type":"string"},"lastActivityAt":{"type":"string","nullable":true,"description":"Latest of: a job created, a sign-in session started"}},"required":["id","slug","name","kind","plan","planStatus","demo","managedBy","counts","createdAt","lastActivityAt"],"additionalProperties":false}},"total":{"type":"number","description":"Companies matching, before limit"}},"required":["companies","total"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/admin/companies/{id}/act-as":{"post":{"summary":"Open a 1-hour session as a company's owner (super admin)","tags":["Admin"],"description":"Returns a session token (`Bearer <session id>.<secret>`) that acts as the company's owner for 60 minutes, to see and fix what they see. It is audited: this call, every request made with it (method, path, status) and its end (POST /v1/auth/logout) each write an admin_actions row with your user id (GET /v1/admin/actions). While acting it can't reach money-moving or key-minting routes (payouts, payments, plan and billing changes, closing or paying pay periods, API, integration and sign-in tokens, platform grants, passkeys) or the admin area (403 acting_as), and nothing is filed in your name (it acts like the company API key). It stops working at once if you stop being a super admin. `reason` goes in the audit row. 404 for an unknown company; 422 for CrewLink's own company.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":300,"description":"Why (support ticket, demo, ...): kept in the audit log"}},"additionalProperties":false,"default":{}}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string"},"expiresAt":{"type":"string"},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"],"additionalProperties":false}},"required":["token","expiresAt","company"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/admin/platforms":{"get":{"summary":"Every platform with its grants (super admin)","tags":["Admin"],"description":"Platforms (integrators, made with `cli.ts platform-create`), whether the key is disabled, calls in the last 7 days, and every grant (live and revoked, newest first) with the company and scopes.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"platforms":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"createdAt":{"type":"string"},"disabledAt":{"type":"string","nullable":true},"calls7d":{"type":"number"},"grants":{"type":"array","items":{"type":"object","properties":{"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"],"additionalProperties":false},"scopes":{"type":"array","items":{"type":"string"}},"grantedAt":{"type":"string"},"revokedAt":{"type":"string","nullable":true},"grantedBy":{"type":"string","nullable":true}},"required":["company","scopes","grantedAt","revokedAt","grantedBy"],"additionalProperties":false}}},"required":["id","slug","name","createdAt","disabledAt","calls7d","grants"],"additionalProperties":false}}},"required":["platforms"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/admin/users":{"get":{"summary":"CrewLink staff: the people of CrewLink's own company (super admin)","tags":["Admin"],"description":"Add someone with POST /v1/office/users while signed in to CrewLink, then promote them with POST /v1/admin/users/{id}/superadmin.","security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"users":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string"},"active":{"type":"boolean"},"superAdmin":{"type":"boolean"},"inviteSentAt":{"type":"string","nullable":true}},"required":["id","name","email","role","active","superAdmin","inviteSentAt"],"additionalProperties":false}}},"required":["users"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/admin/users/{id}/superadmin":{"post":{"summary":"Promote or demote a super admin (super admin)","tags":["Admin"],"description":"Only people of CrewLink's own company can be super admins (404 for anyone else). Promoting needs an active user. Demoting the last active super admin is refused (409 last_super_admin); demoting ends their act-as sessions at once. Audited (superadmin_grant / superadmin_revoke).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"superAdmin":{"type":"boolean"}},"required":["superAdmin"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","format":"uuid"},"in":"path","name":"id","required":true}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true},"role":{"type":"string"},"active":{"type":"boolean"},"superAdmin":{"type":"boolean"},"inviteSentAt":{"type":"string","nullable":true}},"required":["id","name","email","role","active","superAdmin","inviteSentAt"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/admin/actions":{"get":{"summary":"The admin audit log, newest first (super admin)","tags":["Admin"],"description":"act_as_start / act_as_end (with the reason given), act_as_request (each request under an act-as session: method, path, status), superadmin_grant / superadmin_revoke. Filter by `companyId` or `adminUserId`; `before` (an id) pages back. Up to `limit` rows (default 100, at most 500).","parameters":[{"schema":{"type":"string","format":"uuid"},"in":"query","name":"companyId","required":false},{"schema":{"type":"string","format":"uuid"},"in":"query","name":"adminUserId","required":false},{"schema":{"type":"integer"},"in":"query","name":"before","required":false},{"schema":{"type":"integer","minimum":1,"maximum":500,"default":100},"in":"query","name":"limit","required":false}],"security":[{"officeKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"actions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"number"},"admin":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string","nullable":true}},"required":["id","name","email"],"additionalProperties":false},"company":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"slug":{"type":"string"}},"required":["id","name","slug"],"additionalProperties":false,"nullable":true},"action":{"type":"string","enum":["act_as_start","act_as_end","act_as_request","superadmin_grant","superadmin_revoke"]},"method":{"type":"string","nullable":true},"path":{"type":"string","nullable":true},"status":{"type":"number","nullable":true},"sessionId":{"type":"string","nullable":true},"detail":{"type":"object","additionalProperties":{}},"at":{"type":"string"}},"required":["id","admin","company","action","method","path","status","sessionId","detail","at"],"additionalProperties":false}}},"required":["actions"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/offers/{jobId}":{"get":{"summary":"The new times offered to a customer whose booking had to move","tags":["Customer"],"description":"From the texted link (valid 48 hours). Times that have since been taken are left out; if none are left, fresh ones are offered.","parameters":[{"schema":{"type":"string","minLength":10},"in":"query","name":"token","required":true},{"schema":{"type":"integer"},"in":"path","name":"jobId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"company":{"type":"string"},"service":{"type":"string"},"status":{"type":"string"},"timezone":{"type":"string","description":"IANA zone of each option's local time (where the job is)"},"options":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string"},"local":{"type":"string"}},"required":["start","local"],"additionalProperties":false}}},"required":["company","service","status","timezone","options"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}},"post":{"summary":"Pick one of the offered times","tags":["Customer"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":10},"start":{"type":"string","format":"date-time"}},"required":["token","start"],"additionalProperties":false}}}},"parameters":[{"schema":{"type":"integer"},"in":"path","name":"jobId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"jobId":{"type":"number"},"local":{"type":"string"},"techs":{"type":"array","items":{"type":"string"}}},"required":["jobId","local","techs"],"additionalProperties":false}}}},"400":{"description":"bad_request: the body is not valid JSON","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"409":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/public/geocode":{"get":{"summary":"Find an address (for the booking page)","tags":["Booking page"],"description":"Up to 5 matches, biased to the area of the company in `slug` and limited to its countries (its own and every branch's); without `slug`, anywhere. Each match has `country` (ISO-2) and `lines` (the address in that country's order, e.g. '10115 Berlin' or 'Chicago, IL 60657') when the geocoder gave the parts. Answered by Mapbox when the server has a Mapbox token, otherwise (or when Mapbox fails) by OpenStreetMap; `source` says which. One new lookup a second per IP, and 30 a minute per IP or 300 per integration key (X-CrewLink-Key), 429 beyond that. Cached answers are always served.","parameters":[{"schema":{"type":"string","minLength":4,"maxLength":200},"in":"query","name":"q","required":true},{"schema":{"type":"string","maxLength":100},"in":"query","name":"slug","required":false},{"schema":{"type":"string","maxLength":64},"in":"query","name":"share","required":false,"description":"A client share link token (/r/<token>): biased to the area of the company that link books, as slug is"},{"schema":{"type":"string","pattern":"^[A-Za-z]{2}$"},"in":"query","name":"country","required":false,"description":"One more country (ISO-2) to search, e.g. while adding a branch abroad"}],"security":[{},{"integrationKey":[]}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string","description":"The address in one line. When the geocoder dropped the house number or directional you typed (and found only that street), they are put back here"},"lat":{"type":"number"},"lng":{"type":"number"},"precision":{"type":"string","enum":["rooftop","address","street","locality"],"description":"How exact the match is: rooftop (the building), address (the house number, placed along its street), street (only the street: that number wasn't found) or locality (an area, town or postcode). Send it as geoPrecision when you book; street and locality need allowApproximate: true on POST /v1/office/jobs and the booking page"},"country":{"type":"string","description":"ISO 3166-1 alpha-2"},"lines":{"type":"array","items":{"type":"string"},"description":"The address in its country's order, one line each"}},"required":["label","lat","lng","precision"],"additionalProperties":false}},"source":{"type":"string","enum":["mapbox","osm"],"description":"Who answered: 'mapbox' or 'osm' (OpenStreetMap). Absent when no geocoder answered (results is then empty)"}},"required":["results"],"additionalProperties":false}}}},"422":{"description":"invalid_request: the body, query or path failed validation; `message` says which","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"429":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}}}}},"/v1/health":{"get":{"summary":"Is the API up (for uptime monitors)","tags":["Health"],"description":"No key. 200 `ok: true` when the API answers and its database answers within 3 seconds; 503 `ok: false, db: \"down\"` otherwise. Also says which release is running. Never cached.","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"true when the API answers and its database does"},"db":{"type":"string","enum":["ok","down"]},"release":{"type":"string","nullable":true,"description":"The running release id (the runner run id), null in development"},"uptimeSeconds":{"type":"number","description":"Seconds since this API process started"},"time":{"type":"string","description":"Server time (UTC)"}},"required":["ok","db","release","uptimeSeconds","time"],"additionalProperties":false}}}},"429":{"description":"rate_limited: too many requests; wait Retry-After seconds","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"500":{"description":"server_error: something went wrong on our side; retry with backoff","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":true}}}},"503":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","description":"true when the API answers and its database does"},"db":{"type":"string","enum":["ok","down"]},"release":{"type":"string","nullable":true,"description":"The running release id (the runner run id), null in development"},"uptimeSeconds":{"type":"number","description":"Seconds since this API process started"},"time":{"type":"string","description":"Server time (UTC)"}},"required":["ok","db","release","uptimeSeconds","time"],"additionalProperties":false}}}}}}},"/og/{file}":{"get":{"summary":"A CrewLink link-preview card (PNG, 1200x630)","tags":["Link previews"],"description":"The Open Graph and Twitter image the web app's pages point at. file is {kind}.png, kind one of default, book, appointment, offer, invoice, review, docs, notfound. Cached a day (ETag; If-None-Match answers 304).","parameters":[{"schema":{"type":"string","enum":["dark","light"],"default":"dark"},"in":"query","name":"theme","required":false,"description":"dark (near-black, the default) or light"},{"schema":{"type":"string","maxLength":40},"in":"query","name":"v","required":false,"description":"Cache buster the page tags add; ignored"},{"schema":{"type":"string"},"in":"path","name":"file","required":true,"description":"'{kind}.png', e.g. 'default.png'"}],"responses":{"200":{"description":"image/png, 1200x630","content":{"image/png":{"schema":{"type":"string","description":"image/png, 1200x630"}}}},"304":{"description":"Not modified: If-None-Match matched the ETag","content":{"image/png":{"schema":{"enum":["null"],"nullable":true,"description":"Not modified: If-None-Match matched the ETag"}}}},"404":{"description":"Default Response","content":{"image/png":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}},"/og/company/{file}":{"get":{"summary":"A company's link-preview card (PNG, 1200x630)","tags":["Link previews"],"description":"file is {slug}.png. The company name in big type, on the card for its booking page (kind book, the default) or for a customer link (appointment, offer, invoice, review): the business name only, never anything about the customer. A name the card fonts can't draw gets the card without it. Cached a day (ETag; If-None-Match answers 304).","parameters":[{"schema":{"type":"string","enum":["book","appointment","offer","invoice","review"],"default":"book"},"in":"query","name":"kind","required":false},{"schema":{"type":"string","enum":["dark","light"],"default":"dark"},"in":"query","name":"theme","required":false,"description":"dark (near-black, the default) or light"},{"schema":{"type":"string","maxLength":40},"in":"query","name":"v","required":false,"description":"Cache buster the page tags add; ignored"},{"schema":{"type":"string"},"in":"path","name":"file","required":true,"description":"'{slug}.png', e.g. 'acme.png'"}],"responses":{"200":{"description":"image/png, 1200x630","content":{"image/png":{"schema":{"type":"string","description":"image/png, 1200x630"}}}},"304":{"description":"Not modified: If-None-Match matched the ETag","content":{"image/png":{"schema":{"enum":["null"],"nullable":true,"description":"Not modified: If-None-Match matched the ETag"}}}},"404":{"description":"Default Response","content":{"image/png":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}},"required":["error","message"],"additionalProperties":false}}}}}}}},"tags":[{"name":"Booking page","description":"What a company's booking page calls: services, open times at an address, address search, holds and bookings. No key needed."},{"name":"Share links","description":"Client share links: an unguessable booking URL (/r/<token>) a client company's sales reps open to book site surveys with you for that client, no account needed. The office (owner or dispatcher) and platforms with write manage them under a client; the page calls the public routes here. Guide: [Booking page guide](/docs/guides/booking-page#client-share-links)."},{"name":"Customer","description":"The homeowner's links: view, move or cancel a booking (manage token), pick a newly offered time, view and pay an invoice, leave a review."},{"name":"Sign-in","description":"Email code sign-in for people (owners, field managers, dispatchers, techs), and accepting an invitation link. Returns a 30-day session token."},{"name":"Office","description":"The office schedule and setup: jobs, team, branches, services, time off, live board, emergencies, calendar view, settings and the company API key. Owner, field manager or dispatcher; people with a team or own scope see only their requests and jobs."},{"name":"Intake","description":"Every job in one place: the intake queue (jobs with no time yet, oldest first, with dispatch risks), job statuses with their overdue and needs-attention flags, the job list as CSV, and placing a waiting job with the closest techs first. Owner, field manager or dispatcher; scoped people see only their own jobs."},{"name":"Users","description":"People who sign in, their roles (owner, field manager, dispatcher, tech), scopes, teams and tokens, one-time invitation links, and each person's own preferences (time zone, theme, colour filter). Changes are owner only, except invitations: field managers invite dispatchers to their team, dispatchers invite techs."},{"name":"Clients","description":"Client companies that file service requests into yours (an installer booking site surveys, say): set them up, invite their people, issue their API key, turn them off. A client files requests and follows them (status, time, the tech's first name) and sees your open times, nothing else."},{"name":"Messaging","description":"Messages between the people of a company: the company channel, a thread per job, each tech's line to the office, direct messages, and a channel with each CrewConnect partner. Who can message whom follows your company's rules. Customers are texted, not messaged here."},{"name":"Field app","description":"The tech's day: stages, check-in, location, photos, parts, calls and notes. Tech tokens see only their own jobs."},{"name":"Trips","description":"Out-of-area requests from the booking page and the trips that serve them."},{"name":"Recurring","description":"Jobs that repeat on a schedule."},{"name":"Inventory","description":"Parts, stock and what each job used."},{"name":"Fleet","description":"Vehicles, who has them and where they are."},{"name":"Operations","description":"How the platform is running for you: the travel model behind drive times (road router, rush-hour calibration, metro profiles). Owner only."},{"name":"Invoices","description":"Draft, send and list invoices; the customer views and pays from a texted link."},{"name":"Billing","description":"Your CrewLink plan: the itemized monthly bill, the live usage meter and alerts, changing plan, the job list behind the overage, and the card on file (Stripe Checkout and Customer Portal).."},{"name":"Sign-up","description":"Self-serve sign-up: the published plans and rates, creating a company and its owner, Stripe Checkout (30-day trial, card on file) and the welcome sign-in. No key needed."},{"name":"Reports","description":"KPIs for a date range (with the prior period, the change and a daily series), the KPI catalog, where filed jobs stand (status tiles, the request funnel, cycle time), a heat map of jobs and techs, profit and loss per job, and weather and fuel prices where you work."},{"name":"Plan vs actual","description":"What was planned for each day against what happened in the field: every version of the plan, each drive as predicted, as a traffic provider saw it and as driven, each visit planned and worked, and the nightly review (on-time rate, CrewLink vs the traffic provider's ETA error, drive and on-site error). Owner or dispatcher."},{"name":"Pay","description":"Technician pay: base pay per service and per person, mileage over each day's route, bonuses and deductions, weekly pay periods (open, closed, paid) and the pay report. Settings, rates, closing and paying are owner only."},{"name":"Wins","description":"Gamification built only from what really happened: on-time streaks, personal bests, badges with bronze, silver and gold tiers, weekly team goals the owner sets, the wins feed and an opt-in leaderboard (top 3 only, never the bottom). Office: owner or dispatcher (settings owner only). Field app: the tech's own wins."},{"name":"Exports","description":"CSV downloads (jobs, invoices, payments, KPIs, techs, services, plan vs actual, technician pay) and the KPI report as a PDF, for a date range."},{"name":"Reviews","description":"Reviews customers leave after a job."},{"name":"Webhooks","description":"Subscribe HTTPS endpoints to signed events. Owner only."},{"name":"Mirrored conversations","description":"A platform's conversations about a job (client, tech, internal, DMs and groups), mirrored into an office-only log: idempotent by the platform's message id, edits, tombstones and backfill. They notify nobody. The office answers in a job's conversation and the platform gets external_message.created. Guide: [Platforms guide](/docs/guides/platforms#mirroring-conversations)."},{"name":"Platforms","description":"Products that run CrewLink for the companies that granted them access: the company owner grants and revokes access; a platform lists its grants and keeps one webhook subscription for all of them. Guide: [Platforms guide](/docs/guides/platforms)."},{"name":"Admin","description":"CrewLink staff only (super admins, users of the internal `crewlink` company): every company, short audited act-as owner sessions, platforms and their grants, the staff list and promotions, and the audit log. An act-as session can't use this area."},{"name":"CrewConnect","description":"Work shared between companies and independent contractors: connections, open times, sending and passing on jobs, pay terms and payouts."},{"name":"Health","description":"Is the API up: for uptime monitors and release checks. Public, no key, no data."},{"name":"Link previews","description":"The 1200x630 cards that iMessage, Slack, LinkedIn, X, WhatsApp, Facebook and Google show for a shared CrewLink link. Public, no key. A customer link's card names the business only."}]}