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.
https://turnshift.app/api/mcpSend 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 optionalemail.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 optionalscheduleId.assign: assign a person to one date or a filtered date range. Inputs:scheduleId,email,dateorstartDateandendDate, and optionalweekdays.unassign: remove an assignment with the same inputs asassign.auto_assign: fill a schedule's open days, spread fairly across its teams. Inputs:scheduleId,startDate,endDate.get_rotation,start_rotation,change_rotationandstop_rotation: read and run a schedule's automatic assignments. People are named by email, in the order they take turns.change_rotationtakesdryRunto 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 optionalrotationwhen 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 untilupdate_scheduleturns 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_memberandremove_team_member: teams and who is on them. People added get a Slack message.list_usersandset_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_settingsandset_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_schedulesandturn_off_share_link: share schedules with anyone who has the link, with or without a password. Optional inputs:scheduleIdsandpassword.list_changelog: TurnShift's release notes, newest first. Optional inputs:sinceandlimit.
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/mcpRead the Claude.ai and ChatGPT MCP documentationRoles 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/meExpected 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.
Fetch all teams in your organization.
curl -H "Authorization: Bearer <your-api-token>" \
https://turnshift.app/api/v1/teamsAdd 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/membersFetch all shifts in your organization.
curl -H "Authorization: Bearer <your-api-token>" \
https://turnshift.app/api/v1/shiftsAssign 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-15Remove 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-15Users Read Write
List users in your organization, see their team memberships, add people by name and email and set their role.
/api/v1/usersList 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" }]
}
]
}/api/v1/usersAdd 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": []
}
}/api/v1/users/:userId/roleSet 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.
/api/v1/meGet organization information
Response:
{
"organization": {
"id": 123,
"name": "Acme Inc",
"slug": "acme-inc",
"language": "en",
"createdAt": "2024-01-01T00:00:00.000Z"
}
}/api/v1/meChange 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.
/api/v1/shiftsList all shifts
/api/v1/shiftsCreate 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
}/api/v1/shifts/:shiftId/duplicateDuplicate 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]
}/api/v1/shifts/activeTurn 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]
}/api/v1/shifts/:shiftIdGet 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.
/api/v1/shifts/:shiftIdUpdate 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.
/api/v1/shifts/:shiftIdDelete 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).
/api/v1/shifts/:shiftId/bookings?year=2024&month=1Get 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": []
}
}/api/v1/shifts/:shiftId/bookings/:dateBook 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"
}/api/v1/shifts/:shiftId/bookings/rangeBook 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.
/api/v1/shifts/:shiftId/bookings/rangeRemove 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"] }/api/v1/shifts/:shiftId/bookings/auto-assignFill 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" }
}
]
}/api/v1/shifts/:shiftId/bookings/:dateRemove 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.
/api/v1/shifts/:shiftId/rotationGet the shift's rotation: the order, days each person did, the coming turns and recent changes. rotation is null on manual assignments.
/api/v1/shifts/:shiftId/rotationStart 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"
}/api/v1/shifts/:shiftId/rotationChange 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
}/api/v1/shifts/:shiftId/rotationStop 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
/api/v1/on-shift?at=2024-01-15T09:00:00ZWho 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
/api/v1/reports?from=2024-01-01&to=2024-01-31&scheduleId=1Each 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.
/api/v1/teamsList all teams
/api/v1/teamsCreate a team
Request body:
{ "name": "Support Team" }/api/v1/teams/:teamIdGet team details
/api/v1/teams/:teamIdUpdate a team
/api/v1/teams/:teamIdDelete 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).
/api/v1/teams/:teamId/membersList team members
/api/v1/teams/:teamId/membersAdd a member to a team
Request body:
{ "user": "john@example.com" }/api/v1/teams/:teamId/membersRemove a member from a team
Request body:
{ "user": "john@example.com" }Invoices Read
/api/v1/invoicesYour 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.
/api/v1/changelogList 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"
}| Status | Meaning |
|---|---|
| 400 | Bad request / Validation error |
| 401 | Unauthorized (invalid or missing token) |
| 403 | Forbidden (organization blocked) |
| 404 | Resource not found |
| 405 | Method not allowed |
TurnShift API v1