Documentation
Booking
booking.availability
REST
ShippingPOST /api/v1/booking/availabilityMCP tool
Livebooking.availabilityExposed on: REST API · Booking MCP server. Part of the Booking domain.
This capability answers to an older id too
booking.window still resolves here, on every surface, so an integration written against it keeps working. New code should use booking.availability.Operating contract
The real bookable grid: the days the shop can take this visit and the exact start times on each. Every time here has been through the shop's whole gate — opening hours, closed days, lead time, booking horizon, bay and installer capacity, daily caps, mobile drive time and anybody else's live hold — so a time that is here is a time the shop can genuinely take.
OFFER start VERBATIM. It is an absolute ISO 8601 instant, and it is what booking.hold and booking.book take. time and date are the same moment in the shop's own timezone and exist so you can say it out loud; never rebuild start from them.
A day with full: true and full_reason: "closed" is a day the shop does not work; "booked" means it works then and is full. Say which — 'we're closed Sundays' and 'Sunday is fully booked' are different sentences and a customer notices.
staff_ids is present only where the shop lets customers pick an installer. Pass one back as staff_profile_id to narrow every later call to that person's real openings.
TRUNCATION: days_omitted is how many further days of the shop's horizon are NOT in this response. When it is not 0 there may be openings you have not seen — raise max_days (up to 60) before telling a customer there is nothing available.
An EMPTY grid with days_omitted: 0 is a real answer: nothing is bookable inside the shop's horizon for this visit. Say so and offer to take their details, rather than suggesting a time you invented.
SOME VISITS RUN IN MORE THAN ONE BAY. When a slot's plan is present the car goes through plan.bays bays on one booking — the customer still drops off once, at the time you are offering, and nothing else changes for them. When plan.days holds more than one day the car is with the shop across those days: say so, in those words. NEVER quote a finish, ready or collection time for any booking — this response contains none, the shop cannot predict one for a car moving between bays, and the shop confirms it on the day.
duration_minutes is the TOTAL BAY TIME this visit takes, summed across every service and every bay. It is not how long the customer waits and it is not an end time: a multi-bay visit has waits between its blocks, so the car is with the shop longer than this and finishes at a time nobody has promised.
Who may call it
- Permission
- NoneThe same grid the anonymous storefront draws at /book/<slug>. It exposes open times and nothing about who is in them, which is why the public page can render it without a login.
- Plan
- Every planNo plan gate. Available on every Service VIN plan.
- Retries
naturalNaturally idempotent — running it twice leaves the same world as running it once. A retrying integration needs no key.- Rate class
readCounted against the read budget — the widest of the four.
Partial answers
days_omitted is how many matching records are not in the response, and 0 is a real answer meaning you have all of them. There is no cursor here — narrow the filter instead.
Do not summarise from a truncated response
Input
| Field | Type | Description |
|---|---|---|
| service_idsrequired | string[] | The services to book, as `id` values from booking.services. One id books one service; several book one visit covering all of them, and the slot is sized for the whole visit. |
| vehicle_typerequired | string | The customer's vehicle size, one of `vehicle_types` from booking.services. One of:car, truck, suv, van, coupe, motorcycle, commercial, exotic |
| max_days | integer1–60 | How many days of the grid to return, 1-60. Defaults to 14. Default:14 |
| staff_id | stringuuid | Narrow to one installer, as an id from a slot's `staff_ids` or from `installers`, both as returned by booking.availability. Omit for anyone available. |
Output
| Field | Type | Description |
|---|---|---|
| timezone | string | — |
| duration_minutes | number | TOTAL BAY TIME for the whole visit, summed across its services and its bays — never an end time, and never how long the customer waits. |
| lead_time_hours | number | — |
| days | object[] | — |
| days[].date | string | — |
| days[].weekday | string | — |
| days[].full | boolean | — |
| days[].full_reason | string | null | — |
| days[].times | object[] | — |
| days[].times[].time | string | — |
| days[].times[].start | string | — |
| days[].times[].staff_ids | string[] | null | — |
| days[].times[].plan | object | null | The chain planned for THIS drop-off, or null when the visit runs in one bay (which is most visits). The days a chain spans depend on where it starts, so read the plan on the time you are actually offering — not the response-level one. |
| days_omitted | number | — |
| installers | object[] | — |
| installers[].id | string | — |
| installers[].name | string | — |
| blocked_reason | string | null | — |
| plan | object | null | The chain for the EARLIEST offered drop-off, or null when the visit runs in one bay. A summary of the visit; the plan on the slot you actually offer is the one to read out. |
| plan.bays | number | How many bays the car goes through. 1 for an ordinary visit. |
| plan.days | string[] | The shop-local `YYYY-MM-DD` days the car is with the shop, in order. More than one means the visit spans days — say so. |
| plan.blocks | number | How many separate blocks of work the visit is. |
Examples
Built from this capability's own schema — required fields and the ones carrying a default, and nothing invented. Paste one and it validates.
export SERVICEVIN_API_KEY=svk_live_…
curl -X POST https://www.servicevin.com/api/v1/booking/availability \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"service_ids":["…"],"vehicle_type":"car","max_days":14}'const res = await fetch("https://www.servicevin.com/api/v1/booking/availability", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"service_ids": [
"…"
],
"vehicle_type": "car",
"max_days": 14
}),
});
// Success and failure are both envelopes. Switch on error.code, never
// on error.message — the codes are stable, the messages are for people.
const payload = await res.json();
if (!res.ok) throw new Error(payload.error.code);
const data = payload.data;import os, requests
res = requests.post(
"https://www.servicevin.com/api/v1/booking/availability",
headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
json={
"service_ids": [
"…"
],
"vehicle_type": "car",
"max_days": 14
},
timeout=30,
)
payload = res.json()
if not res.ok:
raise RuntimeError(payload["error"]["code"])
data = payload["data"]{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "booking.availability",
"arguments": {
"service_ids": [
"…"
],
"vehicle_type": "car",
"max_days": 14
}
}
}Refusals
The four gates run in this order on every surface, and the order is not arbitrary — see Authentication.
| Status | Code | When |
|---|---|---|
| 404 | not_found | The id is unknown, or the feature is not enabled for this account. Deliberately the same answer for both. |
| 422 | validation_error | An argument was wrong. The message names the field. |
| 429 | rate_limited | Too many read calls. Back off and retry. |
| 500 | internal_error | Something failed on our side. Nothing was changed. |