Skip to main content
Documentation

Staff

staff.role_matrix

List each staff role and exactly what it can open and change
Read-onlyRead budget

REST

Shipping
POST /api/v1/staff/role_matrix

MCP tool

Live
staff.role_matrix

Exposed on: REST API · Shop MCP server · Claude connector · Dash (in-app copilot) · Zapier. Part of the Staff domain.

Operating contract

EVERY ROLE IN THE SHOP WITH ITS ACCESS RESOLVED — not the permission keys it was granted, but what those keys actually open, and at what level.

READ THIS BEFORE ANSWERING 'WHY CAN'T SO-AND-SO SEE X'. A grant name is not an answer; writable and read_only are. A role holding a section READ-ONLY opens every page in it and cannot submit a single form, and that is by far the most common cause of 'the button does nothing'.

THE TWO LEVELS ARE THE WHOLE POINT. writable is where the role can ACT. read_only is where it can look. Reporting them together as 'has access' is the mistake this read exists to prevent.

is_manager: true MEANS FULL ADMIN — every section, every admin-only area, no exceptions. A shop asking to 'give someone the quotes section' who already has this is asking for nothing.

read_only_login: true is the honest headline for an auditor, an insurer or an outside bookkeeper: this login changes nothing. A role with NO grants at all is not read-only — nothing is not read-only — and it reads false here.

member_count is how many people hold the role, unaccepted invites included.

data names the sensitive data classes the role can see. That is the field to check before handing anybody a login that will sit in front of customer contact details or pay rates.

CHANGING A ROLE IS NOT A CAPABILITY. Widening one grants access to every person holding it, retroactively and silently, and it is the single edit that turns a read-only login in this product into a full one.

Every role is returned, 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

This capability takes no arguments.

Output

FieldTypeDescription
itemsobject[]

items[].idstring

items[].keystring

items[].namestring

items[].built_inboolean

items[].member_countnumber

items[].is_managerboolean

items[].read_only_loginboolean

items[].permission_keysstring[]

items[].writablestring[]

items[].read_onlystring[]

items[].data_classesstring[]

totalnumber

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/staff/role_matrix \
  -H "Authorization: Bearer $SERVICEVIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

TypeScript (fetch)
const res = await fetch("https://www.servicevin.com/api/v1/staff/role_matrix", {
  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;

Python (requests)
import os, requests

res = requests.post(
    "https://www.servicevin.com/api/v1/staff/role_matrix",
    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"]

MCP tools/call — https://www.servicevin.com/api/mcp
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "staff.role_matrix",
    "arguments": {}
  }
}

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.
403forbiddenThis login does not hold shop.manage.
422validation_errorAn argument was wrong. The message names the field.
429rate_limitedToo many read calls. Back off and retry.
500internal_errorSomething failed on our side. Nothing was changed.