Grafana Cloud

Schedules HTTP API

Create a schedule

Required permission: grafana-irm-app.schedules:write

shell
curl "{{API_URL}}/api/v1/schedules/" \
  --request POST \
  --header "Authorization: meowmeowmeow" \
  --header "Content-Type: application/json" \
  --header "X-Grafana-URL: https://your-stack.grafana.net" \
  --data '{
    "name": "Demo schedule iCal",
    "ical_url_primary": "https://example.com/meow_calendar.ics",
    "slack": {
      "channel_id": "MEOW_SLACK_ID",
      "user_group_id": "MEOW_SLACK_ID"
    }
  }'

The above command returns JSON structured in the following way:

JSON
{
  "id": "SBM7DV7BKFUYU",
  "name": "Demo schedule iCal",
  "type": "ical",
  "team_id": null,
  "ical_url_primary": "https://example.com/meow_calendar.ics",
  "ical_url_overrides": "https://example.com/meow_calendar_overrides.ics",
  "on_call_now": ["U4DNY931HHJS5"],
  "slack": {
    "channel_id": "MEOW_SLACK_ID",
    "user_group_id": "MEOW_SLACK_ID"
  }
}
ParameterUniqueRequiredDescription
nameYesYesSchedule name.
typeNoYesSchedule type. May be ical (used for iCalendar integration), calendar (used for manually created on-call shifts) or web (for web UI managed schedules).
team_idNoNoID of the team.
time_zoneNoYesSchedule time zone. It is used for manually added on-call shifts in Schedules with type calendar. Default time zone is UTC. For more information about time zones, see time zones. Not used for schedules with type ical.
ical_url_primaryNoIf type = icalURL of external iCal calendar for schedule with type ical.
ical_url_overridesNoOptionalURL of external iCal calendar for schedule with any type. Events from this calendar override events from primary calendar or from on-call shifts.
enable_web_overridesNoOptionalWhether to enable web overrides or not. Setting specific for API/Terraform based schedules (calendar type).
slackNoOptionalDictionary with Slack-specific settings for a schedule. Includes channel_id and user_group_id fields, that take a channel ID and a user group ID from Slack.
shiftsNoOptionalList of shifts. Used for manually added on-call shifts in Schedules with type calendar.

HTTP request

POST {{API_URL}}/api/v1/schedules/

Get a schedule

Required permission: grafana-irm-app.schedules:read

shell
curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/" \
  --request GET \
  --header "Authorization: meowmeowmeow" \
  --header "Content-Type: application/json" \
  --header "X-Grafana-URL: https://your-stack.grafana.net"

The above command returns JSON structured in the following way:

JSON
{
  "id": "SBM7DV7BKFUYU",
  "name": "Demo schedule iCal",
  "type": "ical",
  "team_id": null,
  "ical_url_primary": "https://example.com/meow_calendar.ics",
  "ical_url_overrides": "https://example.com/meow_calendar_overrides.ics",
  "on_call_now": ["U4DNY931HHJS5"],
  "slack": {
    "channel_id": "MEOW_SLACK_ID",
    "user_group_id": "MEOW_SLACK_ID"
  }
}

HTTP request

GET {{API_URL}}/api/v1/schedules/<SCHEDULE_ID>/

List schedules

Required permission: grafana-irm-app.schedules:read

shell
curl "{{API_URL}}/api/v1/schedules/" \
  --request GET \
  --header "Authorization: meowmeowmeow" \
  --header "Content-Type: application/json" \
  --header "X-Grafana-URL: https://your-stack.grafana.net"

The above command returns JSON structured in the following way:

