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.
Every write checks the key holder's role, exactly as the screen does: a viewer's key cannot add a shift (403 forbidden). A staff login cannot use the API: a key held by one is refused (403 forbidden), because the API reads and writes the whole team's records. Staff use My rota.
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. - Approved time off is refused with
409too (3.2): a person on approved time off cannot be put on a shift that day, with a detail line naming their dates.
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)shift.swapped— a manager approves a swap and an existing shift changes hands. The body names who gave the shift to whom:swap_id,shift(as it stands after the swap),from_employeeandto_employee(eachidandname) anddecided_by. A swap schedules nothing new, so it never sendsshift.created; before 3.2.0 it did. An endpoint that lists its events receives this one only once you tick it in API & Webhooks.shift.claimed— a manager approves a person's request to take an OPEN shift (3.2, from My rota). The body carriesclaim_id,shift(now assigned),employee(idandname) anddecided_by.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"
}
**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"*. Pasted as-is, a block written for another client 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.
Connect it to Zapier, Make or n8n
Rostera talks to other tools without a plugin: it sends a webhook (an HTTPS message) the moment something happens, and it accepts calls to the API above. Zapier, Make and n8n can use both.
Before you start
- In Rostera, open API & Webhooks. Mint an API key (choose Read only if the other tool only reads), and add a webhook when you have its URL (below).
- Use your install's full address, for example
https://rostera.yourcompany.com.
When something happens in Rostera: send it to another tool
- In the other tool, create the trigger that gives you a URL to receive webhooks: - Zapier: the "Webhooks by Zapier" app, trigger Catch Hook (Webhooks by Zapier needs a paid Zapier plan). - Make: module Webhooks → Custom webhook. - n8n: the Webhook node.
- Copy that URL into Rostera under API & Webhooks, and choose the event or events below.
- Make the event happen once in Rostera, so the other tool receives a sample and you can map its fields.
| Event | Fires when |
|---|---|
shift.created | A shift is scheduled, from any door (form, API, AI agent, Copy last week, a duplicate on the rota, or a CSV import) |
shift.swapped | A manager approves a swap and a shift changes hands; the payload names who gave it to whom |
shift.claimed | A manager approves a person's request to take an open shift |
rota.published | A week's draft shifts go live; the payload carries the week and the count |
Every delivery carries X-Rostera-Signature, an HMAC-SHA256 signature over the raw body. Zapier and Make accept the delivery without checking it, so keep the webhook URL private. In n8n you can check it in a Code node before using the data.
When something happens elsewhere: act in Rostera
- Zapier: "Webhooks by Zapier", action POST (or Custom Request).
- Make: module HTTP → Make a request.
- n8n: the HTTP Request node.
Send the request to your install's address plus the path from the Endpoints table above, with the header Authorization: Bearer <your API key> and a JSON body. A read-only key cannot write.
The endpoints that change something:
POST /api/shifts: Schedule a shift
Example recipes
- shift.created → a row in Google Sheets: webhook trigger on
shift.created, then the Google Sheets "Create row" action with the fields you want. - shift.swapped → a message in Slack or Teams: webhook trigger on
shift.swapped, then the Slack or Microsoft Teams "Send message" action. - A new form entry or row elsewhere → Rostera: the other tool's trigger (for example a new Google Sheets row or a form submission), then an HTTP action calling
POST /api/shifts(Schedule a shift).
These recipes use only the events and endpoints documented above. We have not run them inside Zapier, Make or n8n ourselves; Rostera's own automated tests check its API.