Skip to content

Appointments API

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).

{
"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
}
FieldTypeDescription
idintegerUnique appointment ID
customer_namestringCustomer’s full name
customer_phonestringCustomer’s phone number
customer_emailstring|nullCustomer’s email address, or null
service_descriptionstring|nullBrief description of the service requested
scheduled_datestringAppointment date in YYYY-MM-DD format
scheduled_timestringAppointment start time in HH:MM (24-hour) format
duration_minutesintegerEstimated duration in minutes (defaults to 30)
statusstringscheduled, pending, confirmed, checked_in, completed, no_show, or cancelled
ticket_idstring|nullID of the linked repair ticket, or null
tech_idstring|nullAssigned tech’s roster ID (the online-booking tech slug, e.g. "john-doe"), or null
location_idinteger|nullLocation this appointment belongs to, or null
notesstring|nullInternal notes about the appointment
sourcestring|nullHow the appointment was booked (e.g. "online_booking", "staff")
cancelled_atstring|nullISO-8601 timestamp of cancellation, or null
cancellation_reasonstring|nullReason provided when cancelled, or null
created_atstringISO-8601 creation timestamp
updated_atstring|nullISO-8601 timestamp of the last edit (reschedule, field change, ticket link, or status reset), or null if never edited

GET /api/v1/appointments

Returns a cursor-paginated list of appointments, sorted by scheduled_date descending (most recent first).

Scope required: appointments.read

ParameterTypeDescription
date_fromstringOnly appointments on or after this date (YYYY-MM-DD)
date_tostringOnly appointments on or before this date (YYYY-MM-DD)
statusstringFilter by status: scheduled, pending, confirmed, checked_in, completed, no_show, cancelled
location_idintegerOnly appointments at this location
customer_emailstringOnly appointments for this customer email (case-insensitive)
ticket_idstringOnly appointments linked to this ticket
tech_idstringOnly appointments assigned to this tech roster ID (max 40 characters)
limitintegerPage size, 1–100 (default: 25)
cursorstringOpaque cursor from a previous response’s next_cursor
Terminal window
curl "https://app.benchkey.com/api/v1/appointments?date_from=2025-12-01&status=confirmed&limit=10" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
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();
{
"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.


GET /api/v1/appointments/slots

Returns 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

ParameterRequiredTypeDescription
dateyesstringDay to check (YYYY-MM-DD)
service_idnostringConfigured service ID (max 40 characters), slots become span-aware for its duration
tech_idnostringTech roster ID (max 40 characters), availability narrows to that tech’s own calendar
Terminal window
curl "https://app.benchkey.com/api/v1/appointments/slots?date=2025-12-10" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
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);
{
"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
}
FieldTypeDescription
timestring|nullSlot start time, HH:MM (24-hour)
availablebooleanThe slot can still be booked
remainingintegerBookings 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.


GET /api/v1/appointments/:id

Returns a single appointment.

Scope required: appointments.read

Terminal window
curl "https://app.benchkey.com/api/v1/appointments/7" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
const res = await fetch(
"https://app.benchkey.com/api/v1/appointments/7",
{ headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } }
);
const appointment = await res.json();

Returns the appointment object. Returns 404 if no appointment with that ID exists.


POST /api/v1/appointments

Books 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

FieldRequiredTypeDescription
customer_nameyesstringCustomer’s full name (max 255 characters)
customer_phoneyesstringCustomer’s phone number (max 50 characters)
scheduled_dateyesstringAppointment date in YYYY-MM-DD format
scheduled_timeyesstringAppointment time in HH:MM (24-hour) format
customer_emailnostringCustomer’s email address (max 320 characters)
service_descriptionnostringBrief description of the service (max 1000 characters)
duration_minutesnointegerDuration in minutes, must be a positive integer
notesnostringInternal notes (max 5000 characters)
ticket_idnostringExisting ticket ID to link to the appointment
Terminal window
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
}'
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 201

Returns the appointment object with HTTP 201.

Use an Idempotency-Key header to make retries safe.


