Skip to content

Booking & Scheduling

UPC providers (groomers, trainers, veterinarians, sitters, kennels, transporters and more) manage availability and appointments on the UPC Scheduling engine, one scheduler for every type of service. Partners and providers can surface booking on their own websites three ways, all backed by the same Scheduling engine — pick the integration depth that fits:

TierEffortFor
Hosted booking pagenone — share a linkProviders who just want a “Book now” button
Embed widget (JavaScript)one script tagProvider websites that want booking inline without building UI
Partner APIfull integration, free; the business keeps a payment card on file and connects your appCustom solutions, native apps, CRMs, marketplaces, WordPress plugins, AI agents

Every provider gets a hosted booking page on UPC. Link to it from any website:

<a href="https://www.unitedpetclub.com/book/{providerHandle}">Book an appointment</a>

(The widget script is not available yet; link to the hosted booking page in the meantime.) The script is a smart loader: it reads your configuration, renders the entry point natively in your page (an inline panel, a “Book now” button that opens a modal, or a floating badge), and opens the booking flow itself in an isolated iframe themed to your site:

<script async src="https://cdn.unitedpetclub.com/embed/booking.js"
data-provider="{providerHandle}"
data-mode="button"
data-service="{serviceId}"
data-profile="spa-site"
data-scheme="dark"
data-locale="en-US"></script>

Or programmatically:

UPCBooking.init({
provider: "{providerHandle}",
mode: "inline", // inline | button | badge
target: "#book",
profile: "spa-site", // named embed profile from the provider's Dashboard
scheme: "dark", // match this page's color scheme
onCompleted: (booking) => analytics.track("booking", booking)
});

Brand theming is configured in the provider’s UPC Dashboard, not in the embed. Colors, font, logo, and corner radius live in Dashboard embed profiles — the flow fetches them server-side, so nothing brand-visual is controllable from the page (a deliberate security boundary: script injection on an embedding site cannot restyle the booking flow). A provider with multiple differently-branded websites defines one profile per site and each embed selects its profile by name. The embed itself passes only page-context hints: display mode, target, locale, preselected service/staff, and light/dark scheme.

Embedding is allowlisted. The provider registers the domains permitted to embed their booking flow in the Dashboard; other origins are refused at the frame level (frame-ancestors) and by the postMessage bridge. The iframe auto-resizes and emits lifecycle events (booking:completed) via that origin-checked bridge, so you can track conversions.

Custom CSS or scripts inside the flow are deliberately not supported: the iframe guarantees the transactional core (conflict handling, localization fallback, guest checkout, deposit payment isolation) is always UPC’s current audited code, and new features reach your site without redeploys. If you need full visual control, use the Partner API below and build your own UI.

Booked customers who don’t have a UPC account are recorded as the provider’s guest clients and can claim their history when they sign up.

The Partner API lets your app act for a business that connected it — a salon’s own booking app, a CRM that keeps its calendar in sync, a marketplace that books on its behalf. It lives on the API host, not on the United Pet Club apps:

https://api.unitedpetclub.com/api/scheduling/v1

The Partner API is free, for your app and for the business. Two things must be true before your app can call it:

  1. The business keeps a valid payment card on file (Dashboard → Finances → Billing), so that United Pet Club can charge it for the bookings and services its apps create. Without one every call answers 403 with platformResponseCode 0010010000407, and the business cannot approve your app on the consent page. A card that expires, is declined or needs re-verification counts as none.
  2. The business connected your app. You send the business to the United Pet Club consent page; it approves the scopes you ask for and you get an authorization code, which you exchange for an access token and a refresh token. That token acts for that one business — there is no business id in the paths. See Authentication → Connecting a business.

Software that installs itself on many sites or runs as an AI agent can register itself and connect without a developer account: see WordPress plugins and AI agents.

An app’s own client_credentials token acts for no business, so the partner routes refuse it (403, 0010010000406). The business can disconnect your app at any time (Dashboard → Settings → Connected Apps); its tokens stop working within seconds (401, 0010010000402).