JSON
{
  "count": 2,
  "next": null,
  "previous": null,
  "results": [
    {
      "id": "SBM7DV7BKFUYU",
      "name": "Demo schedule iCal",
      "type": "ical",
      "team_id": null,
      "ical_url_primary": "https://example.com/meow_calendar.ics",
      "ical_url_overrides": "https://example.com/meow_calendar_overrides.ics",
      "on_call_now": ["U4DNY931HHJS5"],
      "slack": {
        "channel_id": "MEOW_SLACK_ID",
        "user_group_id": "MEOW_SLACK_ID"
      }
    },
    {
      "id": "S3Z477AHDXTMF",
      "name": "Demo schedule Calendar",
      "type": "calendar",
      "team_id": null,
      "time_zone": "America/New_York",
      "on_call_now": ["U4DNY931HHJS5"],
      "shifts": ["OH3V5FYQEYJ6M", "O9WTH7CKM3KZW"],
      "ical_url_overrides": null,
      "slack": {
        "channel_id": "MEOW_SLACK_ID",
        "user_group_id": "MEOW_SLACK_ID"
      }
    }
  ],
  "current_page_number": 1,
  "page_size": 50,
  "total_pages": 1
}

Note: The response is paginated. You may need to make multiple requests to get all records.

The following available filter parameter should be provided as a GET argument:

  • name (Exact match)
  • team_id (Exact match, team ID)

HTTP request

GET {{API_URL}}/api/v1/schedules/

Update a schedule

Required permission: grafana-irm-app.schedules:write

shell
curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/" \
  --request PUT \
  --header "Authorization: meowmeowmeow" \
  --header "Content-Type: application/json" \
  --header "X-Grafana-URL: https://your-stack.grafana.net" \
  --data '{
    "name": "Demo schedule iCal",
    "ical_url": "https://example.com/meow_calendar.ics",
    "slack": {
        "channel_id": "MEOW_SLACK_ID"
    }
  }'

The above command returns JSON structured in the following way:

JSON
{
  "id": "SBM7DV7BKFUYU",
  "name": "Demo schedule iCal",
  "type": "ical",
  "team_id": null,
  "ical_url_primary": "https://example.com/meow_calendar.ics",
  "ical_url_overrides": "https://example.com/meow_calendar_overrides.ics",
  "on_call_now": ["U4DNY931HHJS5"],
  "slack": {
    "channel_id": "MEOW_SLACK_ID",
    "user_group_id": "MEOW_SLACK_ID"
  }
}

HTTP request

PUT {{API_URL}}/api/v1/schedules/<SCHEDULE_ID>/

Delete a schedule

Required permission: grafana-irm-app.schedules:write

shell
curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/" \
  --request DELETE \
  --header "Authorization: meowmeowmeow" \
  --header "Content-Type: application/json" \
  --header "X-Grafana-URL: https://your-stack.grafana.net"

HTTP request

DELETE {{API_URL}}/api/v1/schedules/<SCHEDULE_ID>/

Get current on-call users for a schedule

Required permission: grafana-irm-app.schedules:read

Returns the users currently on-call for a given schedule, including their phone number, phone number status, and shift timing. This endpoint is designed for use with automated call routing systems such as Twilio Studio Flows.

shell
curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/current_oncall/" \
  --request GET \
  --header "Authorization: meowmeowmeow" \
  --header "X-Grafana-URL: https://your-stack.grafana.net"

The above command returns JSON structured in the following way:

JSON
{
  "count": 1,
  "users": [
    {
      "id": "U4DNY931HHJS5",
      "grafana_id": 456,
      "email": "alice@example.com",
      "slack": [
        {
          "user_id": "UALICESLACKID",
          "team_id": "TSLACKTEAMID"
        }
      ],
      "username": "alice",
      "role": "admin",
      "is_phone_number_verified": true,
      "phone_number": "+12345678901",
      "phone_number_status": "available",
      "timezone": "America/New_York",
      "teams": ["TAAM1K1NNEHAG"]
    }
  ],
  "shifts": [
    {
      "schedule_id": "SBM7DV7BKFUYU",
      "schedule_name": "Demo schedule iCal",
      "team_id": null,
      "shift_start": "2026-03-24T09:00:00+00:00",
      "shift_end": "2026-03-25T09:00:00+00:00",
      "users": ["U4DNY931HHJS5"]
    }
  ]
}
ParameterDescription
slackList of Slack user identities (user_id, team_id), or null if not connected.
timezoneThe user’s timezone (e.g. America/New_York), or null.
teamsList of team IDs the user belongs to.
phone_numberThe user’s verified phone number, or null if not configured or private.
phone_number_statusOne of: available (number returned), private (user has hidden their number), not_configured (user has no verified number).
shiftsList of shift entries with schedule reference (schedule_id, schedule_name, team_id) and timing (shift_start, shift_end).