PATCH /api/v1/appointments/:id

Reschedules 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

FieldRequiredTypeDescription
customer_namenostringUpdated name (max 255 characters)
customer_phonenostringUpdated phone (max 50 characters)
customer_emailnostring|nullUpdated email, or null to clear
service_descriptionnostring|nullUpdated service description, or null to clear
scheduled_datenostringNew date in YYYY-MM-DD format
scheduled_timenostringNew time in HH:MM (24-hour) format
duration_minutesnointegerUpdated duration in minutes (must be a positive integer; null is rejected)
statusnostringscheduled, 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.
notesnostring|nullUpdated notes, or null to clear
ticket_idnostring|nullUpdated ticket link, or null to unlink
Terminal window
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"
}'
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();

Returns the updated appointment object.


POST /api/v1/appointments/:id/cancel

Cancels 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

FieldRequiredTypeDescription
reasonnostringOptional cancellation reason
Terminal window
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." }'
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: "..."

Returns the refreshed appointment object with status: "cancelled" and cancelled_at set.


POST /api/v1/appointments/:id/complete

Marks 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

Terminal window
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"
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"

Returns the refreshed appointment object with status: "completed".


POST /api/v1/appointments/:id/no_show

Marks the appointment no_show and cancels its pending reminders. Emits the appointment.no_show webhook event.

Scope required: appointments.write

Terminal window
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"

Returns the refreshed appointment object with status: "no_show".


POST /api/v1/appointments/:id/link_ticket

Attaches 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

FieldRequiredTypeDescription
ticket_idyesstring|integerCanonical ticket ID (order_id). A positive integer is canonicalized to its digit string
Terminal window
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" }'
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"

Returns the refreshed appointment object with ticket_id set.


HTTP statusCodeMeaning
400invalid_idThe :id is not a valid positive integer, or exceeds 2,147,483,647
400invalid_queryA list filter parameter was supplied as an array or object instead of a single scalar value
400invalid_date_fromdate_from filter is not a valid YYYY-MM-DD date
400invalid_date_todate_to filter is not a valid YYYY-MM-DD date
400invalid_location_idlocation_id filter is not a positive integer or exceeds 2,147,483,647
400invalid_customer_emailcustomer_email filter is empty or exceeds 320 characters
400invalid_ticket_idticket_id filter is empty or exceeds 255 characters
400invalid_cursorPagination cursor is malformed
400missing_fieldA required field is absent (customer_name, customer_phone, scheduled_date, scheduled_time)
400invalid_fieldA field value is out of range or the wrong format (e.g. customer_name > 255 chars, customer_email > 320 chars)
400invalid_scheduled_datescheduled_date is not a valid YYYY-MM-DD date
400invalid_scheduled_timescheduled_time is not a valid HH:MM time
400invalid_statusstatus is not one of the allowed values
400invalid_duration_minutesduration_minutes is not a positive integer
400invalid_dateThe slots date parameter is missing or not a valid YYYY-MM-DD date
400invalid_service_idThe slots service_id parameter is empty or exceeds 40 characters
400no_fieldsPATCH body contained no recognised fields
404not_foundNo appointment with that ID exists (or, on link_ticket, the ticket is missing/hidden/deleted/billing-only)
409slot_unavailableThe requested time slot is already taken
409status_reset_requiredA terminal or cancelled appointment must be reset to confirmed before a new status can be applied (PATCH only)
409status_transition_failedA terminal status transition (cancelled, completed, no_show) was rejected by the internal route, including completing a cancelled appointment
409status_reset_failedThe internal reset-to-confirmed step failed (PATCH only)
422create_failedThe internal route rejected the create request
422update_failedThe internal route rejected the update request
422cancel_failedThe appointment could not be cancelled (e.g. already cancelled)
422link_ticket_failedThe ticket link could not be recorded
403insufficient_scopeAPI key lacks appointments.read or appointments.write
429rate_limitedToo many requests; see Retry-After

See Errors for the full error envelope format.

System status