ScopeWhat it allows
scheduling:readThe business, its services, open slots, booking options and price estimates
appointment:readThe business’s appointments
appointment:writeBook, reschedule and cancel appointments
client:readThe business’s clients and their pets; client e-mail and phone on appointments
webhook:manageAppointment webhooks — see Webhooks

Ask only for the scopes you need: the business sees each one on the consent page.

GET /api/scheduling/v1/business
GET /api/scheduling/v1/services?locale=fr-CA

/business answers businessProfileId, timezoneIanaName and isAcceptingBookings. /services lists the services clients can book: serviceId, catalogItemPublicCode, name and description in the requested locale, durationMinutes, deliveryModeCodes (AT_LOCATION, MOBILE, VIRTUAL) and allowSpecificStaff.

GET /api/scheduling/v1/services/{serviceId}/slots?from=2026-10-05&to=2026-10-11

from and to are dates (yyyy-MM-dd) at most 62 days apart. The answer lists the open start times (data[].startUtc / endUtc) with the staff free at each start (resources[]). Slots are computed live from the business’s hours, lead time, booking horizon, existing appointments and buffers — never cache them for more than a minute; the slot you show may be taken by the time you book it, and that is handled below.

Days per type of service (2026-10-09). A business can offer a type of service only on some days and times, for example grooming Monday to Wednesday and training at weekends. Slots honour that: a start is offered only when a qualified team member is working and it falls inside the windows the business set for that service’s type. Two services of the same business can therefore return different days for the same range; search each service separately, and do not reuse one service’s slots for another. A booking or reschedule at a start outside those windows answers 409 Scheduling_SchedulingPartner_AD_Slot_RJ like any other start that is not an open slot.

Public holidays (2026-10-09). A business sets how it opens on each public holiday of its region: closed, usual hours, or special hours. Slots honour that too: a holiday it closes for returns no slots, and a holiday with special hours returns only the starts whose whole visit fits inside them. A booking or reschedule on such a start answers 409 Scheduling_SchedulingPartner_AD_Slot_RJ. Nothing changes on a holiday kept on usual hours, and the business’s regular weekly opening hours do not narrow slots (its team’s working hours do). To show the holidays themselves — “Closed on 25 December” — read the business profile’s opening calendar.

Mobile visits. For a service delivered MOBILE, pass the client’s location so only times when a groomer and a van covering that address are free come back:

GET /api/scheduling/v1/services/{serviceId}/slots?from=…&to=…&latitude=49.28&longitude=-123.12&postalCode=V6B%201A1

Send latitude/longitude when you have them (radius areas) and postalCode always (zone areas). The answer gains vehicleBound (the van check ran), each slot lists the vehicles[] free at that start, and outsideServiceArea: true with an empty list means the business runs vans but none serves this address — show “not in this business’s area”, not “fully booked”.

Grooming prices depend on the pet — size, coat, matting, how it handles — and on when the visit happens (weekends, evenings, public holidays). Read what the business asks and offers, then ask for an estimate before you book:

GET /api/scheduling/v1/booking-options?serviceId={serviceId}&locale=fr-CA
POST /api/scheduling/v1/estimate

/booking-options returns the business’s booking questions (intakeQuestions[], with localized prompts and the answer codes you send back), the named styles it offers per region, its priced extras, and the agreements a client must accept. Do not hard-code prompts or answer labels.

The grooming questions belong to grooming services only (since 2026-10-09). When the business has not classed the service as grooming (a training session, a walk, a vet visit), a booking ignores the grooming answers on its pets (coat, clip and style choices, extras) and raises no clip or corded-coat advisory. Agreements still apply to every service.

{
"serviceIds": ["…full groom…", "…nails…"],
"startUtc": "2026-10-03T14:00:00Z",
"pets": [
{ "name": "Biscuit", "speciesCode": "DOG", "breedTaxonomyCode": "…", "weightKg": 9,
"coatConditionCode": "LIGHT_MATTING", "behaviorCode": "NERVOUS",
"clipSpecs": [ { "regionCode": "BODY", "lengthMm": 3 }, { "regionCode": "HEAD", "styleCode": "TEDDY_BEAR" } ],
"extras": [ { "kindCode": "MEDICATED_BATH", "note": "vet-prescribed chlorhexidine" } ] }
]
}

