Documentation
Booking
booking.book
REST
ShippingPOST /api/v1/booking/bookMCP tool
Livebooking.bookExposed on: REST API · Booking MCP server. Part of the Booking domain.
Operating contract
Creates the appointment. The slot is re-validated against the same engine that drew the grid, the price is recomputed from the live catalog, the customer record is found or created, and the shop is notified exactly as if the customer had booked it themselves.
PASS booking_key, ALWAYS. It is one id you mint for this booking attempt and re-send unchanged on every retry. Without it a timeout you never saw the answer to becomes a SECOND appointment, a second deposit invoice and a second pay link. With it a repeat returns the original booking.
ONE CHANGE PER CALL DOES NOT APPLY HERE — this is a create, and every field describes the same new appointment.
Pass hold_token and the session_key from booking.hold when you took one, so the customer's own reservation is not the reason their booking is refused.
start is a slot's start copied verbatim from booking.availability. Omit it only if the shop offers 'first available' and the customer genuinely does not mind when.
READ reference AND manage_url BACK TO THE CUSTOMER. reference is what the shop will call the job; manage_url is how they reschedule or cancel it themselves, and it is the value booking.get, booking.reschedule and booking.cancel need — keep the token from it for the rest of the conversation.
approval_pending: true means this shop confirms bookings by hand. The slot IS held, but say 'request sent, the shop will confirm' rather than 'you're booked in'.
A refusal comes back as booked: false with error in the shop's own words — a price the shop has not set for that vehicle, a slot that has just gone, a detail that did not look right. Read it out; do not retry the identical call.
Who may call it
- Permission
- NoneThe public booking flow. Its caller is the customer (or an agent acting for them), not a member of staff, and the identical write happens with no credential at all at /book/<slug> — so a dashboard section is the wrong axis. What bounds it is the storefront's own gates: booking switched on, the slot engine, the recomputed price.
- Plan
- Every planNo plan gate. Available on every Service VIN plan.
- Retries
keyHonours an Idempotency-Key header. Pass a stable one and a retry replays the first answer instead of acting again.- Rate class
writeCounted against the write budget, which is tighter than a read.
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 |
| start | string | The slot's `start`, copied from booking.availability. Omit for 'first available'. |
| first_namerequired | stringmax 80 chars, min 1 chars | The customer's first name. |
| last_name | stringmax 80 chars | The customer's last name. Default:"" |
| stringmax 200 chars | Their email. Give an email or a phone number; both is better. Default:"" | |
| phone | stringmax 40 chars | Their mobile number. Give an email or a phone number; both is better. Default:"" |
| vehicle_year | integer1900–2100 | The car's year, if they said. |
| vehicle_make | stringmax 60 chars | The car's make, if they said. |
| vehicle_model | stringmax 60 chars | The car's model, if they said. |
| notes | stringmax 1000 chars | Anything the shop should know. Goes on the job for the shop to read. |
| staff_id | stringuuid | The installer the customer asked for, if any — an id from `staff_ids` or `installers`, as returned by booking.availability. |
| booking_keyrequired | stringmax 128 chars, min 8 chars | One stable id for THIS booking attempt, re-sent unchanged on any retry. Prevents a duplicate appointment. |
| hold_token | stringmax 128 chars | The `hold_token` booking.hold returned, when you took a hold. |
| session_key | stringmax 128 chars | The same session_key you held with. |
Output
| Field | Type | Description |
|---|---|---|
| booked | boolean | — |
| error | string | null | — |
| reference | string | null | — |
| scheduled_start | string | null | — |
| service_names | string[] | — |
| total | number | null | — |
| deposit | number | null | — |
| currency | string | null | — |
| manage_url | string | null | — |
| pay_url | string | null | — |
| installer_name | string | null | — |
| approval_pending | boolean | — |
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/book \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"service_ids":["…"],"vehicle_type":"car","first_name":"Sam Henderson","last_name":"","email":"","phone":"","booking_key":"…"}'const res = await fetch("https://www.servicevin.com/api/v1/booking/book", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"service_ids": [
"…"
],
"vehicle_type": "car",
"first_name": "Sam Henderson",
"last_name": "",
"email": "",
"phone": "",
"booking_key": "…"
}),
});
// 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/book",
headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
json={
"service_ids": [
"…"
],
"vehicle_type": "car",
"first_name": "Sam Henderson",
"last_name": "",
"email": "",
"phone": "",
"booking_key": "…"
},
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.book",
"arguments": {
"service_ids": [
"…"
],
"vehicle_type": "car",
"first_name": "Sam Henderson",
"last_name": "",
"email": "",
"phone": "",
"booking_key": "…"
}
}
}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. |
| 403 | insufficient_scope | The credential is read-only and this capability writes. |
| 422 | validation_error | An argument was wrong. The message names the field. |
| 429 | rate_limited | Too many write calls. Back off and retry. |
| 500 | internal_error | Something failed on our side. Nothing was changed. |