Appointment Guide
Learn how to create, hold, confirm, and manage appointments through the v3 API without exposing internal booking logic.
Environment tip: All examples use
v3.onsched.comfor production. Replace the host withapi-stage.onsched.comwhen calling the staging environment.
When to Use
- Build customer-facing booking flows (create holds, finalize bookings, cancel or reschedule).
- Power operational tooling that needs filtered appointment lists or per-record inspection.
- Sync appointments to downstream systems via polling or in combination with webhooks.
Prerequisites
- Authentication: Either OAuth2 client credentials (
POST /v3/oauth/token) or the dashboard trio ofAuthorization: Bearer <JWT>,x-api-key, andx-client-id. - Company context: Requests are scoped to the authenticated company. Location and service IDs must belong to that company.
- Valid availability check: Always call
GET /v3/availabilityfirst to present conflict-free slots.
Endpoint Overview
| Endpoint | Purpose |
|---|---|
GET /v3/appointments | Paginated list with optional filters (limit, page, locationId, resourceIds, serviceIds, customerId, status, from, to, sort, order). Default sort is created descending (newest row first). Use sort=scheduled to walk the full result by occupancy start, else scheduledStartTime after cancel; when order is omitted, scheduled sort defaults to ascending. |
GET /v3/appointments/filter | Filter by status, date range, resource, or location via query params. |
GET /v3/appointment/:id | Retrieve a single appointment (public routes only return minimal fields). Private responses include rescheduledFromAppointmentId and rescheduledFromStartTime when this row was created by a reschedule, and scheduledStartTime / scheduledEndTime after cancel. |
GET /v3/appointment/:id/audit | Review appointment lifecycle audit events for support and operational troubleshooting. Includes rescheduleFromTrail of predecessor ids. See Appointment Audit Events. |
POST /v3/appointment | Create a booked appointment immediately. |
POST /v3/appointment/hold | Create a short-lived hold (status: IN) so customers can finish intake steps. |
POST /v3/appointment/reserve | Reserve a slot (status: RS) without hold expiration; finalize with PUT .../book or delete when no longer needed. |
PUT /v3/appointment/:id/book | Convert a hold or reserved slot to a booked record without changing slot times. |
PUT /v3/appointment/:id | Update mutable properties such as notes, metadata, CustomFields, confirmed, or the appointment padding override. |
POST /v3/appointment/:id/validateResources | Optional preflight check for custom dashboards before accepting a resource change in the UI. This does not mutate the appointment. |
PUT /v3/appointment/:id/reschedule | Change the appointment time, assigned resources, or both. The request must pass availability validation. The new record keeps IN/RS if the original was a hold or reserved; otherwise it is booked (BK). |
PUT /v3/appointment/:id/confirm | Set confirmed: true only; does not change status or external calendars. |
PUT /v3/appointment/:id/cancel | Cancel and trigger notifications + webhooks. |
DELETE /v3/appointment/:id | Permanently delete an IN or RS hold. Booked and historical records must be retained. |
All write operations are validated server-side (see files under api/validations/appointment/). Invalid IDs or overlapping times return 400 errors without revealing internal scheduling rules.
List Pagination and Request Limits
GET /v3/appointments uses page-based offset pagination. limit defaults to 20
and accepts 1 through 100; page starts at 1. The resulting offset,
(page - 1) * limit, must not exceed 10,000. Requests beyond that boundary return
400 Validation Error. Use from, to, and other filters to partition histories that
cannot be traversed within one 10,000-row window.
For a reliable pagination walk:
- Request pages sequentially with the same filters, sort, order, and limit.
- Stop when
datais empty or whenpage * limit >= count. - Never advance to another page after an empty response.
- Avoid parallel pagination walks over the same filters.
- On
429, wait for theRetry-Afternumber of seconds and retry the same page.
By default, appointment-list requests use a Company-scoped rate budget with a burst
capacity of 10 requests and a sustained refill of one request per second. Each API
instance runs at most two list requests at once and at most one per Company. A small,
bounded queue absorbs brief overlap without increasing active database work; queue-full
or queue-timeout requests receive 429.
Booking Flow
- Hold (optional):
POST /v3/appointment/holdwithUnavailability.startTime/endTimeand optionalCustomer,CustomFields, andResourceIds. Holds respect expiration windows defined per location (expirationDelayseconds). When a location has noexpirationDelay(or it is0), the hold never expires on its own and keeps the slot locked until you book or delete it. - Book (and optional confirm): Call
PUT /v3/appointment/:id/bookto finalize a hold intoBKwith calendar sync, or usePOST /v3/appointmentto book in one step. UsePUT /v3/appointment/:id/confirmonly when you needconfirmed: truewithout changing status or calendars. - Resource Assignment: Provide
ResourceIdsas repeated query string parameters (e.g.,ResourceIds=id1&ResourceIds=id2). When omitted, the service’s eligible resources are auto-assigned based on the round robin mode you requested during availability. - Notifications/Webhooks: The platform automatically dispatches notifications and webhooks when appointments are booked, rescheduled, confirmed, or cancelled. Immediate create and reschedule requests honor
skip_notifications=true. On reschedule, this suppresses email/SMS while preserving the lifecycle webhook. Book, confirm, and cancel transitions still emit their notifications and webhooks. - Calendar Events: Google Calendar / Outlook events are created when a booked appointment is created (
POST /v3/appointment), a hold or reserve is finalized (PUT /v3/appointment/:id/book), or an appointment is rescheduled. Rescheduling deletes the old synced event and creates the replacement event for the new time and/or resource assignment. Holds and reserves (POST /v3/appointment/hold,POST /v3/appointment/reserve) do not create external events until booked.PUT /confirmdoes not sync calendars.
Rescheduling and Resource Changes
Use PUT /v3/appointment/:id/reschedule for any change that affects the slot assignment:
- time changes only
- resource changes only, even when
Unavailability.startTimeandUnavailability.endTimestay exactly the same - time and resource changes together
Use PUT /v3/appointment/:id for metadata and appointment padding overrides. Use the reschedule route for time or resource changes.
Appointment Padding Overrides
Authenticated integrations can replace a Service's padding for one appointment. Use
the same whole-minute value when previewing and creating the appointment:
GET /v3/availability?overridePadding=...previews availability with the override.POST /v3/appointment,/hold, or/reserveacceptsoverridePaddingas a query parameter and stores it on the appointment.PUT /v3/appointment/:idandPUT /v3/appointment/:id/rescheduleacceptoverridePaddingin the JSON body.
0 removes padding for that appointment. On update or reschedule, null restores the
Service padding, while omitting the field preserves the appointment's current explicit
override. Values must be integers from 0 through 2147483647. Public endpoints ignore
the field or query parameter, even when malformed, and never expose the stored override.
Padding can be changed while an appointment is IN, RS, or BK. Cancelled (CN) and
rescheduled-away (RE) records reject normal PUT changes. An increase in effective
padding must pass slot availability validation and is rolled back on conflict; a decrease
applies without that validation. A successful normal PUT /v3/appointment/:id change
clears affected availability caches after commit and produces an update_padding audit
event. Padding-only updates do not emit lifecycle notifications or webhooks. If the appointment
already has a Google or Outlook event, the API refreshes the separate padding event
best-effort: 0 removes it, and a later positive override recreates it. The main
appointment event is not replaced. Reschedule remains a
reschedule lifecycle audit event even when its replacement appointment changes padding.
curl -X PUT https://v3.onsched.com/v3/appointment/<appointment-id> \
-H "Authorization: Bearer <token>" \
-H "x-api-key: <company-key>" \
-H "Content-Type: application/json" \
-d '{"overridePadding": 15}'Resource-only reschedule
To move an appointment from one provider, room, or equipment resource to another at the same time, call the reschedule route and pass the replacement resources as repeated ResourceIds query parameters. You can omit Unavailability when the time is unchanged.
curl -X PUT "https://v3.onsched.com/v3/appointment/<appointment-id>/reschedule?ResourceIds=<new-resource-id>" \
-H "Authorization: Bearer <token>" \
-H "x-api-key: <company-key>" \
-H "Content-Type: application/json" \
-d '{
"notes": "Reassigned to another provider",
"skip_notifications": true
}'Use skip_notifications: true when the provider assignment should change silently. The API still emits the APPOINTMENT_RESCHEDULED webhook and refreshes external calendar events.
For multi-resource appointments, repeat the parameter once per resource:
?ResourceIds=<resource-id-1>&ResourceIds=<resource-id-2>Server-side behavior is the same reschedule lifecycle used for time changes:
- The API validates that the requested resources are linked to the appointment's location and service.
- The API validates that the existing appointment time is available for the requested resources.
- The original appointment remains as an
RErescheduled placeholder for audit/history. - A new appointment record is created with the requested resource assignment.
rescheduledFromAppointmentIdon the new row is the original appointment id;rescheduledFromStartTimeis that predecessor’s scheduled start instant. - The
APPOINTMENT_RESCHEDULEDwebhook is emitted. Email and SMS are emitted unlessskip_notifications: trueis provided. - External calendar events are refreshed for the new assignment.
Validate resources before saving
Client dashboards can call POST /v3/appointment/:id/validateResources as a dry run before allowing a user to select new resources. This is useful for resource pickers that should reject unavailable providers before the user clicks Save.
curl -X POST https://v3.onsched.com/v3/appointment/<appointment-id>/validateResources \
-H "Authorization: Bearer <token>" \
-H "x-api-key: <company-key>" \
-H "Content-Type: application/json" \
-d '{
"ResourceIds": ["<new-resource-id>"],
"Unavailability": {
"startTime": "2025-03-14T15:00:00Z",
"endTime": "2025-03-14T15:30:00Z"
}
}'A successful response means the proposed resource assignment is valid at that moment. It does not update the appointment, create calendar events, or send notifications. Always call PUT /v3/appointment/:id/reschedule to save the change; the reschedule route validates again because availability can change between preflight and save.
Status Lifecycle
api/enums/models.js defines the canonical status values:
IN– Initial hold. Auto-expires if not promoted.BK– Booked appointment (most common steady state).RS– Reserved hold that is kept longer than a quickIN.RE– Rescheduled placeholder left behind when a booking is moved.CN– Cancelled appointment.
The API prevents conflicting state transitions (for example, cancelling a CN appointment returns a 400). Use holds for multi-step checkout; skip them for instant bookings.
Filter the list by status
GET /v3/appointments accepts a status filter with one or more values. Repeat the parameter or send a comma-separated list, in either case:
curl -G "https://v3.onsched.com/v3/appointments" \
--data-urlencode "status=BK" \
--data-urlencode "status=CN" \
-H "Authorization: Bearer <token>" \
-H "x-api-key: <company-key>"?status=BKreturns booked appointments only;?status=BK&status=CNreturns booked and cancelled.?status=BK,CNis equivalent, and values are case-insensitive (bkworks).- Omit
statusto keep the default behavior: appointments of every status, including holds (IN,RS) and rescheduled placeholders (RE). - An unrecognized or empty value returns
400listing the accepted values.
Status transitions and side effects
POST /v3/appointment/hold→INstatus. The slot is locked untilexpirationDelay(seconds) on the location elapses or you finalize it. Without a positiveexpirationDelay, the hold has no timer and behaves likeRSuntil booked or deleted.POST /v3/appointment/reserve→RSstatus. The slot stays locked without that hold timer; finalize withPUT /v3/appointment/:id/bookor remove the appointment when done.POST /v3/appointment→BKstatus. Confirmation emails/SMS and confirmation webhooks fire unless you passskip_notifications=true.PUT /v3/appointment/:id/book→BKstatus. Finalizing a hold or reserve emitsNEW_APPOINTMENT; this endpoint does not honorskip_notifications.PUT /v3/appointment/:id/confirmsetsconfirmed: trueand does not changestatusor sync external calendars. To finalize a hold into a booked record with calendar sync, usePUT /v3/appointment/:id/book.PUT /v3/appointment/:id/rescheduleleaves the original record behind inREstatus (for audit/history) and creates a new appointment for the new time and/or resource assignment; status on the new row follows the same hold/reserved vs booked rules asPOST /v3/appointment/#buildAppointmentData(not alwaysBK). The new row’srescheduledFromAppointmentIdpoints at the predecessor andrescheduledFromStartTimesnapshots that predecessor’s scheduled start. Passskip_notifications: trueto suppress email/SMS while retaining webhooks and calendar synchronization.PUT /v3/appointment/:id/cancelmoves the booking toCN, stops reminder cadence, and emits cancellation webhooks/notifications. OccupancyUnavailability(and padding Unavailability) is destroyed so the slot is free again. Private appointment responses keep the cancelled interval onscheduledStartTimeandscheduledEndTime(read-only; set only by cancel).GET /v3/appointmentsdate filters use occupancy start when present, otherwise the scheduled start, so cancelled rows still appear infrom/tolists.DELETE /v3/appointment/:idpermanently removes onlyINandRSholds.BK,CN, andREappointments are retained for billing and lifecycle history; cancel aBKappointment instead.
Custom Fields & Metadata
- Send
CustomFieldsas an object. Keys must exist in your schema configuration. Customeraccepts either an existingidor the fields required to create a new record (firstName,emailat minimum).Unavailabilityrepresents the time block; padding is computed server-side when the service has non-zero padding. On active appointments this is occupancy — the block that holds capacity. After cancel, nestedUnavailabilityis null; readscheduledStartTimeandscheduledEndTimeon the appointment instead.
Build appointments from availability results
Use the values returned by GET /v3/availability to avoid validation errors:
LocationIdandServiceIdmust match the request you used to find the slot.ResourceIdsshould mirror the slot returned (or the resources you requested whenroundRobin=NONE).Unavailability.startTime/endTimeshould come directly from the chosenavailableTimesentry.Customeris required for immediate bookings; holds (POST /v3/appointment/hold) can be created without a customer and later promoted withPUT /v3/appointment/:id/bookonce customer details are ready.
If you need extra time for intake, use holds plus a higher expirationDelay on the location; otherwise, book directly to keep the flow simple.
Example: Create a Hold Then Book It
# Step 1: create hold (expires automatically if untouched)
curl -X POST https://v3.onsched.com/v3/appointment/hold \
-H "Authorization: Bearer <token>" \
-H "x-api-key: <company-key>" \
-H "Content-Type: application/json" \
-d '{
"LocationId": "<location-id>",
"ServiceId": "<service-id>",
"ResourceIds": ["<resource-id>"],
"Customer": {
"firstName": "Ada",
"lastName": "Lovelace",
"email": "[email protected]"
},
"Unavailability": {
"startTime": "2025-03-14T15:00:00Z",
"endTime": "2025-03-14T15:30:00Z"
}
}'
# Step 2: finalize
curl -X PUT https://v3.onsched.com/v3/appointment/<hold-id>/book \
-H "Authorization: Bearer <token>" \
-H "x-api-key: <company-key>" \
-H "Content-Type: application/json" \
-d '{"notes": "Confirmed via concierge"}'Troubleshooting
- “The selected time slot is not available.” Confirm you are passing the same
ResourceIdsthat were returned by the availability search and that no one else booked the slot in the meantime. - Invalid Customer errors: Ensure either
Customer.idexists or you include required creation fields. Holds without customer context only work whenisHoldis true. - Notifications skipped intentionally:
skip_notifications=trueapplies to immediate create requests and reschedules. On reschedule it skips email/SMS only; the lifecycle webhook and calendar synchronization still run. Book, confirm, and cancel endpoints still emit their lifecycle notifications/webhooks. - Missing fields from public routes: Public endpoints deliberately trim fields like
notes,CustomFields, and contact info. Use authenticated company routes for internal tooling. - External calendar missing after “confirm”:
PUT /confirmdoes not call Google or Outlook. UsePUT /book(from a hold) orPOST /v3/appointment(immediate book) so the API can create calendar events. - Investigating calendar issues in logs: Search API logs for the appointment id and for
[Calendar]warnings (failed create/delete) or messages fromGoogleCalendar(for exampleinvalid_grant). OnSched-tagged Google events includeextendedProperties.private.createdBy=ONSCHEDin the Calendar API; events without that tag were not created by this sync path.
With these patterns you can compose reliable booking flows while keeping OnSched’s scheduling engine as the source of truth.
Updated 17 days ago