Your app describes the pets the client tells you about: species, breed (breedTaxonomyCode) or isMixed, weightKg or sizeClassCode, the coat fields and today’s answers. At most 10 pets. The answer carries one line per service × pet (totalMinorUnits is null until the business has a rate card), the deposit that would apply, the questions the business asks, and advisories.

Prices are estimates. Present every grooming price as an estimate: the groomer confirms it once they have seen the pet (coat condition, size, temperament). The hosted booking page labels its summary Estimated price; your UI should do the same.

Clip advisories. Asking for a short body clip (at or below the business’s threshold, 10 mm by default) on a double-coated, wire-coated or unknown coat raises an advisory (DO_NOT_SHAVE, HAND_STRIP_ONLY, COAT_UNKNOWN) with the text to show and a textHash. Depending on the business’s setting the booking then needs the client’s acknowledgement (409 Scheduling_Booking_VL_ClipAdvisory_RJ without it) or is refused outright (409 Scheduling_Booking_VL_ClipRefused_RJ).

Corded coats. coatConditionCode accepts CORDED — a coat kept in cords (Puli, Komondor, Bergamasco, corded Poodle). It is not matting, but it takes hours to dry, so a corded coat raises a CORDED_COAT advisory. Depending on the business’s setting the booking then needs the acknowledgement (409 Scheduling_Booking_VL_CordedCoat_RJ without it) or is refused (409 Scheduling_Booking_VL_CordedRefused_RJ).

POST /api/scheduling/v1/appointments
Idempotency-Key: 5f1c0a4e-booking-7781
{
"serviceId": "…",
"startUtc": "2026-10-03T14:00:00Z",
"client": { "givenName": "Jane", "familyName": "Doe", "email": "jane@example.com", "phone": "+16045550100", "languageCode": "en" },
"pets": [ { "name": "Biscuit", "speciesCode": "DOG", "weightKg": 9, "coatConditionCode": "LIGHT_MATTING" } ],
"notes": "Side gate is open.",
"address": { "formatted": "123 Main St, Vancouver BC", "latitude": 49.28, "longitude": -123.12, "postalCode": "V6B 1A1" },
"references": [ { "url": "https://photos.example.com/biscuit-summer", "label": "last summer" } ],
"acknowledgements": [ { "advisoryCode": "DO_NOT_SHAVE", "textHash": "…" } ],
"agreementAcceptances": [ { "agreementId": "…", "bodyHash": "…", "signerName": "Jane Doe" } ]
}
  • The start must be an open slot of that service. If it is not (anymore), the booking answers 409 Scheduling_SchedulingPartner_AD_Slot_RJ: search the slots again and offer the client another time — do not retry the same instant. A race the slot check cannot see answers 409 Scheduling_Booking_AD_Conflict_RJ (slot clash) or 409 Scheduling_Booking_AD_Capacity_RJ (no qualified staff free); handle them the same way. A start on a holiday the business closes for (or outside its holiday hours) is not an open slot either. On the hosted booking page and the embed widget the same refusal is 409 Scheduling_Booking_VL_ProviderClosed_RJ (“Closed at that time”), which the booking flow shows to the client itself.
  • The client needs a name and an e-mail address or a phone number. It becomes (or is found again, by e-mail or phone) one of the business’s own client records.
  • address is for a service that comes to the client (MOBILE); it is snapshotted onto the appointment and feeds distance-based pricing. Outside every van’s area: 409 Scheduling_Booking_VL_ServiceArea_RJ; no van free: 409 Scheduling_Booking_VL_Vehicle_RJ.
  • references are up to five links (https) to photos of the look the client wants.
  • acknowledgements echo every advisory the estimate raised, with the textHash of the text you showed. agreementAcceptances accept every required agreement from /booking-options, with the bodyHash you were given and a typed name where requiresTypedName is true; 409 Scheduling_Booking_VL_Agreement_RJ means one is missing or the business changed its text — reload the booking options and show the new version.
  • Idempotency-Key (up to 100 characters, recommended): repeating a request with the same key and the same body answers the booking it made, with 200 and Idempotent-Replayed: true instead of 201. The same key with a different body is 409 Scheduling_SchedulingPartner_AD_Idempotency_RJ. Keys are per app.

