Skip to main content
Documentation

Staff

staff.pay_periods

Read the shop's recent pay periods and what each staff run came to
Read-onlySensitiveExpensive budget

REST

Shipping
POST /api/v1/staff/pay_periods

MCP tool

Live
staff.pay_periods

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

Operating contract

The shop's payroll periods, newest first, with what each one totals and how it splits per person.

status is open, closed or paid. Only an OPEN period can still take lines; a closed one is frozen, and it is what a payroll run was built from.

is_earliest_open marks the oldest period still open. It is the one every late entry falls into and the only one a close may sweep — periods close in order, so a stuck earliest-open period blocks every later close, and that is usually the reason a shop's payroll board looks wrong.

late_total is money that landed in a period AFTER the work it relates to. It is not an error, but it explains why a period's total does not match the work done inside its dates.

cadence is how often the shop pays, and it is what turns dates into periods. A shop that changed cadence keeps its stored old periods rather than having history re-cut under it.

READ ONLY. Closing a period freezes what everybody is paid, and reopening one is restricted to the newest closed period even for a person.

TRUNCATION: truncated: true means the ledger scan hit its cap and the totals on this response are INCOMPLETE. Do not quote a payroll figure from a truncated read.

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
expensiveCounted against the expensive budget — a model call, a document render or a fan-out scan.

Partial answers

truncated is how many matching records are not in the response, and 0 is a real answer meaning you have all of them. There is no cursor here — narrow the filter instead.

Do not summarise from a truncated response

`truncated` is a flag rather than a count: when it is true the pay ledger scan hit its cap and every total here is INCOMPLETE. Say so rather than quoting a payroll figure.

Input

This capability takes no arguments.

Output

FieldTypeDescription
cadencestring

currencystring

truncatedboolean

periodsobject[]

periods[].idstring | null

periods[].labelstring

periods[].statusstring

periods[].is_earliest_openboolean

periods[].is_currentboolean

periods[].totalstring

periods[].line_countnumber

periods[].late_totalstring

periods[].late_countnumber

periods[].workersobject[]

periods[].workers[].staff_idstring

periods[].workers[].namestring

periods[].workers[].totalstring

periods[].workers[].line_countnumber

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/pay_periods \
  -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/pay_periods", {
  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/pay_periods",
    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.pay_periods",
    "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 expensive calls. Back off and retry.
500internal_errorSomething failed on our side. Nothing was changed.