Note

When the organization setting Override phone number privacy for API is enabled, users who have set their phone number to private will still have their number returned with status available. This setting does not affect phone number visibility in the UI.

When no one is currently on-call, count is 0, users is an empty list, and shifts is an empty list.

HTTP request

GET {{API_URL}}/api/v1/schedules/<SCHEDULE_ID>/current_oncall/

Get previous on-call users for a schedule

Required permission: grafana-irm-app.schedules:read

Returns the users who were on-call during the most recently completed shift for a given schedule.

shell
curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/previous_oncall/" \
  --request GET \
  --header "Authorization: meowmeowmeow" \
  --header "X-Grafana-URL: https://your-stack.grafana.net"

The response format is identical to current_oncall. The shifts array contains the most recently ended shift(s).

Query ParameterRequiredDefaultDescription
daysNo7Number of days to look back for past shifts. Maximum value: 30.

HTTP request

GET {{API_URL}}/api/v1/schedules/<SCHEDULE_ID>/previous_oncall/

Get next on-call users for a schedule

Required permission: grafana-irm-app.schedules:read

Returns the users who will be on-call during the next upcoming shift for a given schedule. Shifts that overlap with the current time are skipped.

shell
curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/next_oncall/" \
  --request GET \
  --header "Authorization: meowmeowmeow" \
  --header "X-Grafana-URL: https://your-stack.grafana.net"

The response format is identical to current_oncall. The shifts array contains the next upcoming shift(s).

Query ParameterRequiredDefaultDescription
daysNo7Number of days to look ahead for future shifts. Maximum value: 30.

HTTP request

GET {{API_URL}}/api/v1/schedules/<SCHEDULE_ID>/next_oncall/

Export a schedule’s final shifts

Required permission: grafana-irm-app.schedules:read

HTTP request

shell
curl "{{API_URL}}/api/v1/schedules/SBM7DV7BKFUYU/final_shifts?start_date=2023-01-01&end_date=2023-02-01" \
  --request GET \
  --header "Authorization: meowmeowmeow" \
  --header "X-Grafana-URL: https://your-stack.grafana.net"

The above command returns JSON structured in the following way:

JSON
{
  "count": 12,
  "next": null,
  "previous": null,
  "results": [
    {
      "user_pk": "UC2CHRT5SD34X",
      "user_email": "alice@example.com",
      "user_username": "alice",
      "shift_start": "2023-01-02T09:00:00Z",
      "shift_end": "2023-01-02T17:00:00Z"
    },
    {
      "user_pk": "U7S8H84ARFTGN",
      "user_email": "bob@example.com",
      "user_username": "bob",
      "shift_start": "2023-01-04T09:00:00Z",
      "shift_end": "2023-01-04T17:00:00Z"
    },
    {
      "user_pk": "UC2CHRT5SD34X",
      "user_email": "alice@example.com",
      "user_username": "alice",
      "shift_start": "2023-01-06T09:00:00Z",
      "shift_end": "2023-01-06T17:00:00Z"
    },
    {
      "user_pk": "U7S8H84ARFTGN",
      "user_email": "bob@example.com",
      "user_username": "bob",
      "shift_start": "2023-01-09T09:00:00Z",
      "shift_end": "2023-01-09T17:00:00Z"
    },
    {
      "user_pk": "UC2CHRT5SD34X",
      "user_email": "alice@example.com",
      "user_username": "alice",
      "shift_start": "2023-01-11T09:00:00Z",
      "shift_end": "2023-01-11T17:00:00Z"
    },
    {
      "user_pk": "U7S8H84ARFTGN",
      "user_email": "bob@example.com",
      "user_username": "bob",
      "shift_start": "2023-01-13T09:00:00Z",
      "shift_end": "2023-01-13T17:00:00Z"
    },
    {
      "user_pk": "UC2CHRT5SD34X",
      "user_email": "alice@example.com",
      "user_username": "alice",
      "shift_start": "2023-01-16T09:00:00Z",
      "shift_end": "2023-01-16T17:00:00Z"
    },
    {
      "user_pk": "U7S8H84ARFTGN",
      "user_email": "bob@example.com",
      "user_username": "bob",
      "shift_start": "2023-01-18T09:00:00Z",
      "shift_end": "2023-01-18T17:00:00Z"
    },
    {
      "user_pk": "UC2CHRT5SD34X",
      "user_email": "alice@example.com",
      "user_username": "alice",
      "shift_start": "2023-01-20T09:00:00Z",
      "shift_end": "2023-01-20T17:00:00Z"
    },
    {
      "user_pk": "U7S8H84ARFTGN",
      "user_email": "bob@example.com",
      "user_username": "bob",
      "shift_start": "2023-01-23T09:00:00Z",
      "shift_end": "2023-01-23T17:00:00Z"
    },
    {
      "user_pk": "UC2CHRT5SD34X",
      "user_email": "alice@example.com",
      "user_username": "alice",
      "shift_start": "2023-01-25T09:00:00Z",
      "shift_end": "2023-01-25T17:00:00Z"
    },
    {
      "user_pk": "U7S8H84ARFTGN",
      "user_email": "bob@example.com",
      "user_username": "bob",
      "shift_start": "2023-01-27T09:00:00Z",
      "shift_end": "2023-01-27T17:00:00Z"
    }
  ],
  "current_page_number": 1,
  "page_size": 50,
  "total_pages": 1
}