A new booking answers 201 Created with the appointment and a Location header. What state it lands in is the business’s policy: CONFIRMED right away, REQUESTED / PENDING_PROVIDER when the business reviews requests, or HOLD when a deposit is due — the business turns the hold into a payment request the client pays through United Pet Club, and an unpaid hold lapses at holdExpiresAtUtc. Your app never takes a payment. The business must be verified for online bookings: until it is, bookings answer 403 Scheduling_Booking_AD_VerificationRequired_RJ (see Verification before public bookings). It must also have finished its booking setup: until it has, new bookings answer 409 Scheduling_Provider_AD_NotReady_RJ (see A business that is not ready for bookings).

GET /api/scheduling/v1/appointments?from=2026-10-01T00:00:00Z&to=2026-10-31T00:00:00Z&state=CONFIRMED&limit=200
GET /api/scheduling/v1/appointments/{appointmentId}

from and to (ISO 8601) are required and at most 62 days apart; state is optional; limit is 1–500 (default 200). The list covers every appointment of the business in that window, not only the ones your app booked — bookedByThisApp tells them apart. Each appointment carries stateCode, startUtc, endUtc, timezoneIanaName, serviceId, deliveryModeCode, bookingOriginCode, the client (kind GUEST or MEMBER, displayName; email and phone only when your token has client:read), pets[], note, addressFormatted, the quote (quotedAmountMinorUnits, quotedCurrencyCode), the deposit fields, settlementStatusCode and, once cancelled, cancellationReasonCode / cancelledAtUtc.

To stay in sync without polling, subscribe to webhooks.

POST /api/scheduling/v1/appointments/{appointmentId}/reschedule { "startUtc": "2026-10-04T15:00:00Z" }
POST /api/scheduling/v1/appointments/{appointmentId}/cancel { "reason": "CUSTOMER_REQUEST" }

A new start must be an open slot of the appointment’s service, as for a booking (409 Scheduling_SchedulingPartner_AD_Slot_RJ otherwise). reason is CUSTOMER_REQUEST (the default), PROVIDER_REQUEST, WEATHER, EMERGENCY, ILLNESS or OTHER; a late customer cancellation may assess a fee under the business’s policy — the call still answers 200 with the cancelled appointment, and the fee arrives as a separate charge. A move or cancel the appointment’s state does not allow is 409 Scheduling_Booking_UP_State_RJ.

GET /api/scheduling/v1/clients?search=doe&page=1&pageSize=25
GET /api/scheduling/v1/clients/{clientId}
GET /api/scheduling/v1/clients/{clientId}/pets

Needs client:read. The list (pageSize 1–100; search matches a name, client number, contact, phone digits or pet name) returns the business’s clients with their pets in brief, visit counts and lastVisitAtUtc / nextVisitAtUtc, plus pagination. A client’s detail adds contacts[], addresses[] and full pets[] (handling notes and alerts included). Respect optedOutAtUtc on a contact: never message a channel the client opted out of. Merged and erased client records answer 404.

A full pet carries color (free text) and, since 2026-10-02, appearance when its colours are recorded as codes: { "areas": [ { "areaCode": "COAT", "areaName": "Coat", "colors": [ { "code": "BLACK", "name": "Black", "hexCode": "#1C1C1C", "accentHexCode": null } ], "patterns": [ { "code": "TAN_POINT", "name": "Tan point" } ], "patternSlots": [] } ], "markings": [ { "markingCode": "BLAZE", "name": "Blaze", "colorCode": "WHITE", "colorName": "White", "note": null } ], "summary": "Black · Tan point; Blaze (White)", "source": "PICKED", "unmatchedText": null }. Names follow the request’s Accept-Language. source says where the codes came from: ANIMAL (the owner’s pet profile), PICKED (chosen by the business) or PARSED (read from the typed color; unmatchedText holds the words no code covers). appearance is null when no colour is recorded; color is always kept. The hex values above are illustrative.

