Plan My Shifts API

Content

Introduction

Authentication

Request-Response with Callbacks

Request-Response with Polling

Understanding the Request

Consumer Management

Developer Guide Plan My Shifts API

Do you want to use AI to solve your schedules?

This document walks you through how to use our AI to solve your scheduling problems.

If you have any questions just reach out to us at [email protected].

Did you see our Swagger Documentation? You can check it out at https://planmyshifts.com/swagger/partner/v1. On one hand, we have a simple API with only two endpoints. On the other the hand, the payload is complex so we strongly recommend that you scaffold your API client on the OpenAPI file in the Swagger documentation.

A simple schedule with 2 shifts and 2 employees has 9 possible solutions so any real-life schedule becomes too complex to brute-force.

With our API you can simply POST your problem to us and we will return the optimal schedule given your rules. We will elaborate below on the details but in the big picture: Our data structure is highly abstracted so it can fit any scheduling problem.

You might think that consuming a scheduling API is a GDPR risk but as you will see this API works on IDs.

According to GDPR, identifiers are only personally identifiable to a third party like us if we can reasonably identify an individual based on the data so make sure to use random identifiers that you map to internal identifiers on your side.

Authentication

We use Bearer authentication (also called token authentication) so get your token by doing:

curl -H "Accept: application/json" \
     -H "Content-type: application/json" \
     -X POST --data-binary '{"email":"[email protected]","password":"YOUR_PASSWORD"}' \
     https://planmyshifts.com/api/auth

This way we issue API tokens without the complication of OAuth. This feature is inspired by GitHub and other applications which issue "personal access tokens". Put this YOUR_TOKEN in the header of your request:

curl -H "Accept: application/json" \
     -H "Content-type: application/json" \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -X POST  --data-binary '{JSON_PAYLOAD}' \
     https://planmyshifts.com/api/v1

Request-Response with Callbacks

Solving a schedule is computational intensive and takes up to 10 minutes to solve so we use callbacks to POST the answer to you.

You specify the endpoint and the credentials we should use to authenticate in the payload. We require you to use authentication variables for our callback to you for your safety.

{
    // The rest of the payload goes here
    ...
    "callbackUrl": "https://your_application.com/CALLBACK_ENDPOINT",
    "callbackAuthenticationHeaderName": "YOUR_AUTH_HEADER_NAME",
    "callbackAuthenticationHeaderValue": "YOUR_OWN_TOKEN",
}

We will retry 3 times with 500 miliseconds between attempts before stopping. On the last failure, we will email you with the last error that your server returned.

Request-Response with Polling

In case you prefer polling to callbacks, you can use this flow to fetch the response:

  1. POST your problem and store the id, called solver_problem_id you get as a response.
  2. GET /api/v1 to see the last 1000 requests
  3. Find the one with where solver_problem_id matches the one from bullet 1 and retrieve the id.
  4. When the schedule is ready, you can use the id to retrieve the schedule like so GET /api/v1/{id}:

Understanding the Request

In this paragraph we will put further words onto the request payload.

For an elaboration you can always email us at [email protected] or look at the Swagger Document above.

Let us start with a valid payload along with some inline comments:

{
  "employees": [ // The list of users who takes shifts for the duraton of the schedule
    {
      "id": 1, // This has to be a unique integer for every user
      "teams": [
        {
          "id": 1, // This has to be a unique integer for every team should be found the teams in the "shifts" part of the payload.
          "start": "2020-06-28T09:00:00+00:00", // In case the employee is only assigned to this team for a limited period, otherwise null
          "end": "2025-06-28T09:00:00+00:00" // In case the employee is only assigned to this team for a limited period, otherwise null
        }
      ],
      "skills": [
        {
          "id": 1,  // This has to be a unique integer for every skill should be found the skill in the "shifts" part of the payload.
          "start": "2020-06-28T09:00:00+00:00", // In case the employee has this skill for a limited period, otherwise null
          "end": "2025-06-28T09:00:00+00:00" // In case the employee has this skill for a limited period, otherwise null
        }
      ],
      "contract": {
        "hoursBetweenShift": 11, // If not relevant set to a LOW number such as 0.
        "maxHoursDay": 12, // If not relevant set to a HIGH number such as 999.
        "maxHoursWeek": 50, // If not relevant set to a HIGH number such as 999.
        "maxHoursMonth": 190, // If not relevant set to a HIGH number such as 999.
        "maxWorkDaysMonth": 20, // If not relevant set to a HIGH number such as 999.
        "maxConsecutiveDays": 4, // If not relevant set to a HIGH number such as 999.
        "minConsecutiveDays": 2, // If not relevant set to a LOW number such as 0.
        "weekendWorkFrequency": 4, // How often do you work weekends. If not relevant set to a LOW number, ie. 1
        "maxNhours": 144, // This one goes together with InKWeeks, so in this configuration the employee will never work more than 144 hours over a 4 week period.
        "InKWeeks": 4, // In most labour-unionized contracts you deduct parts of the maxNhours when there are vacations or national holidays.
        "maxConsecutiveLateShifts": 3, // If not relevant set to a HIGH number such as 999.
        "maxConsecutiveNightShifts": 4 // If not relevant set to a HIGH number such as 999.
      },
      "wishes": [ // The times periods where the employee cannot (or wants to) work
        {
          "type": "UNAVAILABLE", // UNAVAILABLE = cannot work, UNDESIRED =  would rather not work and DESIRED = wants to work.
          "start": "2020-06-28T09:00:00+00:00",
          "end": "2025-06-28T09:00:00+00:00"
        }
      ]
    }
  ],
  "shifts": [
    {
      "id": 1, // This has to be a unique integer for every shift.
      "spot": 1, // This has to be in the "spots" part of the payload.
      "start": "2020-06-28T09:00:00+00:00", // The time the shift starts.
      "end": "2020-06-28T16:00:00+00:00", // The time the shift end.
      "importance": "MANDATORY", // the importance of assigning this shift. Can be MANDATORY and OPTIONAL.
      "suggestedEmployees": [ // If you prefer certain employees to take the shift, suggest it to the AI here.
        1
      ],
       "allowedCollisionSpots": [ // If you want to circumvent users.*.contract.hoursBetweenShift for a group of shifts
          1
       ],
      "fixed": true, // If fixed, you must assign an employee and we wont change that employee.
      "historic": false // If historic, we will use this shift to seed the solver and won't change it. Use this flag if past shifts are important for contractual terms such as consecutive days of work. 
      "employee": 1 // If fixed is false, set this to null and we will assign an employee.
      "foreignInt": 5 // To be able to assist you in the best way of potential issues/questions, this can help you and us the best way to track down the issues/questions. This field is OPTIONAL, if filled, make it a unique integer.
    }
  ],
  "spots": [
    {
      "id": 1, // This has to be a unique integer for every spot.
      "teams": [ // This has to be a unique integer for every team should be found the teams in the "employees.*.teams" part of the payload.
        1
      ],
      "skills": [ // This has to be a unique integer for every skill should be found the skills in the "employees.*.skills" part of the payload.
        1
      ],
      "foreignInt": 10 // To be able to assist you in the best way of potential issues/questions, this can help you and us the best way to track down the issues/questions. This field is OPTIONAL, if filled, make it a unique integer.
    }
  ],
  "settings": {}, // Unless you have worked with this API before just leave "settings" as an empty object, i.e {} and we will add the correct values. Otherwise, see the possible keys and values in the Swagger-documentation.
  "callbackUrl": "https://www.yourdomain.app/custom_callback_url",
  "callbackAuthenticationHeaderName": "X-Api-Key",
  "callbackAuthenticationHeaderValue": "SECRET"
}

In the part below we review the enums of this payload. We have done our best to keep them to a minimum but consider that any schedule is a balance of priorities: From the super-important contractual priorities all the way to the "nice-to" priorities such as avoiding certain shifts for employee X.

Shift Importance

You can flag your shifts with either "importance": "MANDATORY" or "importance": "OPTIONAL" depending on how important the shift is to you.

If you are under-staffed you can prioritise which shifts gets staffed before others.

Wish Type

You can flag your employees' wishes with either "type": "UNAVAILABLE", "type": "UNDESIRED" or "type": "DESIRED".

Use the first if you have promised an employee vacations, paternity, etc. Use the second if the employee would rather not work but can be asked to. Use the third if the employee would prefer working on a given day.