Note: The response is paginated. You may need to make multiple requests to get all records.

Caveats

Some notes on the start_date and end_date query parameters:

  • they are both required and should represent ISO 8601 formatted dates
  • end_date must be greater than or equal to start_date
  • end_date cannot be more than 365 days in the future from start_date

Note: you can update schedules affecting past events, which will then change the output you get from this endpoint. To get consistent information about past shifts you must be sure to avoid updating rotations in-place but apply the changes as new rotations with the right starting dates.

Example script to transform data to .csv for all of your schedules

The following Python script will generate a .csv file, oncall-report-2023-01-01-to-2023-01-31.csv. This file will contain three columns, user_pk, user_email, and hours_on_call, which represents how many hours each user was on call during the period starting January 1, 2023 to January 31, 2023 (inclusive).

Python
import csv
import requests
from datetime import datetime

# CUSTOMIZE THE FOLLOWING VARIABLES
START_DATE = "2023-01-01"
END_DATE = "2023-01-31"
OUTPUT_FILE_NAME = f"oncall-report-{START_DATE}-to-{END_DATE}.csv"
MY_ONCALL_API_BASE_URL = "https://oncall-prod-us-central-0.grafana.net/oncall/api/v1/schedules"
MY_ONCALL_API_KEY = "meowmeowwoofwoof"

headers = {"Authorization": MY_ONCALL_API_KEY}
schedule_ids = [schedule["id"] for schedule in requests.get(MY_ONCALL_API_BASE_URL, headers=headers).json()["results"]]
user_on_call_hours = {}

for schedule_id in schedule_ids:
  response = requests.get(
    f"{MY_ONCALL_API_BASE_URL}/{schedule_id}/final_shifts?start_date={START_DATE}&end_date={END_DATE}",
    headers=headers)

  for final_shift in response.json()["results"]:
    user_pk = final_shift["user_pk"]
    end = datetime.fromisoformat(final_shift["shift_end"])
    start = datetime.fromisoformat(final_shift["shift_start"])
    shift_time_in_seconds = (end - start).total_seconds()
    shift_time_in_hours = shift_time_in_seconds / (60 * 60)

    if user_pk in user_on_call_hours:
      user_on_call_hours[user_pk]["hours_on_call"] += shift_time_in_hours
    else:
      user_on_call_hours[user_pk] = {
        "email": final_shift["user_email"],
        "hours_on_call": shift_time_in_hours,
      }

with open(OUTPUT_FILE_NAME, "w") as fp:
  csv_writer = csv.DictWriter(fp, ["user_pk", "user_email", "hours_on_call"])
  csv_writer.writeheader()

  for user_pk, user_info in user_on_call_hours.items():
    csv_writer.writerow({
      "user_pk": user_pk, "user_email": user_info["email"], "hours_on_call": user_info["hours_on_call"]})