TurnShift shift scheduling API

Read and change your schedules, assignments, rotations and teams from your own code with the REST API, or from Claude, ChatGPT and other AI assistants with the MCP server.

Overview

The TurnShift API lets you integrate shift scheduling into your existing workflows. Build custom integrations, automate repetitive tasks, or sync TurnShift with your internal tools.

What you can do with the API:

  • Manage shifts — Create, update, and delete shifts programmatically
  • Automate bookings — Assign team members to shifts based on your own logic
  • Sync teams — Keep your TurnShift teams in sync with your HR system
  • Build dashboards — Pull schedule data into your own reporting tools
  • Integrate with Slack/Discord — Create custom bots that interact with your schedules

All endpoints return JSON and use standard HTTP methods.Read = read-onlyWrite = modifies data

Authentication

All API requests require a Bearer token in the Authorization header:

Authorization: Bearer <your-api-token>

MCP server

Add the TurnShift MCP server to Claude, ChatGPT, Cursor or any MCP client with one URL. The client signs you in with OAuth, then acts as you: a member can only assign themselves on schedules that are not open. Clients without OAuth can send your personal token from Settings, API, or the organization token, which has admin rights.

POSThttps://turnshift.app/api/mcp

Send JSON-RPC requests to this Streamable HTTP endpoint.

Without OAuth, send a token with every request:

Authorization: Bearer <your-api-token>

Tools

  • list_schedules: list schedules and their assigned teams. No inputs.
  • get_schedule: get one schedule. Input: scheduleId.
  • list_team_members: list a team's members. Input: teamId.
  • list_schedule_members: list the members of a schedule's teams. Input: scheduleId.
  • get_bookings: list assignments in a date range. Inputs: scheduleId, startDate, endDate, and optional email.
  • who_is_on_shift: show people on shift at an instant. Optional input: at.
  • get_report: each person's shifts, days and hours over a period, split into weekday, weekend and night hours. Inputs: startDate, endDate, and optional scheduleId.
  • assign: assign a person to one date or a filtered date range. Inputs: scheduleId, email, date or startDate and endDate, and optional weekdays.
  • unassign: remove an assignment with the same inputs as assign.
  • auto_assign: fill a schedule's open days, spread fairly across its teams. Inputs: scheduleId, startDate, endDate.
  • get_rotation, start_rotation, change_rotation and stop_rotation: read and run a schedule's automatic assignments. People are named by email, in the order they take turns. change_rotation takes dryRun to preview whose turns move.
  • create_schedule: create a schedule ready to use, like New schedule in the app: its hours, its Slack channel by name (slackChannel, like #support), the people by email (a new team named after it), existing teams, and an optional rotation when people take turns. Managers and admins only.
  • duplicate_schedule: copy a schedule to start a similar one, without its assignments or rotation. The copy is off until update_schedule turns it on. Managers and admins only.
  • set_schedules_active: turn several schedules on or off at once, like selecting them on the Schedules list. All or nothing.
  • update_schedule: change a schedule's name, coverage (hours, time zone, people needed each weekday), whether it is on, its Slack channel by name, its Microsoft Teams channel, and its Slack messages, the managers' open seats warning included.
  • list_microsoft_channels: the Microsoft Teams channels a schedule can post to.
  • list_teams, add_team_member and remove_team_member: teams and who is on them. People added get a Slack message.
  • list_users and set_role: everyone with their role, and change a role (admins only).
  • add_person: add someone who is not in your Slack, by name and email (managers and admins, for the organizations trying it first).
  • ask_admin: ask your organization's admins for a new role. Each admin gets one Slack message, or an email when they muted TurnShift's messages. Once a day, and only when signed in as yourself: an API token has no person behind it.
  • mute_direct_messages: mute or unmute TurnShift's Slack messages to you.
  • get_notification_settings and set_notification_setting: where you get each kind of notification (changes to your shifts, reminders, open seats), on Slack, Teams or email. Only when signed in as yourself.
  • set_slack_language: the language of TurnShift's Slack messages for the organization.
  • list_invoices: your TurnShift invoices with links to view and download them.
  • list_share_links, share_schedules and turn_off_share_link: share schedules with anyone who has the link, with or without a password. Optional inputs: scheduleIds and password.
  • list_changelog: TurnShift's release notes, newest first. Optional inputs: since and limit.

Managers and admins run auto-assign, rotations, teams and invoices, like in the app.

Example request

POST https://turnshift.app/api/mcp
Authorization: Bearer <your-api-token>
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "list_schedules",
    "arguments": {}
  }
}

