Skip to main content
Documentation

Booking

booking.book

Take a booking for a customer at a chosen appointment time
WritesReaches the customerSensitiveWrite budget

REST

Shipping
POST /api/v1/booking/book

MCP tool

Live
booking.book

Exposed 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

FieldTypeDescription
service_idsrequiredstring[]

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_typerequiredstring

The customer's vehicle size, one of `vehicle_types` from booking.services.

One of: car, truck, suv, van, coupe, motorcycle, commercial, exotic
startstring

The slot's `start`, copied from booking.availability. Omit for 'first available'.

first_namerequiredstringmax 80 chars, min 1 chars

The customer's first name.

last_namestringmax 80 chars

The customer's last name.

Default: ""
emailstringmax 200 chars

Their email. Give an email or a phone number; both is better.

Default: ""
phonestringmax 40 chars

Their mobile number. Give an email or a phone number; both is better.

Default: ""
vehicle_yearinteger1900–2100

The car's year, if they said.

vehicle_makestringmax 60 chars

The car's make, if they said.

vehicle_modelstringmax 60 chars

The car's model, if they said.

notesstringmax 1000 chars

Anything the shop should know. Goes on the job for the shop to read.

staff_idstringuuid

The installer the customer asked for, if any — an id from `staff_ids` or `installers`, as returned by booking.availability.

booking_keyrequiredstringmax 128 chars, min 8 chars

One stable id for THIS booking attempt, re-sent unchanged on any retry. Prevents a duplicate appointment.

hold_tokenstringmax 128 chars

The `hold_token` booking.hold returned, when you took a hold.

session_keystringmax 128 chars

The same session_key you held with.

Output

FieldTypeDescription
bookedboolean

errorstring | null

referencestring | null

scheduled_startstring | null

service_namesstring[]

totalnumber | null

depositnumber | null

currencystring | null

manage_urlstring | null

pay_urlstring | null

installer_namestring | null

approval_pendingboolean

Examples

Built from this capability's own schema — required fields and the ones carrying a default, and nothing invented. Paste one and it validates.

curl
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":"…"}'

TypeScript (fetch)
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;

Python (requests)
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"]

MCP tools/call — https://www.servicevin.com/api/mcp
{
  "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.

StatusCodeWhen
404not_foundThe id is unknown, or the feature is not enabled for this account. Deliberately the same answer for both.
403insufficient_scopeThe credential is read-only and this capability writes.
422validation_errorAn argument was wrong. The message names the field.
429rate_limitedToo many write calls. Back off and retry.
500internal_errorSomething failed on our side. Nothing was changed.