StatusplatformResponseCodeCauseWhat to do
4010010010000402The business disconnected your app, or your app was suspended.Stop; ask the business to connect the app again.
4030010010000406A client_credentials token on a route that acts for a business.Use a token from the authorization-code grant.
4030010010000407The business has no valid payment card on file (any more).Tell the business; it adds a card in the Dashboard (Finances → Billing). Calls work again within about 10 seconds.
5030010010000503The business’s payment card or its connection could not be checked right now.Retry after Retry-After seconds.
400 / 404 / 409 / 503Scheduling_… or Clientele_…A refusal of the booking or client service, relayed with its status and code — for example Scheduling_SchedulingPartner_AD_Slot_RJ or Scheduling_Booking_VL_Agreement_RJ.See the sections above.
409Scheduling_Provider_AD_NotReady_RJThe business has not finished its booking setup; no new bookings for now.Do not retry. Tell the client the business can’t take online bookings yet; see below.
503Scheduling_Provider_HD_Readiness_FLThe business’s booking setup could not be checked right now. Nothing was booked.Retry with backoff.

Everything else follows Errors. Rate limits count per app and per connected business (see Rate limits).

An appointment gets three business-initiated text messages (confirmation, 24-hour reminder, “on my way” / “ready for pick-up”), reset when the appointment is rescheduled; e-mail and push notifications are separate and unlimited. Since 2026-09-30 the three texts are included only when the booking is paid online through United Pet Club (a deposit is enough), or the business is on Pro or a Partner in good standing; otherwise every text uses one of the business’s text credits (WhatsApp and SMS reminders). The business sends the staff-initiated text from its Dashboard; when no included text applies and it holds no text credits the send is refused and the appointment timeline records the non-send. Nothing about the software is metered per seat, location or client — see Fees on bookings.

