Documentation
Staff
staff.list
REST
ShippingPOST /api/v1/staff/listMCP tool
Livestaff.listExposed on: REST API · Shop MCP server · Claude connector · Dash (in-app copilot) · Zapier. Part of the Staff domain.
Operating contract
The whole bench: who works here, what they are called, what they can do, whether the scheduler may book them, how much live work they are already carrying, and what their login can reach.
TWO IDS COME BACK AND THEY ARE NOT INTERCHANGEABLE. id is the STAFF PROFILE — what jobs.assign, scheduling.time_off and booking.availability all take. user_id is the LOGIN, and it is what leads.assign takes. A person can have one and not the other: bench staff often have no login, and an owner often has no bench profile. Passing the wrong one is refused rather than mis-assigned, but reading the right one saves the round trip.
bookable is narrower than login_state. It says whether the scheduler may put this person on a booking; somebody not bookable can still be assigned work by hand, and somebody with no login at all can still be bookable.
active_jobs is how much unfinished work they are already holding. It is the number behind 'who is free', and it is more honest than the calendar, which shows only what is scheduled.
login_state distinguishes none (no login exists), invited (sent, not accepted), active and suspended. plan_locked is different again and worth saying out loud: their login is blocked because the shop's PLAN no longer covers the seat, which undoes itself on upgrade rather than being anybody's decision.
PAY IS NOT HERE. Rates live on staff.pay_rates, which is its own capability so that reading the roster does not casually hand over what everybody earns.
People who have left are not returned, so somebody missing from this list is no longer on the bench.
This returns the whole roster in one call and does not page, so nothing is truncated.
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 |
|---|---|---|
| bookable_only | boolean | True returns only people the scheduler may book onto work. |
| skill | stringmax 60 chars | Only people carrying this skill key, as seen on another roster row. |
Output
| Field | Type | Description |
|---|---|---|
| items | object[] | — |
| items[].id | string | — |
| items[].user_id | string | null | — |
| items[].name | string | — |
| items[].role_name | string | null | — |
| items[].bookable | boolean | — |
| items[].login_state | string | — |
| items[].plan_locked | boolean | — |
| items[].skills | string[] | — |
| items[].certifications | string[] | — |
| items[].active_jobs | number | — |
| items[].weekly_days_available | number | — |
| total | number | — |
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/staff/list \
-H "Authorization: Bearer $SERVICEVIN_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'const res = await fetch("https://www.servicevin.com/api/v1/staff/list", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SERVICEVIN_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
// 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/staff/list",
headers={"Authorization": f"Bearer {os.environ['SERVICEVIN_API_KEY']}"},
json={},
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": "staff.list",
"arguments": {}
}
}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. |