Demands and Shifts

You might be used to work with demands instead of shifts, i.e. you need one spot covered from eight in the morning until ten in the evening even if it takes several personnel to do so.

Creating a shift starting 08:00:00 and ending 22:00:00 will not work because we are only assigning a single employee to a given shift.

The way of doing it would be to create several shifts spanning the day:

This will conflict with users.*.contract.hoursBetweenShift and you do not want to set this field to 0 because of worktime and rest regulation.

You can disable hoursBetweenShift for a certain group of shifts like this (we omitted parts of the payload):

{
   "spots": [
      {
         "id": 1
      }
   ],
   "shifts": [
      {
         "start": "2023-01-01T08:00:00+00:00",
         "end": "2023-01-01T09:00:00+00:00",
         "allowedCollisionSpots": [1]
      },
      {
         "start": "2023-01-01T09:00:00+00:00",
         "end": "2023-01-01T10:00:00+00:00",
         "allowedCollisionSpots": [1]
      }
   ],
   "numberOfAllowedShiftCollisions": 12,
   "settings": {
      "employeeHourlyPatternCompactness": {
         "firstPriorityWeight": 0,
         "secondPriorityWeight": 0,
         "thirdPriorityWeight": 5,
         "fourthPriorityWeight": 0
      }
   }
}

Please note that allowedCollisionSpots also affects overlapping shifts. In the example above, if you have a demand for two employees from 08:00 to 22:00, there must be two different spots with their own IDs, otherwise we would assign all shifts from 08:00 to 22:00 to the same employee even if there are 3 identical shifts from 08:00 to 09:00.

If you want the hours to be compact so that a single employee takes as many hours in a row, add the settings.employeeHourlyPatternCompactness like in the example above.

If you have a maximal number of spots you want to assign in this way of "grouping shifts together" add the numberOfAllowedShiftCollisions to the payload.

The Settings Object

The settings object is the most complicated object, which is why it is optional. We use a default configuration that works for "most" workplaces. If you really need to tweak the settings, we strongly recommend that you do it together with us.

Please also have a look our setting generator before filling out the settings object.

Every setting has four degrees, so you can prioritise them amongst each other. Most workplaces will never break a contractual obligation at the expense of a staffing shift. But some workplaces actually do the opposite: They would rather break the contractual obligation to make sure shifts are staffed.

If, for example, you are such a workplace then configure the settings object like so:

{
    ...
    "settings": {
        "assignShift": { "firstPriorityWeight": 3, "secondPriorityWeight": 0, "thirdPriorityWeight": 0, "fourthPriorityWeight": 0 },
        "maxNHoursInKWeeks": { "firstPriorityWeight": 0, "secondPriorityWeight": 2, "thirdPriorityWeight": 0, "fourthPriorityWeight": 0 },
    }
}

With these settings, we will always assign shifts at the expense of respecting the maxNHoursInKWeeks in the employees' contracts. You may note that we have set the numeric values to 3 and 2, respectively. Our API will return a HTTP 422 and a validation error message if the numbers are not integers between 1 and 5.

Consumer Management

In case you are operating something like a multi-tenant app, you might prefer that each tenant has its own credentials to authenticate against our endpoints. We call them consumers.

With your credentials, you can create and delete consumers as described in our Swagger documentation. Here is an example of how to create a consumer.

curl -H "Accept: application/json" \
     -H "Content-type: application/json" \
     -H "Authorization: Bearer YOUR_TOKEN" \
     -X POST  --data-binary '{ "email": "[email protected]", "password": "AT_LEAST_10_CHARS_LONG" }' \
     https://planmyshifts.com/api/v1/consumers

Note that this consumer cannot create other consumers. Only your original consumer can do that.

On top of that, it is also only possible for you original consumer to DELETE a consumer, if that given consumer no longer is of use to you.

Deleting is done by the DELETE endpoint as described in our Swagger documentation.

Our API will return a 401 (Unauthorized) or 404 (Not Found) if unauthorized attempts to either of the endpoints are made. Likewise, our API will return 201 (Created) for a successful POST and 204 (No Content) for a successful DELETE. Lastly, our API will return 422 (Unprocessable Entity) if any does not pass validation.