Showing businesses that come to the client? The coverage reads are anonymous platform app endpoints (https://www.unitedpetclub.com/api/scheduling/v1/…), not Partner API routes: GET /api/scheduling/v1/mobile-coverage?profileId={id} → mobileServiceAvailable, inStoreServiceAvailable, baseAddressHidden and areas[] (RADIUS with centerLat/centerLng/radiusKm, or ZONES with postal prefixes). When the business hides its base address the centre is approximate (centerPrecisionCode: APPROXIMATE) — for “near me” use GET /mobile-coverage/contains?profileId=…&lat=…&lng=…&postalCode=… and let the platform answer exactly. A location lists once; there is no listing per van.

Start with the hosted page or widget — they are always up to date with new features. Move to the Partner API only when you need custom UI or server-side flows for a business that keeps a payment card on file and has connected your app. All three tiers share the same engine and localization, so you can mix them (e.g. widget on the marketing site, Partner API in the business’s own app).

The booking software is free to providers. Since 2026-10-09 every seller surface names the fees a provider pays with three names, and so does this page:

FeeWhat it isRate
CommissionUnited Pet Club’s percentage on a sale, charged once on the booking total (a deposit followed by a balance pays one Commission)Service bookings 1.25 %, minimum 0.50. Classes 7 % in person, 17 % online, minimum 0.50
Platform Processing FeeUnited Pet Club’s published card-processing rate, on card paymentsToday it equals the card network list price (2.9 % + 0.30 online, 2.7 % + 0.05 in person)
Stripe Payout FeesStripe’s costs for paying the provider out, passed on at cost with no margin0.25 % + 0.25 per payout, plus 2.00 in each calendar month the provider gets at least one payout
  • Classes. A booking is priced as a class when the service is a group class (more than one place per slot), or when the client buys a class enrollment. It is an online class when the appointment’s delivery mode is VIRTUAL (or, with no delivery mode, when the service has a video mode); otherwise it is in person.
  • The Pro plan’s 0 % Commission covers service bookings only, never classes. A class booked with a Pro business still carries the class Commission.
  • The Platform Processing Fee is United Pet Club’s own versioned schedule, per country and currency; a new version is announced at least 30 days before it takes effect. Tips carry the Platform Processing Fee only.
  • Stripe Payout Fees are priced per country (in CAD for Canada, USD for the United States). They are taken from the provider’s next transfer and listed on its fee statement; Canadian providers also pay GST/HST on them, others no tax. They concern the provider’s payouts only: a booking and its price never change because of them.

Nothing in the booking, availability or embed APIs changes because of fees, and the Partner API routes carry no fee rates. What a partner integration sees is the fee snapshot on a booking’s earnings line: platformFeeMinor (the Commission), processingFeeMinor (the Platform Processing Fee), commissionRateId (the schedule row that priced the booking) and processingRateVersionId (the processing-rate version in force when it was paid). Treat both ids as opaque; they let a provider reconcile a payout statement against the fee schedule that applied on the day, and a later schedule change never re-prices a booking that already carries them.

The Platform Agreement (section 13.5) asks the business to record the true price of every booking, including bookings your app creates or reports. Each service has a reference price for the business’s country (and province or state): when the price recorded is lower, the Commission is worked out on the reference price. This changes nothing for an integration:

  • Bookings and their prices are never refused, changed or re-quoted. A partner booking is priced from the provider’s own price list, as before.
  • No API carries a reference price, and none is published. The provider sees it only in its own Dashboard, when it sets the fee.
  • A booking pays one Commission however it is paid: deposit by card and balance in cash, or several outside payments, are priced together on the booking’s total.
  • A completed booking that is never paid through United Pet Club or marked paid outside is charged the Commission on its quoted price 14 days after completion. If your integration takes payment in its own checkout, have the provider record it in United Pet Club so the booking is settled.

Bookings below their reference price can be reviewed; reporting a lower price than the one charged breaks section 13.6.

The schedules themselves are public. Two anonymous reads on the gateway (no bearer) return them:

GET /api/commission/v1/public/fee-sheet?saleKind=SERVICE_BOOKING&country=CA
GET /api/commission/v1/payout-fee-rates/current?country=CA

public/fee-sheet is the schedule of fees for one sale kind in one country (Commission, Platform Processing Fee and usage rates, current and upcoming); it is cached for an hour. Since 2026-10-09 it also carries a payoutFees object: the Stripe Payout Fees of that market, the same document payout-fee-rates/current returns on its own (cached for ten minutes):

{
"data": {
"id": "d0000000-…",
"countryCode": "CA",
"currencyCode": "CAD",
"perPayoutBps": 25,
"perPayoutFixedMinor": 25,
"monthlyActiveAccountFeeMinor": 200,
"effectiveFromUtc": "2026-10-09T00:00:00+00:00",
"level": "COUNTRY",
"effectiveToUtc": null
},
"resultCode": "Commission_PayoutFeeRate_GT_Resource_SU"
}
FieldMeaning
perPayoutBpsThe percentage of every payout, in basis points (25 = 0.25 %); rounded half up to the minor unit
perPayoutFixedMinorThe fixed part of every payout, in minor units of currencyCode
monthlyActiveAccountFeeMinorCharged once in each calendar month the provider gets at least one payout
levelCOUNTRY (the market’s own row) or PLATFORM (the currency’s row for every other country)
effectiveFromUtc / effectiveToUtcThe window the row is in force; effectiveToUtc is null while open-ended

country is ISO 3166-1 alpha-2 and defaults to CA; currency defaults to the country’s settlement currency. An invalid country answers 400 Commission_Resolver_VL_Country_RJ; a market with no published Stripe Payout Fees answers 404 Commission_PayoutFeeRate_GT_Resource_FL. Show these as the provider’s costs, never as something the client pays.

Bookings never go through an app store. A booking is a service delivered in person, which Apple’s and Google’s billing rules exclude from in-app purchase, so a booking made from the native app is paid through Stripe exactly as on the web — no store commission, no marked-up price, and the same two ids on its earnings line. (Digital items and subscriptions bought inside the app do carry the store’s commission as a further schedule row, added on top of the platform fee and never taken from the professional’s share; that is a separate earnings line kind, STORE_RECEIPT, and is not a booking.)

A booking asks the client for money as payment requests — a deposit when the provider’s deposit policy applies, the balance (plus any tip) when the appointment is completed, a no-show fee when one is assessed. The appointment read (GET v1/appointments/{id}) carries them under payment:

FieldMeaning
settlementStatusCodeNONE, DEPOSIT_DUE, DEPOSIT_PAID, BALANCE_DUE, SETTLED (every request paid through the platform) or SETTLED_OUTSIDE (the provider recorded a cash / transfer payment). Independent of the lifecycle state.
outstandingMinorUnitsThe sum of requests still open, tax included.
payments[]One row per request: kindCode DEPOSIT / BALANCE / NO_SHOW_FEE (or OUTSIDE for a recorded outside payment), statusCode REQUESTED / PAID / CANCELLED, amountMinorUnits, currencyCode, tipMinorUnits, and — once the platform has prepared the payment — orderId, orderPaymentId, taxMinorUnits (the tax the platform computed for this request, on top of the amount; null when there is no tax basis) and totalMinorUnits (amountMinorUnits + taxMinorUnits — what the checkout must charge).

A client pays a request by card through the standard checkout for totalMinorUnits, naming the request’s orderId and tagging the intent with upcOrderPaymentId; a hosted booking page or embed does this for you. Tax is computed by the platform on the quoted service total for the provider’s location and allocated across the deposit and the balance (never on a tip); the provider collects and remits it. Quoted amounts are always before tax, in every market: in VAT markets (EU, GB, CH, AU) the VAT is added on top too, so taxMinorUnits is never zero there just because VAT is shown tax-inclusive on product pages. The provider (owner or staff — never the client) can instead record that a booking was paid directly: POST v1/appointments/{id}/settle-outside { amountMinorUnits, currency?, method } with method CASH / ETRANSFER / OTHER, allowed while the appointment is confirmed, in progress, completed or a no-show and not yet settled. The Commission on the amount recorded is netted from the provider’s next payout; a low amount is never refused (the minimum fee applies). Refusals are governed codes: Scheduling_Settlement_UP_State_RJ (409), _VL_Owner_RJ (403), _VL_Request_RJ (400), and Payment_CardOnFile_AD_Required_RJ (409) when the provider’s account has neither a verified card on file nor a positive balance covering the fee on the amount — the provider adds a card under Finances → Billing in the Dashboard (a $0 verification; nothing is charged) and the call can be repeated. The same code is returned when a service’s video mode is switched to PLATFORM.

A session redeemed against a package or pass the client already bought is booked with paidByPackageId on the booking request; it raises no payment request, because the fee was paid once on the purchase.

Providers read their own fee sheet in the Dashboard under Finances → Fees; a hosted booking page or embed never shows fees to the client, because the client pays the listed price and the provider pays the fees out of it.

Verification before public bookings (2026-09-20)

Section titled “Verification before public bookings (2026-09-20)”

A business takes online bookings — the hosted page, the embed widget, Reserve with Google and POST scheduling/v1/public/bookings — only once it is public-ready: a verified business address and a connected payout account whose identity Stripe has verified. There is nothing to configure on the partner side; the platform reads the provider’s verification gates on every public booking. Until the business is verified (and from the platform’s enforcement date), public bookings answer Scheduling_Booking_AD_VerificationRequired_RJ (403, detail “Verification required: …” with the reason codes ADDRESS_UNVERIFIED, IDENTITY_UNVERIFIED / IDENTITY_PENDING / IDENTITY_FAILED, PAYOUT_NOT_CONNECTED). Show the visitor the message and do not retry; the provider clears the steps from their Dashboard checklist. If the gate itself cannot be read the booking fails closed with Scheduling_Booking_HD_Verification_FL (503) — retry with backoff. Bookings the business records itself through the manage endpoints are never gated, and neither is availability search. Text reminders on a booking additionally need the provider’s payout account connected (see Text reminders on a booking); a reminder the platform could not send shows on the timeline as TEXT_NOT_SENT_UNVERIFIED.

A business that is not ready for bookings (2026-10-09)

Section titled “A business that is not ready for bookings (2026-10-09)”

A business takes new bookings only once it has finished its booking setup in its Dashboard: payouts connected, the United Pet Club service fees accepted, and a payment card on file. The platform announces the date from which this is required; before it, nothing is refused. From it, for a business that is not ready:

CallAnswer
POST /api/scheduling/v1/appointments (book), and the hosted page, the embed and POST scheduling/v1/public/bookings409 Scheduling_Provider_AD_NotReady_RJ
Slot search, /booking-options, /estimate, reading appointmentsAnswered as usual
Reschedule, cancel, and every change to an appointment that is already bookedAnswered as usual: booked appointments are never blocked

The 409 body carries data.reasons[], the steps the business still has to finish: PAYOUT_NOT_CONNECTED, FEE_TERMS_NOT_ACCEPTED, NO_CARD_ON_FILE (and, rarely, FEE_SHEET_UNAVAILABLE or FEE_TERMS_UNPUBLISHED when the fees for its country could not be read or are not published). They are for your logs and for the business; do not show them to the client. Show the client that the business can’t take online bookings right now, and do not retry: the business clears the steps from its Dashboard, and bookings work again from then on, with nothing to change on your side.

If the setup cannot be checked at that moment, the booking fails closed with 503 Scheduling_Provider_HD_Readiness_FL and nothing is booked; retry with backoff.

This is separate from verification (a 403) and from the payment card your app connection needs (0010010000407): without a valid card on file, your app’s calls are refused before they reach booking.

A service whose delivery modes include VIRTUAL carries a videoModeCode on the service read: PLATFORM (United Pet Club creates a private video room per booking — one per class slot — that opens 10 minutes before the start and closes 30 minutes after the end) or EXTERNAL_LINK (the provider’s own meeting URL). Book it exactly like any other service with deliveryModeCode: "VIRTUAL".

  • The appointment detail carries a video object: videoModeCode, roomStatusCode (SCHEDULED | OPEN | LIVE | ENDED | …), opensAtUtc / closesAtUtc, liveParticipantCount, the platform join page URL (joinUrl) and, once the room closed, the metered actuals (billableParticipantMinutes, usageAmountMinorUnits).
  • Never embed a token in a link. The join page mints a short-lived grant on click: POST scheduling/v1/appointments/{id}/video/join for a signed-in participant (provider = host, staff = presenter, customer = attendee), POST scheduling/v1/public/video/join { appointmentId, token } for a guest, where token is the signed value in the guest’s joinUrl (?t=). The answer is EMBEDDED (token + roomUrl: open roomUrl?t=token in an iframe with camera / microphone allowed) or LINK_OUT (joinUrl). Refusals: 403 Scheduling_SchedulingVideo_VL_Participant_RJ, 409 …_AD_Join_RJ (outside the open window), 503 …_AD_Room_FL (retry).
  • Cost: platform video is metered at the published VIDEO_PARTICIPANT_MINUTE usage rate (GET commission/v1/usage-rates/current?kind=VIDEO_PARTICIPANT_MINUTE&country=CA&currency=CAD, $0.01 today) per participant-minute, staff included, and deducted from the provider’s payout monthly. That read needs a bearer (any signed-in profile; without one the gateway answers 401); for the same rate with no token, read the anonymous published fee sheet, which carries the usage schedule, current and upcoming. GET scheduling/v1/services/{id}/video-estimate?participants=2&minutes=60 returns the estimate for one session. The booking fee is unchanged. EXTERNAL_LINK sessions cost nothing.