Connect a client

In Claude, open Customize > Connectors and add a custom connector with this URL. In ChatGPT, open Plugins > + > Add custom MCP server (changes need a Business, Enterprise or Edu workspace). Then sign in to TurnShift when asked.

https://turnshift.app/api/mcp
Read the Claude.ai and ChatGPT MCP documentation

Roles and access

The token belongs to your organization, not to one person. Every request made with it has the same access as an admin: it can read and change every schedule, team and booking, including schedules that are not open. Treat it like an admin password and keep it in a secret store.

Only admins and managers can see the token in your organization settings. The member role limits what someone can do when they sign in to the dashboard, it does not limit the API token.

Admin can do everything a manager can, plus change roles.

Manager can create and edit schedules, assign anyone, and manage teams, Slack, billing and this token.

Member can book their own shifts. On an open schedule they can do everything except close it.

Quick Start

Test your API token by fetching your organization info.

curl -H "Authorization: Bearer <your-api-token>" \
  https://turnshift.app/api/v1/me

Expected response:

{
  "organization": {
    "id": 123,
    "name": "Your Organization",
    "slug": "your-org",
    "createdAt": "2024-01-01T00:00:00.000Z"
  }
}

Common Examples

Here are curl examples for the most common API operations.

GETList all teamsRead

Fetch all teams in your organization.

curl -H "Authorization: Bearer <your-api-token>" \
  https://turnshift.app/api/v1/teams
POSTAdd a user to a teamWrite

Add a team member by email. Replace :teamId with the team ID.

curl -X POST \
  -H "Authorization: Bearer <your-api-token>" \
  -H "Content-Type: application/json" \
  -d '{"user": "john@example.com"}' \
  https://turnshift.app/api/v1/teams/1/members
GETList all shiftsRead

Fetch all shifts in your organization.

curl -H "Authorization: Bearer <your-api-token>" \
  https://turnshift.app/api/v1/shifts
POSTBook a user for a shiftWrite

Assign a user to a shift on a specific date. Add ?notify=true to send a Slack notification.

curl -X POST \
  -H "Authorization: Bearer <your-api-token>" \
  -H "Content-Type: application/json" \
  -d '{"user": "john@example.com"}' \
  https://turnshift.app/api/v1/shifts/1/bookings/2024-01-15
DELETERemove a bookingWrite

Remove a user from a shift on a specific date. Add ?notify=true to send a Slack notification.

curl -X DELETE \
  -H "Authorization: Bearer <your-api-token>" \
  -H "Content-Type: application/json" \
  -d '{"user": "john@example.com"}' \
  https://turnshift.app/api/v1/shifts/1/bookings/2024-01-15

Users Read Write

List users in your organization, see their team memberships, add people by name and email and set their role.

GET/api/v1/users

List all users in your organization

Response:

{
  "users": [
    {
      "id": 1,
      "email": "john@example.com",
      "name": "John Doe",
      "timezone": "America/New_York",
      "role": "manager",
      "teams": [{ "id": 1, "name": "Support" }]
    }
  ]
}
POST/api/v1/users

Add someone by email

A person TurnShift can put on teams and schedules without a Slack account. TurnShift emails them an invite, and they sign in with a code sent to that email. timezone is optional and defaults to the one most of your people have. An email someone in the organization already has is refused with a 400. For the organizations trying it first, others get a 404.

Request:

{
  "name": "Ana Lima",
  "email": "ana@example.com",
  "timezone": "Europe/Paris"
}

Response (201):

{
  "user": {
    "id": 42,
    "email": "ana@example.com",
    "name": "Ana Lima",
    "timezone": "Europe/Paris",
    "role": "member",
    "teams": []
  }
}
PUT/api/v1/users/:userId/role

Set someone's role

:userId is a TurnShift user ID, a Slack user ID or an email address. Giving a role to a Slack person who never signed in creates their TurnShift user. The last admin cannot be demoted, promote someone else first. When a number is the TurnShift ID of one person and the Slack user ID of another, the request fails with a 409: use their email instead.

Request:

{ "role": "manager" }

Response:

{
  "user": { "id": 1, "role": "manager" }
}

Example:

curl -X PUT \
  -H "Authorization: Bearer $TURNSHIFT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role": "manager"}' \
  "https://turnshift.app/api/v1/users/john@example.com/role"

Organization Read

Retrieve information about your organization, and set the language of its Slack messages.

