Appointments API
Esta página aún no está disponible en tu idioma.
Appointments represent scheduled service visits booked for a customer. Each appointment tracks the customer’s contact info, the service requested, a date/time, an optional duration, a status, an optional assigned tech, and an optional link to a ticket. Status moves through: scheduled / pending → confirmed → checked_in → completed (or no_show / cancelled).
The appointment object
Section titled “The appointment object”{ "object": "appointment", "id": 7, "customer_name": "Jane Smith", "customer_phone": "555-867-5309", "customer_email": "jane.smith@example.com", "service_description": "iPhone 15 screen replacement", "scheduled_date": "2025-12-10", "scheduled_time": "14:30", "duration_minutes": 60, "status": "confirmed", "ticket_id": "103", "tech_id": "john-doe", "location_id": 1, "notes": "Customer prefers text updates.", "source": "online_booking", "cancelled_at": null, "cancellation_reason": null, "created_at": "2025-12-01T18:22:00.000Z", "updated_at": null}| Field | Type | Description |
|---|---|---|
id | integer | Unique appointment ID |
customer_name | string | Customer’s full name |
customer_phone | string | Customer’s phone number |
customer_email | string|null | Customer’s email address, or null |
service_description | string|null | Brief description of the service requested |
scheduled_date | string | Appointment date in YYYY-MM-DD format |
scheduled_time | string | Appointment start time in HH:MM (24-hour) format |
duration_minutes | integer | Estimated duration in minutes (defaults to 30) |
status | string | scheduled, pending, confirmed, checked_in, completed, no_show, or cancelled |
ticket_id | string|null | ID of the linked repair ticket, or null |
tech_id | string|null | Assigned tech’s roster ID (the online-booking tech slug, e.g. "john-doe"), or null |
location_id | integer|null | Location this appointment belongs to, or null |
notes | string|null | Internal notes about the appointment |
source | string|null | How the appointment was booked (e.g. "online_booking", "staff") |
cancelled_at | string|null | ISO-8601 timestamp of cancellation, or null |
cancellation_reason | string|null | Reason provided when cancelled, or null |
created_at | string | ISO-8601 creation timestamp |
updated_at | string|null | ISO-8601 timestamp of the last edit (reschedule, field change, ticket link, or status reset), or null if never edited |
List appointments
Section titled “List appointments”GET /api/v1/appointmentsReturns a cursor-paginated list of appointments, sorted by scheduled_date descending (most recent first).
Scope required: appointments.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Description |
|---|---|---|
date_from | string | Only appointments on or after this date (YYYY-MM-DD) |
date_to | string | Only appointments on or before this date (YYYY-MM-DD) |
status | string | Filter by status: scheduled, pending, confirmed, checked_in, completed, no_show, cancelled |
location_id | integer | Only appointments at this location |
customer_email | string | Only appointments for this customer email (case-insensitive) |
ticket_id | string | Only appointments linked to this ticket |
tech_id | string | Only appointments assigned to this tech roster ID (max 40 characters) |
limit | integer | Page size, 1–100 (default: 25) |
cursor | string | Opaque cursor from a previous response’s next_cursor |
curl "https://app.benchkey.com/api/v1/appointments?date_from=2025-12-01&status=confirmed&limit=10" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/appointments?date_from=2025-12-01&status=confirmed&limit=10", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const { data, has_more, next_cursor } = await res.json();Response
Section titled “Response”{ "object": "list", "data": [ { "object": "appointment", "id": 7, "status": "confirmed" } ], "has_more": false, "next_cursor": null}Additional resource fields are omitted from this example.
See Pagination for how to page through results.
List bookable time slots
Section titled “List bookable time slots”GET /api/v1/appointments/slotsReturns availability for a single day from the shop’s booking engine. Business hours, the advance-booking window, per-slot capacity, optional service spans (service_id), and per-tech calendars (tech_id) are all honored, the same engine that powers online booking.
The list envelope always carries closed (true when the shop is closed that day). Other empty days carry an advisory flag, present only when it applies: disabled (online booking disabled), too_far_ahead (beyond the advance-booking window, with max_days), unknown_tech (tech_id matches no roster tech), or tech_off (the tech doesn’t work that day).
Scope required: appointments.read
Query parameters
Section titled “Query parameters”| Parameter | Required | Type | Description |
|---|---|---|---|
date | yes | string | Day to check (YYYY-MM-DD) |
service_id | no | string | Configured service ID (max 40 characters), slots become span-aware for its duration |
tech_id | no | string | Tech roster ID (max 40 characters), availability narrows to that tech’s own calendar |
curl "https://app.benchkey.com/api/v1/appointments/slots?date=2025-12-10" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/appointments/slots?date=2025-12-10", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const { data } = await res.json();const open = data.filter((s) => s.available).map((s) => s.time);Response
Section titled “Response”{ "object": "list", "data": [ { "object": "appointment_slot", "time": "09:00", "available": true, "remaining": 2 }, { "object": "appointment_slot", "time": "09:30", "available": false, "remaining": 0 }, { "object": "appointment_slot", "time": "10:00", "available": true, "remaining": 1 } ], "closed": false}| Field | Type | Description |
|---|---|---|
time | string|null | Slot start time, HH:MM (24-hour) |
available | boolean | The slot can still be booked |
remaining | integer | Bookings still accepted for this slot |
When service_id or tech_id was supplied, the envelope echoes the resolved service ({ id, minutes }) and tech ({ id }) objects.
Retrieve an appointment
Section titled “Retrieve an appointment”GET /api/v1/appointments/:idReturns a single appointment.
Scope required: appointments.read
curl "https://app.benchkey.com/api/v1/appointments/7" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/appointments/7", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const appointment = await res.json();Response
Section titled “Response”Returns the appointment object. Returns 404 if no appointment with that ID exists.
Create an appointment
Section titled “Create an appointment”POST /api/v1/appointmentsBooks a new appointment. customer_name, customer_phone, scheduled_date, and scheduled_time are required. Emits the appointment.created webhook event, the event also fires for bookings made in the dashboard or through the online-booking page, so subscribers see every new appointment.
Scope required: appointments.write
Request body
Section titled “Request body”| Field | Required | Type | Description |
|---|---|---|---|
customer_name | yes | string | Customer’s full name (max 255 characters) |
customer_phone | yes | string | Customer’s phone number (max 50 characters) |
scheduled_date | yes | string | Appointment date in YYYY-MM-DD format |
scheduled_time | yes | string | Appointment time in HH:MM (24-hour) format |
customer_email | no | string | Customer’s email address (max 320 characters) |
service_description | no | string | Brief description of the service (max 1000 characters) |
duration_minutes | no | integer | Duration in minutes, must be a positive integer |
notes | no | string | Internal notes (max 5000 characters) |
ticket_id | no | string | Existing ticket ID to link to the appointment |
curl -X POST https://app.benchkey.com/api/v1/appointments \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: appt-create-jane-20251210-$(date +%s)" \ -d '{ "customer_name": "Jane Smith", "customer_phone": "555-867-5309", "customer_email": "jane.smith@example.com", "service_description": "iPhone 15 screen replacement", "scheduled_date": "2025-12-10", "scheduled_time": "14:30", "duration_minutes": 60 }'Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/appointments", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", "Idempotency-Key": `appt-create-jane-20251210-${Date.now()}`, }, body: JSON.stringify({ customer_name: "Jane Smith", customer_phone: "555-867-5309", customer_email: "jane.smith@example.com", service_description: "iPhone 15 screen replacement", scheduled_date: "2025-12-10", scheduled_time: "14:30", duration_minutes: 60, }),});const appointment = await res.json(); // HTTP 201Response
Section titled “Response”Returns the appointment object with HTTP 201.
Use an Idempotency-Key header to make retries safe.
Update an appointment
Section titled “Update an appointment”PATCH /api/v1/appointments/:idReschedules or updates an appointment. Send only the fields you want to change, omitted fields are left unchanged. At least one field is required. Emits the appointment.updated webhook event; dashboard edits emit it too.
Scope required: appointments.write
Request body
Section titled “Request body”| Field | Required | Type | Description |
|---|---|---|---|
customer_name | no | string | Updated name (max 255 characters) |
customer_phone | no | string | Updated phone (max 50 characters) |
customer_email | no | string|null | Updated email, or null to clear |
service_description | no | string|null | Updated service description, or null to clear |
scheduled_date | no | string | New date in YYYY-MM-DD format |
scheduled_time | no | string | New time in HH:MM (24-hour) format |
duration_minutes | no | integer | Updated duration in minutes (must be a positive integer; null is rejected) |
status | no | string | scheduled, pending, confirmed, checked_in, completed, no_show, or cancelled. When setting a terminal status (cancelled, completed, no_show), this must be the only field in the request body. |
notes | no | string|null | Updated notes, or null to clear |
ticket_id | no | string|null | Updated ticket link, or null to unlink |
curl -X PATCH https://app.benchkey.com/api/v1/appointments/7 \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -d '{ "scheduled_date": "2025-12-12", "scheduled_time": "10:00", "status": "confirmed" }'Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/appointments/7", { method: "PATCH", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ scheduled_date: "2025-12-12", scheduled_time: "10:00", status: "confirmed", }),});const appointment = await res.json();Response
Section titled “Response”Returns the updated appointment object.
Cancel an appointment
Section titled “Cancel an appointment”POST /api/v1/appointments/:id/cancelCancels an appointment. Sets status to cancelled, records cancelled_at, and stores an optional reason. All downstream side effects (notifications, calendar updates) fire through the internal route. Emits the appointment.canceled webhook event; dashboard cancellations emit it too.
Scope required: appointments.write
Request body
Section titled “Request body”| Field | Required | Type | Description |
|---|---|---|---|
reason | no | string | Optional cancellation reason |
curl -X POST https://app.benchkey.com/api/v1/appointments/7/cancel \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: appt-7-cancel-1" \ -d '{ "reason": "Customer rescheduled for next week." }'Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/appointments/7/cancel", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", "Idempotency-Key": "appt-7-cancel-1", }, body: JSON.stringify({ reason: "Customer rescheduled for next week." }),});const appointment = await res.json(); // status: "cancelled", cancelled_at: "..."Response
Section titled “Response”Returns the refreshed appointment object with status: "cancelled" and cancelled_at set.
Complete an appointment
Section titled “Complete an appointment”POST /api/v1/appointments/:id/completeMarks the appointment completed and cancels its pending reminders. A cancelled appointment must be reset to confirmed first (409). Emits the appointment.completed webhook event.
Scope required: appointments.write
curl -X POST https://app.benchkey.com/api/v1/appointments/7/complete \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Idempotency-Key: appt-7-complete-1"Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/appointments/7/complete", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Idempotency-Key": "appt-7-complete-1", },});const appointment = await res.json(); // status: "completed"Response
Section titled “Response”Returns the refreshed appointment object with status: "completed".
Mark a no-show
Section titled “Mark a no-show”POST /api/v1/appointments/:id/no_showMarks the appointment no_show and cancels its pending reminders. Emits the appointment.no_show webhook event.
Scope required: appointments.write
curl -X POST https://app.benchkey.com/api/v1/appointments/7/no_show \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Idempotency-Key: appt-7-noshow-1"Response
Section titled “Response”Returns the refreshed appointment object with status: "no_show".
Link an appointment to a ticket
Section titled “Link an appointment to a ticket”POST /api/v1/appointments/:id/link_ticketAttaches the appointment to an existing repair ticket by its canonical ticket ID. The ticket must exist on the public surface, hidden, deleted, or billing-only tickets return 404.
Scope required: appointments.write
Request body
Section titled “Request body”| Field | Required | Type | Description |
|---|---|---|---|
ticket_id | yes | string|integer | Canonical ticket ID (order_id). A positive integer is canonicalized to its digit string |
curl -X POST https://app.benchkey.com/api/v1/appointments/7/link_ticket \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -d '{ "ticket_id": "TK-10042" }'Node.js
Section titled “Node.js”const res = await fetch("https://app.benchkey.com/api/v1/appointments/7/link_ticket", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", }, body: JSON.stringify({ ticket_id: "TK-10042" }),});const appointment = await res.json(); // ticket_id: "TK-10042"Response
Section titled “Response”Returns the refreshed appointment object with ticket_id set.
Error codes
Section titled “Error codes”| HTTP status | Code | Meaning |
|---|---|---|
400 | invalid_id | The :id is not a valid positive integer, or exceeds 2,147,483,647 |
400 | invalid_query | A list filter parameter was supplied as an array or object instead of a single scalar value |
400 | invalid_date_from | date_from filter is not a valid YYYY-MM-DD date |
400 | invalid_date_to | date_to filter is not a valid YYYY-MM-DD date |
400 | invalid_location_id | location_id filter is not a positive integer or exceeds 2,147,483,647 |
400 | invalid_customer_email | customer_email filter is empty or exceeds 320 characters |
400 | invalid_ticket_id | ticket_id filter is empty or exceeds 255 characters |
400 | invalid_cursor | Pagination cursor is malformed |
400 | missing_field | A required field is absent (customer_name, customer_phone, scheduled_date, scheduled_time) |
400 | invalid_field | A field value is out of range or the wrong format (e.g. customer_name > 255 chars, customer_email > 320 chars) |
400 | invalid_scheduled_date | scheduled_date is not a valid YYYY-MM-DD date |
400 | invalid_scheduled_time | scheduled_time is not a valid HH:MM time |
400 | invalid_status | status is not one of the allowed values |
400 | invalid_duration_minutes | duration_minutes is not a positive integer |
400 | invalid_date | The slots date parameter is missing or not a valid YYYY-MM-DD date |
400 | invalid_service_id | The slots service_id parameter is empty or exceeds 40 characters |
400 | no_fields | PATCH body contained no recognised fields |
404 | not_found | No appointment with that ID exists (or, on link_ticket, the ticket is missing/hidden/deleted/billing-only) |
409 | slot_unavailable | The requested time slot is already taken |
409 | status_reset_required | A terminal or cancelled appointment must be reset to confirmed before a new status can be applied (PATCH only) |
409 | status_transition_failed | A terminal status transition (cancelled, completed, no_show) was rejected by the internal route, including completing a cancelled appointment |
409 | status_reset_failed | The internal reset-to-confirmed step failed (PATCH only) |
422 | create_failed | The internal route rejected the create request |
422 | update_failed | The internal route rejected the update request |
422 | cancel_failed | The appointment could not be cancelled (e.g. already cancelled) |
422 | link_ticket_failed | The ticket link could not be recorded |
403 | insufficient_scope | API key lacks appointments.read or appointments.write |
429 | rate_limited | Too many requests; see Retry-After |
See Errors for the full error envelope format.