en
Bookings›Confirm a booking on behalf of the customer

Confirm a booking on behalf of the customer

Fires the customer-actor `confirm_booking` action from the BUSINESS surface (audited as business_on_behalf). Two uses: (1) LIVE — staff confirm a slot for a customer who booked by phone; (2) SANDBOX — the customer magic-token surface is live-only (a sandbox job's link can never reach a real customer), so this is the ONLY way to drive a sandbox test job past booking (book → quote → confirm → assign → complete). Body carries the customer-chosen scheduled_at (business-local naive datetime). DECISION TABLE — every 409 this endpoint returns, and the correct NEXT STEP (branch on error_code, never on the HTTP status): • JOB_REQUEST_STAGE_CONFLICT — the job changed since you read it (NOTE: every FAILED confirm attempt also bumps status_version by design). Next: re-GET the job, retry with the fresh status_version. • JOB_REQUEST_ACTION_NOT_PENDING — the job is no longer at the confirm step (usually: already confirmed). Next: re-GET and show current status; do not retry. • JOB_REQUEST_NO_TECHNICIAN_AVAILABLE — the TIME is infeasible for everyone (outside working hours / the customer window, or nobody qualifies). Best-effort `data` breakdown: `considered` (roster size checked), `blocked_by` (histogram of blockers[0].kind → count, only kinds that actually blocked someone), `truncated` (roster larger than the check covered). Next: pick another time via booking-windows / time-segments. NOT an emergency case — displacement cannot conjure capacity. • JOB_REQUEST_TECH_INFEASIBLE — the FORCED technician can never take the job then; `data.reason` says why (blockers[0].kind): outside_service_area | missing_required_skills | not_lead_tier | no_working_day | on_time_off | off_shift | visit_too_long | cannot_arrive_in_time (see `data.earliest_feasible_at`, RFC3339 UTC — the first same-day time they CAN be on site → offer it) | not_available_today (diagnosis unavailable). `data.blockers[]` names EVERY cause, most-structural first (`data.technician` carries an id; `name` is always empty on this confirm path today — nothing here calls the name lookup move's TECH_NOT_FEASIBLE payload uses); a UI that reads only `reason` still works. Next: keep the tech and reschedule to earliest_feasible_at+, OR keep the time and drop technician_id (auto-pick) / choose another tech from time-segments. NOT an emergency case. • JOB_REQUEST_P0_REQUIRES_DISPLACEMENT — the ONLY code that routes to the EMERGENCY flow: the job is P0, the tech qualifies, but the lane is genuinely occupied. Next: POST emergency/candidates → preview → commit (the commit auto-confirms). Caveat: if the occupying jobs are themselves P0 the preview will reject with EMERGENCY_RESCHEDULE_SLOT_OCCUPIED (P0 never displaces P0) — then pick another tech/time.

Arguments

idstringpathrequired
Job request ID
Idempotency-Keystringheaderoptional
Unique key making retries safe: a repeat send with the same key replays the original response (header Idempotent-Replayed: true) instead of re-running the operation. Reusing a key with a different body returns 422 IDEMPOTENCY_KEY_REUSE.
after_hours_overridebooleanbodyoptional
AfterHoursOverride (BUSINESS confirm only): schedule this P0 outside the technician's working hours or approved time-off — you have phoned them and they agreed. Requires priority p0, a single-person job, and technician_id. Double-booking, service area, required skills and the lead-tier rule still reject. Deliberately NOT a binding-tag rule: gin evaluates tags before the handler, which would make the three AFTER_HOURS codes unreachable.
arrival_window_minutesintegerbodyoptional
ArrivalWindowMinutes = width (phút) của arrival-window ô khách bấm ở slot-picker (chính là time_slot_step_minutes, mặc định 30). Persist để post-confirm detail render lại đúng window. Optional; bounds ([5, 240], khớp slot-picker step) validate ở usecase — single authority, một error code (JOB_REQUEST_INVALID_INPUT).
scheduled_atstringbodyoptional
Chosen start time — business-local wall clock (the business_time.datetime value from the time-segments picker), converted to UTC against the job's business timezone. Seconds may be omitted and a space may replace the T. An offset is accepted only when it agrees with the business timezone; a disagreeing one is refused with JOB_REQUEST_INVALID_INPUT naming what it means locally.
status_versionintegerbodyoptional
Optimistic-lock fence: the status_version from your last read. Omitted/0 = fence on the row's current version (no race protection).
technician_idstringbodyoptional
TechnicianID (BUSINESS confirm only — ignored on the customer surface): force-assign the job to this technician instead of the ranked auto-pick. Ranking is bypassed; feasibility (hours/time-off/geo/skills), the TierLead rule and the double-booking guard still apply — an infeasible forced tech rejects the confirm (P0 gets the displacement hint).
POST/v1/job-requests/{id}/confirm
curl -X POST "https://api.crisphive.com/v1/job-requests/9b2f6c3e-1a4d-4f0a-8f2e-7c5d1b3a9e01/confirm" \
  -H "Authorization: Bearer chsk_test_4eC8xQ9mZ2pL7Ka0rT" \
  -H "Content-Type: application/json" \
  -d '{
  "after_hours_override": true,
  "arrival_window_minutes": 1,
  "scheduled_at": "2030-06-14T09:00:00",
  "status_version": 2,
  "technician_id": "9b2f6c3e-1a4d-4f0a-8f2e-7c5d1b3a9e01"
}'
Request body
{
"after_hours_override": ,
"arrival_window_minutes": ,
"scheduled_at": ,
"status_version": ,
"technician_id":
}

Responses