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 a dashboard access token plusx-api-key. Public booking usesx-client-idand an Allowed Domain on/v3/public/*. - 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/cursor | Preferred forward-only traversal for exports and backend ingestion. Returns pageInfo.nextCursor instead of count; see Cursor Pagination for Appointments. |
GET /v3/appointments/filter | Deprecated alias retained for compatibility. Use /v3/appointments or /v3/appointments/cursor. |
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. Invalid IDs or overlapping times return 400 errors without revealing internal scheduling rules.
List Pagination and Request Limits
For exports, reconciliation jobs, and backend ingestion, prefer
Cursor Pagination for Appointments. Cursor traversal
does not compute a total count and its query cost does not grow with a page number.
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.
If an existing integration must continue using page pagination:
- 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.
Appointment-list requests use a company-scoped rate budget (burst of 10, then about
one request per second). That budget is separate from the generic API read budget.
On 429, honor Retry-After and retry the same page. Do not run parallel walks
over the same filters.
Public appointment list
GET /v3/public/appointments uses the same optional appointment filters and sort
options, with limit from 1 through 100 and page starting at 1. It returns
{ success, count, data }. The public response uses a restricted appointment field
set and does not include scheduledStartTime or scheduledEndTime. The 10,000-row
offset cutoff documented for the private list does not apply to this route. Public
requests use the generic public-read rate limit; on 429, wait for Retry-After
before retrying.
Supported filters are locationId, resourceIds, serviceIds, customerId,
status, from, and to. Resource and service IDs can be repeated or
comma-separated; status accepts IN, BK, RS, RE, or CN, repeated or
comma-separated. Dates use YYYY-MM-DD. sort is created or scheduled
(default created); order is asc or desc, defaulting to descending for
created and ascending for scheduled.
Booking Flow
- Hold (optional):
POST /v3/appointment/holdwithUnavailability.startTime/endTimeand optionalCustomer,CustomFields, andResourceIds. A positive LocationexpirationDelay(seconds) sets the hold's expiration time. New locations default to 900 seconds (15 minutes) when omitted; set0ornullto disable the timer. Existing locations keep their current value when this field is omitted. Without a positive delay, the hold has no timer and keeps the slot locked until you book or delete it. Locations created before this default may have no expiry and should set a positive delay. See Settings overview and Availability rules. - 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.
Finalize a hold or reserve
PUT /v3/appointment/:id/book and PUT /v3/public/appointment/:id/book
accept only these top-level JSON keys: Customer, bookedBy, notes,
location, and CustomFields. The lowercase location field is descriptive
text; it does not change the appointment's LocationId.
The book body cannot change Unavailability, ServiceId, LocationId,
ResourceIds, resourceId, status, or confirmed. Other unsupported keys,
including skip_notifications, are also rejected. A rejected body returns
400 with message: "Validation Error" and an errors array whose msg
names the unexpected fields and points to
PUT /v3/appointment/{id}/reschedule. The hold remains unchanged.
Public routes ignore overridePadding; see Appointment Padding Overrides.
Use the authenticated reschedule route to change times or resources, then book
the replacement hold using the new appointment ID. To choose another service
or location, create a new hold with the correct query parameters.
Use PUT /v3/appointment/:id/confirm to set confirmed.
allowPaddingOverflow remains a query option, rather than a top-level book
body key. A hold created with allowPaddingOverflow=true retains that setting
when booked. It permits padding past the end of the working day; it does not
waive conflicts with other appointments during the day.
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.
Ignore availability
Authenticated company-scoped callers (Machine Token or Dashboard JWT) can pass
ignoreAvailability=true (alias ignore_availability) on POST /v3/appointment
and POST /v3/appointment/hold. The API then creates the appointment or hold
even when the slot is outside hours, overlaps another appointment or an
external calendar block, is over capacity, or fails a service quota or
customer booking limit. Required location, service, and linked resources are
still validated.
On hold or reserve, the flag is stored as CustomFields.ignoreAvailability=true
so PUT /v3/appointment/{id}/book keeps skipping those checks. The API writes
that key only for an authenticated query flag. A value sent in the request body
does not grant the override. PUT /confirm does not re-validate availability.
Public /v3/public/appointment and /hold routes reject the query flag and a
truthy CustomFields.ignoreAvailability (or ignore_availability) with 403.
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
produces an update_padding audit event, and later availability requests use the new padding. 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.
Padding past closing time
When a slot ends exactly at a resource's scheduled close or at the location/service
closing time, padding may extend beyond that close if you pass
allowPaddingOverflow=true to both GET /v3/availability and the corresponding
POST /v3/appointment, /hold, or /reserve request. A hold stores this choice, so
PUT /v3/appointment/:id/book does not need the flag again. The flag does not waive
padding for a mid-day slot, or capacity consumed by another appointment, active
hold, or external calendar block. It also does not permit a resource that closes
later than the selected slot to exceed its own close.
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
Statuses are IN (hold), BK (booked), RS (reserved, no hold timer), RE (rescheduled-away), and CN (cancelled). There is no separate “completed” status. See Appointment statuses.
The API rejects conflicting transitions (for example, cancelling a CN appointment returns 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. A positive LocationexpirationDelay(seconds) sets the hold's expiration time. Without a positive delay, the hold has no timer and protects the slot until 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. The new row staysINorRSif the original was a hold or reserve; otherwise it isBK. 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. LocationId and ServiceId are query parameters.
curl -X POST "https://v3.onsched.com/v3/appointment/hold?LocationId=<location-id>&ServiceId=<service-id>&ResourceIds=<resource-id>" \
-H "Authorization: Bearer <token>" \
-H "x-api-key: <company-key>" \
-H "Content-Type: application/json" \
-d '{
"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
- Book returns “Validation Error” with “Unexpected field(s) in book request”: Send only
Customer,bookedBy,notes,location, andCustomFields. Readerrors[].msgfor the rejected keys. For time or resource changes, usePUT /v3/appointment/:id/rescheduleand continue with the replacement appointment ID. - “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. ImmediatePOST /v3/appointmentrequires a customer;POST /v3/appointment/holdcan omit the customer until you book. - 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: If a booked appointment is missing from Google or Outlook, confirm the resource has an active write calendar and that you used
POST /v3/appointmentorPUT /v3/appointment/:id/book(notPUT /confirm). Reconnect the calendar if authorization has expired.
See also
Updated 13 days ago
