Documentation
Scheduling
scheduling.travel_estimate
REST
ShippingPOST /api/v1/scheduling/travel_estimateMCP tool
Livescheduling.travel_estimateExposed on: REST API · Shop MCP server · Claude connector · Dash (in-app copilot) · Zapier. Part of the Scheduling domain.
Operating contract
How long it takes to get from one mobile appointment to the next, in whole minutes, using the shop's own assumed speed and its minimum buffer.
ROUNDED UP, AND NEVER BELOW THE SHOP'S BUFFER. Half a minute of slack does not justify a tighter promise to a customer, and a shop that holds 20 minutes between mobile stops holds them whatever the map says.
WHEN EITHER END HAS NO COORDINATES the answer falls back to the minimum buffer and known: false says so. That is honest — we know a drive exists and not how far — and it is very different from 'the drive is short'. Do not promise a tight turnaround on an unknown one.
IN-SHOP WORK NEEDS NONE OF THIS. A drive buffer only applies when BOTH stops are mobile; a car swap in a bay is covered by the turnover buffer instead. needs_buffer says which case you are in.
This is a straight-line estimate at an assumed speed, not a routed drive time. It does not know about traffic, ferries or a closed bridge. Treat it as the floor of what to hold, not as an arrival time to give a customer.
One pair in, one answer out — nothing to truncate.
Who may call it
- Permission
shop.manageThe caller must hold shop.manage at the ACT level. A read-only dashboard grant on the same section is refused.- 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.
Input
| Field | Type | Description |
|---|---|---|
| from_latrequired | number-90–90 | Latitude of the first stop. |
| from_lngrequired | number-180–180 | Longitude of the first stop. |
| to_latrequired | number-90–90 | Latitude of the second stop. |
| to_lngrequired | number-180–180 | Longitude of the second stop. |
| both_mobile | boolean | False when either stop is work in the shop's own bay, which needs no drive buffer. Default:true |
Output
| Field | Type | Description |
|---|---|---|
| minutes | number | — |
| distance_km | number | null | — |
| known | boolean | — |
| needs_buffer | 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/scheduling/travel_estimate \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"from_lat":-90,"from_lng":-180,"to_lat":-90,"to_lng":-180,"both_mobile":true}'const res = await fetch("https://www.servicevin.com/api/v1/scheduling/travel_estimate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"from_lat": -90,
"from_lng": -180,
"to_lat": -90,
"to_lng": -180,
"both_mobile": true
}),
});
// 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/scheduling/travel_estimate",
headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
json={
"from_lat": -90,
"from_lng": -180,
"to_lat": -90,
"to_lng": -180,
"both_mobile": True
},
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": "scheduling.travel_estimate",
"arguments": {
"from_lat": -90,
"from_lng": -180,
"to_lat": -90,
"to_lng": -180,
"both_mobile": true
}
}
}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 | forbidden | This login does not hold shop.manage. |
| 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. |