Ownware
Home›Rostera›API
Rostera · API

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 & pathWhat it does
GET /api/pingVerify a key
GET /api/rota?week=YYYY-MM-DDOne week of the rota. Any date inside the week works — it is snapped to your configured week start. `?status=draft\published` filters.
POST /api/shiftsSchedule a shift
GET /api/employeesThe roster. ?active=0 includes archived staff.
GET /api/hours?from=&to=Per-employee hours for a date range
GET /api/openapi.jsonOpenAPI 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:00 and 0900 all 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_minutes is 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, published count)

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 — name required; role and weekly target (in hours, e.g. 37.5) optional.
  • Shifts — employee, date, start, end required; label and status optional. 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/rota returns 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.

ToolWrites?What it does
list_shiftsnoShifts in a week or date range, optionally for one person.
shift_detailnoOne shift, with its derived duration and midnight flag.
assign_shiftYESCreates a shift or assigns an existing one.
open_shiftsnoShifts nobody is assigned to yet.
roster_periodnoHours 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 addresshttps://your-install/mcp
The keyheader Authorization: Bearer apk_xxxx
The transportMCP 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.

← Back to Rostera · Manual · Quickstart · Test run

Affiliate program
Recommend tools people own — earn 35% on every sale. 90-day tracking, instant delivery, payouts by Lemon Squeezy.
Become an affiliate →