GET/api/v1/me

Get organization information

Response:

{
  "organization": {
    "id": 123,
    "name": "Acme Inc",
    "slug": "acme-inc",
    "language": "en",
    "createdAt": "2024-01-01T00:00:00.000Z"
  }
}
PATCH/api/v1/me

Change the language of TurnShift's Slack messages and channel topics with { "language": "fr" }: en, fr, de or es. People who picked their own language in TurnShift keep it for their direct messages.

Shifts Read Write

Create, update, and delete shifts. Configure shift times, timezones, and notification settings.

startMessageNote is a string or null. It is optional Slack mrkdwn appended to shift start messages and can be up to 1000 characters.

GET/api/v1/shifts

List all shifts

POST/api/v1/shifts

Create a shift ready to use, with its people and how they get assigned, like New schedule in the app

Request body:

{
  "name": "Morning Support",
  "startTime": "09:00",
  "endTime": "17:00",
  "timeZone": "Europe/Paris",
  "slackChannelName": "#support",  // or "slackChannelId": 123
  "microsoftChannel": "Support crew › General",  // optional, or its id
  "numberOfShiftsToBookPerDay": { "1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 0, "7": 0 },
  "startMessageNote": "Remember to check the runbook.",
  "users": ["john@example.com", "jane@example.com"],  // optional
  "teamIds": [3],                                     // optional
  "rotation": {                                       // optional
    "turnUnit": "week",  // or "day"
    "turnLength": 1,     // 1 to 8
    "startsOn": "2024-01-15"
  }
}

Name its Slack channel with slackChannelName, or send the slackChannelId another shift shows. For a private channel, type /invite @TurnShift in it first, or you get a 404. An organization that started without Slack has no channels: leave both out. microsoftChannel is its Microsoft Teams channel, named Team › Channel.

The shift is on from the start. users (IDs or emails) become a new team named after the shift, and teamIds are existing teams put on it. Leave out rotation for manual assignments. With it, the people in users take turns in that order, as many per turn as the shift needs at a time, or the ones you list in rotation.users, who must be in users or on the teams. Everything is created together, or nothing is. People get a Slack message: their turns, or that they were added.

Response:

{
  "shift": { "id": 1, "name": "Morning Support", "active": true, ... },
  "teamId": 7,      // the new team, null without users
  "rotationId": 4   // null for manual assignments
}
POST/api/v1/shifts/:shiftId/duplicate

Duplicate a shift to start a similar one, like Duplicate in its settings

Request body (optional):

{
  "name": "Evening Support"  // "<name> (copy)" by default
}

The copy has the same days, hours, time zone, people needed, Slack channel, notifications and teams, and the days changed from today on. Bookings and the rotation are not copied. It is off (active: false), so it posts nothing to Slack until you turn it on with PUT /api/v1/shifts/:shiftId. A number is added to a name another shift has.

Response:

{
  "shift": { "id": 2, "name": "Morning Support (copy)", "active": false, ... },
  "teamIds": [3, 7]
}
POST/api/v1/shifts/active

Turn several shifts on or off at once, like selecting them on the Schedules list

Request body:

{
  "shiftIds": [2, 3, 4],
  "active": true  // false turns them off
}

Off keeps their bookings but posts nothing to Slack. All or nothing: an id that is not one of your shifts answers 404 and changes none. Shifts already in that state stay as they are, and changedIds lists the ones this call switched.

Response:

{
  "shifts": [{ "id": 2, "name": "Morning Support", "active": true }, ...],
  "changedIds": [2, 4]
}
GET/api/v1/shifts/:shiftId

Get shift details

Shift object:

{
  "id": 1,
  "name": "Morning Support",
  "startTime": "09:00",
  "endTime": "17:00",
  "timeZone": "Europe/Paris",
  "slackChannelId": 123,
  "startMessageNote": null
}

slackChannelId is null when the shift posts in no Slack channel, for example after its channel was deleted. Update it with slackChannelName to post again.

PUT/api/v1/shifts/:shiftId

Update a shift

Request body:

{
  "slackChannelName": "#support-oncall",  // or "slackChannelId": 124
  "microsoftChannel": "Support crew › On-call",  // or its id, null for none
  "startMessageNote": "Remember to check the runbook.",
  "numberOfShiftsToBookPerDay": { "1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 0, "7": 0 },
  "channelMessagesOnChangeOnly": true,
  "sendOpenSeatsWarning": true,
  "freeForm": false
}

Send only the fields you change. slackChannelName moves it to another Slack channel, found by name. numberOfShiftsToBookPerDay is the people needed each ISO weekday, Monday 1 to Sunday 7, 0 when the shift does not run. The same start and end time means all day. freeForm is an open schedule, where members assign anyone. sendOpenSeatsWarning sends the schedule's managers a Slack message at 10:00 the day before a day that still needs people (an email when they muted TurnShift's messages). microsoftChannel is the Microsoft Teams channel it posts to when a shift starts and ends, named Team › Channel, one of the teams TurnShift was added to.

DELETE/api/v1/shifts/:shiftId

Delete a shift

Bookings Read Write

Assign users to shifts on specific dates and retrieve booking schedules.

Specifying users: You can identify users by their numeric ID (e.g., 123) or email (e.g., john@example.com).

GET/api/v1/shifts/:shiftId/bookings?year=2024&month=1

Get bookings for a month

Response:

{
  "shift": { "id": 1, "name": "Support" },
  "year": 2024,
  "month": 1,
  "bookings": {
    "2024-01-15": [
      { "id": 1, "email": "john@example.com", "name": "John Doe" }
    ],
    "2024-01-16": []
  }
}
POST/api/v1/shifts/:shiftId/bookings/:date

Book a user for a shift on a specific date

Query parameters:

  • notify=true (optional) - Send a Slack notification to the user

Request body:

{
  "user": "john@example.com"  // or user ID: "123"
}
POST/api/v1/shifts/:shiftId/bookings/range

Book a user on selected weekdays in a date range

Query parameters:

  • notify=true (optional) - Send one Slack notification to the user

Request body:

{
  "user": "john@example.com",
  "startDate": "2024-01-15",
  "endDate": "2024-01-26",
  "weekdays": [1, 2, 3, 4, 5]
}

The response includes the assigned dates and skipped dates with their reasons. Ranges can contain up to 93 calendar days.

Example response:

{
  "assigned": ["2024-01-15", "2024-01-16"],
  "skipped": [
    { "date": "2024-01-17", "reason": "already_booked" }
  ]
}

When the person is also on another schedule one of those days, the response adds alsoOnAnotherScheduleThatDay, one entry per day with the other schedule and a url to that day on the calendar. It is a warning, the booking is made.

DELETE/api/v1/shifts/:shiftId/bookings/range

Remove a user's bookings on selected weekdays in a date range, rotation days included

Request body, the same as assigning a range:

{
  "user": "john@example.com",
  "startDate": "2024-01-15",
  "endDate": "2024-01-26",
  "weekdays": [1, 2, 3, 4, 5]
}

Example response:

{ "unassigned": ["2024-01-15", "2024-01-16"] }
POST/api/v1/shifts/:shiftId/bookings/auto-assign

Fill the open days between two dates, spread fairly across the people on the shift's teams

Request body, at most 93 days:

{
  "startDate": "2024-01-15",
  "endDate": "2024-01-26"
}

Example response:

{
  "assigned": [
    {
      "date": "2024-01-15",
      "user": { "id": 1, "email": "john@example.com", "name": "John Doe" }
    }
  ]
}
DELETE/api/v1/shifts/:shiftId/bookings/:date

Remove a booking

Query parameters:

  • notify=true (optional) - Send a Slack notification to the user

Request body:

{
  "user": "john@example.com"
}

Rotation Read Write

A rotation is a shift's automatic assignments: the people listed take turns, a day or a week each, on the days the shift runs, until it stops. A change starts at the next round, so turns people already planned around stay put. Name people by ID or email, in the order they take turns.

GET/api/v1/shifts/:shiftId/rotation

Get the shift's rotation: the order, days each person did, the coming turns and recent changes. rotation is null on manual assignments.

POST/api/v1/shifts/:shiftId/rotation

Start a rotation. Everyone must be on the shift's teams. People get a Slack message with their next turns.

Request body:

{
  "users": ["john@example.com", "jane@example.com"],
  "turnUnit": "week",   // or "day"
  "turnLength": 1,      // 1 to 8
  "peoplePerTurn": 1,   // optional
  "startsOn": "2024-01-15"
}
PUT/api/v1/shifts/:shiftId/rotation

Change who takes turns, their order or the turn length, from the next round. Send dryRun: true to see whose turns move and which turns are left open first.

Request body:

{
  "users": ["jane@example.com", "john@example.com", "ana@example.com"],
  "turnUnit": "week",
  "turnLength": 1,
  "startSooner": false,  // true: people added take the next turn
  "covers": [            // who takes the open turns of people removed
    { "user": "jane@example.com", "dates": ["2024-01-22", "2024-01-23"] }
  ],
  "dryRun": true
}
PATCH/api/v1/shifts/:shiftId/rotation

Stop the rotation with { "stop": true }: the shift goes back to manual assignments and days from today on are open again. { "stop": false } the same day undoes it.

On shift now Read

GET/api/v1/on-shift?at=2024-01-15T09:00:00Z

Who is on every shift running now, or at the instant you pass as at. Handles time zones and overnight shifts.

Response:

{
  "at": "2024-01-15T09:00:00.000Z",
  "schedules": [
    {
      "schedule": { "id": 1, "name": "Support", "startTime": "09:00",
        "endTime": "18:00", "timeZone": "Europe/Paris" },
      "date": "2024-01-15",
      "people": [{ "email": "john@example.com", "name": "John Doe" }]
    }
  ]
}

Reports Read

GET/api/v1/reports?from=2024-01-01&to=2024-01-31&scheduleId=1

Each person's shifts, days and hours between two dates (366 days at most), for every shift or one with scheduleId. Hours are counted in each shift's time zone: weekends are Saturday and Sunday, nights 10 PM to 6 AM. Rotation turns and swaps count for whoever was on, people who left Slack have leftSlack. Add format=csv for a spreadsheet with the same numbers.

Response:

{
  "from": "2024-01-01",
  "to": "2024-01-31",
  "scheduleId": 1,
  "schedules": [{ "id": 1, "name": "Support", "timeZone": "Europe/Paris" }],
  "people": [
    {
      "id": 2, "name": "John Doe", "email": "john@example.com",
      "leftSlack": false, "shifts": 8, "days": 8, "hours": 72,
      "weekdayHours": 54, "weekendHours": 18, "nightHours": 0,
      "schedules": [{ "id": 1, "shifts": 8, "days": 8, "hours": 72,
        "weekdayHours": 54, "weekendHours": 18, "nightHours": 0 }]
    }
  ],
  "totals": { "shifts": 8, "hours": 72, "weekdayHours": 54,
    "weekendHours": 18, "nightHours": 0 }
}

Teams Read Write

Create, update, and delete teams. Teams group users together for shift assignments.

GET/api/v1/teams

List all teams

POST/api/v1/teams

Create a team

Request body:

{ "name": "Support Team" }
GET/api/v1/teams/:teamId

Get team details

PUT/api/v1/teams/:teamId

Update a team

DELETE/api/v1/teams/:teamId

Delete a team

Team Members Read Write

Add and remove users from teams.

Specifying users: You can identify users by their numeric ID (e.g., 123) or email (e.g., john@example.com).

GET/api/v1/teams/:teamId/members

List team members

POST/api/v1/teams/:teamId/members

Add a member to a team

Request body:

{ "user": "john@example.com" }
DELETE/api/v1/teams/:teamId/members

Remove a member from a team

Request body:

{ "user": "john@example.com" }

Invoices Read

GET/api/v1/invoices

Your TurnShift invoices, newest first, the ones listed in Settings, Billing.

Response:

{
  "invoices": [
    {
      "number": "TS-0042",
      "date": "2024-01-01",
      "total": "35.00",
      "currency": "EUR",
      "status": "paid",
      "url": "https://invoice.stripe.com/...",
      "pdf": "https://pay.stripe.com/invoice/.../pdf"
    }
  ]
}

Changelog Read

TurnShift's release notes, newest first, the same as the changelog. No token needed. The MCP server has it too, as list_changelog, and so does the RSS feed.

GET/api/v1/changelog

List release notes. Optional query parameters: since (YYYY-MM-DD, notes published that day or later) and limit (1 to 50, 10 by default).

Response:

{
  "entries": [
    {
      "title": "Roles are here",
      "date": "2026-08-29T22:52:35Z",
      "url": "https://turnshift.app/blog/roles-are-here",
      "summary": "Until now anyone in your organization could edit anything...",
      "markdown": "Until now anyone in your organization could edit anything..."
    }
  ]
}

Error Responses

Errors return a JSON object with error and optional details:

{
  "error": "Unauthorized",
  "details": "Invalid API token"
}
StatusMeaning
400Bad request / Validation error
401Unauthorized (invalid or missing token)
403Forbidden (organization blocked)
404Resource not found
405Method not allowed

TurnShift API v1