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 the dashboard trio of Authorization: Bearer <JWT>, x-api-key, and x-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/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/filterFilter by status, date range, resource, or location via query params.
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 (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 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.

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

  1. Hold (optional): POST /v3/appointment/hold with Unavailability.startTime/endTime and optional Customer, CustomFields, and ResourceIds. Holds respect expiration windows defined per location (expirationDelay seconds). When a location has no expirationDelay (or it is 0), the hold never expires on its own and keeps the slot locked until you book or delete it.
  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.

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.

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

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 quick IN.
  • 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=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/holdIN status. The slot is locked until expirationDelay (seconds) on the location elapses or you finalize it. Without a positive expirationDelay, the hold has no timer and behaves like RS until booked or deleted.
  • POST /v3/appointment/reserveRS status. The slot stays locked without that hold timer; finalize with PUT /v3/appointment/:id/book or remove the appointment when done.
  • POST /v3/appointmentBK status. Confirmation emails/SMS and confirmation webhooks fire unless you pass skip_notifications=true.
  • PUT /v3/appointment/:id/bookBK 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; status on the new row follows the same hold/reserved vs booked rules as POST /v3/appointment / #buildAppointmentData (not always 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 (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 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. Holds without customer context only work when isHold is true.
  • 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 in logs: Search API logs for the appointment id and for [Calendar] warnings (failed create/delete) or messages from GoogleCalendar (for example invalid_grant). OnSched-tagged Google events include extendedProperties.private.createdBy = ONSCHED in 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.


Did this page help you?