Reservas›Book, schedule and confirm a job in one call
Book, schedule and confirm a job in one call
Creates the job, quotes it (job_duration_minutes, or the job type's default_duration_minutes) and confirms it at scheduled_at on the customer's behalf — the dashboard's "book for a caller" in a single request, built for voice agents and automation platforms that cannot run create → quote → slots → confirm.
Customer: send `customer_id`, or `customer` (+ `address`) to match-or-create by phone/email — a caller who already exists is matched, never duplicated. Find a caller first with listCustomers?phone=.
Every input is validated BEFORE anything is written (time and business timezone, future start, a duration or a job-type default, non-negative mobilization/demobilization minutes, active job type, phone format); those failures are ordinary 4xx and create nothing.
⚠️ A `technician_id` the engine cannot honour at that time is refused BEFORE anything is written: `409 JOB_REQUEST_TECH_INFEASIBLE`, no job, no customer, no webhook. Change the technician or the time and send again with a NEW Idempotency-Key: like every 4xx here the 409 is cached against its key for 24h, so the same key replays it (same body) or answers 422 IDEMPOTENCY_KEY_REUSE (changed body). This check is BEST-EFFORT — if the scheduling engine cannot be reached it is skipped and the call proceeds, so a pinned request can still come back as the 200 + `refusal` below. Handle both.
⚠️ ONE carve-out: a `p0` whose pinned technician is merely BUSY is NOT refused here. The job is created and the call answers 200 + `refusal` carrying JOB_REQUEST_P0_REQUIRES_DISPLACEMENT, because the remedy for that code is the emergency flow and listEmergencyCandidates / previewEmergencyReschedule / commitEmergencyReschedule all require `emergency_job_id` — a 409 carries no job id. Read `job_id` off the 200 and run the emergency flow; do not re-book. Every other doomed pin on a `p0` (out of area, missing skills, not a lead) still answers 409.
⚠️ A SECOND carve-out, narrower: for a `p0`, a PERSONAL-CALENDAR overlap on the pinned technician is not refused here either — naming them is treated as having already phoned them — and the call answers 200 + `confirmed: true`. Nothing on this response discloses it; the only record is the business's activity feed (`job_calendar_override`). `calendar_busy`/`calendar_conflict` therefore never appear in the 409 reason vocabulary below when the pin is a `p0` naming a technician explicitly (this endpoint never books a crew, so the exclusion is unconditional here).
⚠️ Otherwise, once the job is created it is never discarded, and the call answers 200 even if scheduling then fails: `confirmed: false` with `refusal` (stage + the exact error_code/data the quote or confirm endpoint would have returned, e.g. JOB_REQUEST_NO_TECHNICIAN_AVAILABLE with blockers). The job is then quoted and waiting in the coordinator's queue — tell the caller the office will confirm a time. Retry with the SAME Idempotency-Key to replay the result; a new key books a second job.
P0 dispatch: send `emergency` with `priority: "p0"`. `dispatch: "now"` plans the displacement cascade BEFORE anything is written — a refusal is a 409 creating nothing — then books, quotes and commits it. `dispatch: "later"` parks a quoted P0 in the tray and ignores the other fields.
⚠️ `emergency.expected_move_ids` is REQUIRED with `dispatch: "now"` and has three meanings: omitted is refused, `[]` means "my preview showed no displacement" and commits a free lane, and a named set is the fence. Get the ids from previewEmergencyReschedule.
⚠️ A refusal AFTER the job is created answers 200 with `confirmed: false` and `refusal.stage: "dispatch"`. Read `job_id`, re-preview with previewEmergencyReschedule and commit with commitEmergencyReschedule. Never re-book.
Argumentos
Idempotency-Keystringheaderopcional
Makes retries safe: a repeat send with the same key returns the original result instead of booking again
▾addressobjectbodyopcional
Service address. REQUIRED with `customer`. OPTIONAL with `customer_id`: when sent it is used for THIS JOB and the stored customer address is only the fallback when it is omitted — it is never written back to the customer record. Missing coordinates are geocoded.
citystringbodyopcional
countrystringbodyopcional
formattedstringbodyopcional
latitudenumberbodyopcional
linestringbodyopcional
line2stringbodyopcional
longitudenumberbodyopcional
postal_codestringbodyopcional
statestringbodyopcional
▾customerobjectbodyopcional
A caller who may or may not exist yet: matched by phone/email against the business's customers (no duplicate is created for a known caller), created otherwise. Send this OR `customer_id`.
emailstringbodyopcional
Email. Phone or email is required.
full_namestringbodyopcional
Customer's full name. Required with `customer`.
phonestringbodyopcional
Phone in E.164 with the leading + (e.g. +16135550142); separators are stripped. A bare national number is refused with PHONE_INVALID. Phone or email is required.
sms_opt_inbooleanbodyopcional
Record SMS consent ONLY if the caller explicitly agreed to texts (a voice agent must ask). Without it the customer gets email, never SMS.
customer_idstringbodyopcional
UUID of an existing customer. Send this OR `customer`, not both.
demobilization_minutesintegerbodyopcional
Demobilization (teardown) minutes after the work, min 0 — same rules as mobilization_minutes.
descriptionstringbodyopcional
Free-text description of the work. Optional; max 2000 chars.
▾emergencyobjectbodyopcional
P0 dispatch. Present only with priority p0 (or a business whose default priority is p0). See the endpoint description.
after_hours_overridebooleanbodyopcional
Place this P0 outside working hours / approved time-off — you have phoned the technician and they agreed.
dispatchenumbodyopcional
"now" commits the cascade in this call; "later" parks a quoted P0 in the tray and ignores every other field here.
displacement_modeenumbodyopcional
Fate of displaced jobs: reschedule (default) or reassign. Genuinely optional, unlike mode — omitted means reschedule. Must match the preview.
expected_move_idsarray<string>bodyopcional
The displaced job IDs your preview returned. REQUIRED with dispatch "now". Send [] when the preview showed no displacement — an omitted key is refused, an empty array commits a free lane.
modeenumbodyopcional
Cascade mode: overtime = displaced jobs stay same-day; next_day = overflow rolls to the next working day. REQUIRED with dispatch "now" (omitted is a 400 EMERGENCY_RESCHEDULE_INVALID_INPUT, data.field = "emergency.mode"); ignored with "later". Must match the preview.
job_duration_minutesintegerbodyopcional
Hands-on work minutes. Optional when the job type has a default_duration_minutes.
job_type_idstringbodyopcional
UUID of the job type: it classifies the job and supplies default_duration_minutes when job_duration_minutes is omitted. Optional: omitted, the business's DEFAULT job type (is_default, the seeded "General", 60 min + 15 + 15 unless the business changed it) is used.
mobilization_minutesintegerbodyopcional
Mobilization (setup/travel-prep) minutes before the work, min 0 — same meaning as on quoteJobRequest. Optional: omitted, the job type's default_mobilization_minutes applies when job_duration_minutes is ALSO omitted, and 0 when you sent a duration (an explicit duration is never mixed with the type's buffers). A value you send always wins. Counted when checking whether a pinned technician_id is free.
priorityenumbodyopcional
Scheduling priority p0–p3. Optional; omitted = the business default.
scheduled_atstringbodyopcional
Start time — a BUSINESS-LOCAL wall clock, same rule as confirmJobRequest: canonical `2026-09-30T10:00:00`, seconds optional, a space may replace the T; an offset is accepted only when it agrees with the business timezone. Must be in the future.
skill_idsarray<string>bodyopcional
UUIDs of skills the work needs. Optional; up to 20.
technician_idstringbodyopcional
Force a specific lead technician (feasibility still enforced). Omit to let Crisphive pick.