Rostera API, as shipped in the download
Rostera REST API & Webhooks (v2.0)
Rostera ships a JSON REST API and signed webhooks so the rota can talk to whatever else you run — a time clock, a payroll export, a Slack channel, a door system, or your own scripts.
Authentication
Create a key in API & Webhooks (in the sidebar). A key belongs to a user and can do what that user can do in the app. Send it on every call:
Authorization: Bearer apk_...
The key is shown once at creation and stored as a SHA-256 hash. Revoke any time; a revoked key stops working immediately.
Endpoints
| Method & path | What it does | |
|---|---|---|
GET /api/ping | Verify a key | |
GET /api/rota?week=YYYY-MM-DD | One week of the rota. Any date inside the week works — it is snapped to your configured week start. `?status=draft\ | published` filters. |
POST /api/shifts | Schedule a shift | |
GET /api/employees | The roster. ?active=0 includes archived staff. | |
GET /api/hours?from=&to= | Per-employee hours for a date range | |
GET /api/openapi.json | OpenAPI 3 spec (no auth) |
Schedule a shift
curl -X POST https://your-install/api/shifts \
-H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
-d '{"employee_id": 3, "date": "2026-08-10", "start": "09:00", "end": "17:00",
"label": "Front of house", "status": "draft"}'
Omit employee_id (or send 0) to create an open / unassigned shift.
The API runs the same guards as the rota screen. This is the part worth knowing:
- Times go through the app's own parser, so
9:00,09:00and0900all work, and a zero-length shift is refused with the same sentence the form uses. - A shift whose end time is at or before its start is understood as crossing midnight, not as an error. The response says so explicitly in
crosses_midnight, so a consumer comparing"22:00"to"06:00"cannot read the shift as negative. duration_minutesis the real, DST-aware length, computed in your business timezone. A shift across a clock change is 7 or 9 hours, not always 8.- Double-booking is refused with
409, using the identical overlap check the rota screen runs — including the day either side, so a shift that runs past midnight still collides. The detail line names the conflicting shift's id.
There is no second, weaker validation path: the API and the form call the same function.
Hours
GET /api/hours returns exactly what the Hours report page shows, from the same aggregation code — per employee, with minutes, published_minutes and the weekly target. Open/unassigned shifts come back as a row with employee_id: null. Payroll pulled through the API therefore cannot disagree with the figure your manager is looking at on screen.
Webhooks
Add receiver URLs in API & Webhooks. Events:
shift.created— any shift is scheduled: from the form, the API, the AI agent, Copy last week, an Alt-drag duplicate on the rota, or a CSV import (one event per shift)rota.published— a week's drafts go live (week_start,week_end,publishedcount)
Each delivery is an HTTP POST with a JSON body and:
X-Rostera-Event: shift.created
X-Rostera-Signature: sha256=<hex HMAC-SHA256 of the raw body, keyed with the endpoint's secret>
Verify the signature before trusting a payload. Delivery is one attempt with a short timeout — deliberately simple; the Recent deliveries log on the settings page shows every attempt and its response code. Design receivers to be idempotent.
CSV import
Two entities, both under Import:
- Employees —
namerequired;roleandweekly target(in hours, e.g.37.5) optional. - Shifts —
employee,date,start,endrequired;labelandstatusoptional. The employee must already exist and be active, so import staff first.
Both run a mandatory dry-run whose report is produced by the same code that performs the import — the preview count is the count you get. Shift rows go through the double-booking guard too, so an import cannot introduce an overlap the app itself would refuse. A row that cannot be applied in full writes nothing at all.
Limits, honestly
- The API acts at user level; there is no separate scope system beyond the app's own roles.
- One delivery attempt per webhook event (log + idempotent receivers, not a retry queue).
GET /api/rotareturns one week per call by design — it is a rota, not a data warehouse.- Shifts can be created through the API but not edited or deleted; those stay in the UI, where the reassignment and clash-resolution flows live.
MCP — the agent endpoint (new in 3.0)
Rostera speaks MCP (Model Context Protocol) at POST /mcp, authenticated with the same revocable API keys the REST API uses.
| Tool | Writes? | What it does |
|---|---|---|
list_shifts | no | Shifts in a week or date range, optionally for one person. |
shift_detail | no | One shift, with its derived duration and midnight flag. |
assign_shift | YES | Creates a shift or assigns an existing one. |
open_shifts | no | Shifts nobody is assigned to yet. |
roster_period | no | Hours per employee — the Hours report's own aggregation. |
Connect it to an assistant
Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.
Every client needs the same three facts, and nothing in the handshake is vendor-specific:
| The address | https://your-install/mcp |
| The key | header Authorization: Bearer apk_xxxx |
| The transport | MCP over streamable HTTP, stateless |
Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:
claude mcp add --transport http rostera https://your-install/mcp \
--header "Authorization: Bearer apk_xxxx"
ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):
{
"type": "mcp",
"server_label": "rostera",
"server_url": "https://your-install/mcp",
"authorization": "apk_xxxx",
"require_approval": "never"
}
Own Your AI reads a list of servers in this shape:
{
"mcpServers": [
{ "id": "rostera", "name": "Rostera", "url": "https://your-install/mcp",
"token": "apk_xxxx", "enabled": true }
]
}
**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"* — not an mcpServers array. Pasted as-is, the block above will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.
A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:
curl -X POST https://your-install/mcp \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Rostera implements the protocol, not an integration with a particular vendor.
The double-booking guard applies to agents too
assign_shift runs the same validator and the same overlap check as the rota screen — including the day either side, so a shift that runs past midnight still collides. A conflict is refused with the screen's own sentence and the id of the shift it clashes with, and **nothing is written**. There is exactly one call to that guard in the product; every door goes through it.
Shift swaps (new in 3.0)
A worker offers a shift, a colleague accepts it, and a manager decides once. The approval is the interesting part: it **re-runs the double-booking check for the accepting worker at the moment of approval**, because their rota may have changed since they accepted. If it now clashes, the swap is refused and nothing moves. A swap cannot be used to smuggle a conflict past the rule the product exists to enforce.
iCal feeds
Per-worker and full-roster, token-guarded and read-only. A worker's feed shows their shifts — a personal token never serves the whole company's roster.