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.com for production. Replace the host with api-stage.onsched.com when 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 plus x-api-key. Public booking uses x-client-id and 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/availability first to present conflict-free slots.

Endpoint Overview

EndpointPurpose
GET /v3/appointmentsPaginated 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/cursorPreferred forward-only traversal for exports and backend ingestion. Returns pageInfo.nextCursor instead of count; see Cursor Pagination for Appointments.
GET /v3/appointments/filterDeprecated alias retained for compatibility. Use /v3/appointments or /v3/appointments/cursor.
GET /v3/appointment/:idRetrieve 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/auditReview appointment lifecycle audit events for support and operational troubleshooting. Includes rescheduleFromTrail of predecessor ids. See Appointment Audit Events.
POST /v3/appointmentCreate a booked appointment immediately.
POST /v3/appointment/holdCreate a short-lived hold (status: IN) so customers can finish intake steps.
POST /v3/appointment/reserveReserve a slot (status: RS) without hold expiration; finalize with PUT .../book or delete when no longer needed.
PUT /v3/appointment/:id/bookConvert a hold or reserved slot to a booked record without changing slot times.
PUT /v3/appointment/:idUpdate mutable properties such as notes, metadata, CustomFields, confirmed, or the appointment padding override.
POST /v3/appointment/:id/validateResourcesOptional preflight check for custom dashboards before accepting a resource change in the UI. This does not mutate the appointment.
PUT /v3/appointment/:id/rescheduleChange 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/confirmSet confirmed: true only; does not change status or external calendars.
PUT /v3/appointment/:id/cancelCancel and trigger notifications + webhooks.
DELETE /v3/appointment/:idPermanently 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 data is empty or when page * limit >= count.
  • Never advance to another page after an empty response.
  • Avoid parallel pagination walks over the same filters.
  • On 429, wait for the Retry-After number 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

  1. Hold (optional): POST /v3/appointment/hold with Unavailability.startTime/endTime and optional Customer, CustomFields, and ResourceIds. A positive Location expirationDelay (seconds) sets the hold's expiration time. New locations default to 900 seconds (15 minutes) when omitted; set 0 or null to 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.
  2. Book (and optional confirm): Call PUT /v3/appointment/:id/book to finalize a hold into BK with calendar sync, or use POST /v3/appointment to book in one step. Use PUT /v3/appointment/:id/confirm only when you need confirmed: true without changing status or calendars.
  3. Resource Assignment: Provide ResourceIds as 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.
  4. 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.
  5. 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 /confirm does 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.startTime and Unavailability.endTime stay 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 /reserve accepts overridePadding as a query parameter and stores it on the appointment.
  • PUT /v3/appointment/:id and PUT /v3/appointment/:id/reschedule accept overridePadding in 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 RE rescheduled placeholder for audit/history.
  • A new appointment record is created with the requested resource assignment. rescheduledFromAppointmentId on the new row is the original appointment id; rescheduledFromStartTime is that predecessor’s scheduled start instant.
  • The APPOINTMENT_RESCHEDULED webhook is emitted. Email and SMS are emitted unless skip_notifications: true is 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=BK returns booked appointments only; ?status=BK&status=CN returns booked and cancelled.
  • ?status=BK,CN is equivalent, and values are case-insensitive (bk works).
  • Omit status to keep the default behavior: appointments of every status, including holds (IN, RS) and rescheduled placeholders (RE).
  • An unrecognized or empty value returns 400 listing the accepted values.

Status transitions and side effects

  • POST /v3/appointment/hold → IN status. A positive Location expirationDelay (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 → RS status. The slot stays locked without that hold timer; finalize with PUT /v3/appointment/:id/book or remove the appointment when done.
  • POST /v3/appointment → BK status. Confirmation emails/SMS and confirmation webhooks fire unless you pass skip_notifications=true.
  • PUT /v3/appointment/:id/book → BK status. Finalizing a hold or reserve emits NEW_APPOINTMENT; this endpoint does not honor skip_notifications.
  • PUT /v3/appointment/:id/confirm sets confirmed: true and does not change status or sync external calendars. To finalize a hold into a booked record with calendar sync, use PUT /v3/appointment/:id/book.
  • PUT /v3/appointment/:id/reschedule leaves the original record behind in RE status (for audit/history) and creates a new appointment for the new time and/or resource assignment. The new row stays IN or RS if the original was a hold or reserve; otherwise it is BK. The new row’s rescheduledFromAppointmentId points at the predecessor and rescheduledFromStartTime snapshots that predecessor’s scheduled start. Pass skip_notifications: true to suppress email/SMS while retaining webhooks and calendar synchronization.
  • PUT /v3/appointment/:id/cancel moves the booking to CN, stops reminder cadence, and emits cancellation webhooks/notifications. Occupancy Unavailability (and padding Unavailability) is destroyed so the slot is free again. Private appointment responses keep the cancelled interval on scheduledStartTime and scheduledEndTime (read-only; set only by cancel). GET /v3/appointments date filters use occupancy start when present, otherwise the scheduled start, so cancelled rows still appear in from/to lists.
  • DELETE /v3/appointment/:id permanently removes only IN and RS holds. BK, CN, and RE appointments are retained for billing and lifecycle history; cancel a BK appointment instead.

Custom Fields & Metadata

  • Send CustomFields as an object. Keys must exist in your schema configuration.
  • Customer accepts either an existing id or the fields required to create a new record (firstName, email at minimum).
  • Unavailability represents 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, nested Unavailability is null; read scheduledStartTime and scheduledEndTime on the appointment instead.

Build appointments from availability results

Use the values returned by GET /v3/availability to avoid validation errors:

  • LocationId and ServiceId must match the request you used to find the slot.
  • ResourceIds should mirror the slot returned (or the resources you requested when roundRobin=NONE).
  • Unavailability.startTime/endTime should come directly from the chosen availableTimes entry.
  • Customer is required for immediate bookings; holds (POST /v3/appointment/hold) can be created without a customer and later promoted with PUT /v3/appointment/:id/book once 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, and CustomFields. Read errors[].msg for the rejected keys. For time or resource changes, use PUT /v3/appointment/:id/reschedule and continue with the replacement appointment ID.
  • “The selected time slot is not available.” Confirm you are passing the same ResourceIds that were returned by the availability search and that no one else booked the slot in the meantime.
  • Invalid Customer errors: Ensure either Customer.id exists or you include required creation fields. Immediate POST /v3/appointment requires a customer; POST /v3/appointment/hold can omit the customer until you book.
  • Notifications skipped intentionally: skip_notifications=true applies 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 /confirm does not call Google or Outlook. Use PUT /book (from a hold) or POST /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/appointment or PUT /v3/appointment/:id/book (not PUT /confirm). Reconnect the calendar if authorization has expired.

See also


Did this page help you?