This is a topical guide to general API client usage. Module Reference has in-depth documentation on client classes and methods.
InstallationThis library is available on the Python Package Index as pagerduty, e.g.:
Command Line InterfaceThis package also includes a basic CLI for PagerDuty Events API V2. For example, to trigger an incident:
pagerduty trigger -k $ROUTING_KEY --description "Network latency is high"
For more details, use the -h
flag to display the script’s helptext.
The first step is to construct a client object. The first argument to the constructor is the secret to use for accessing the API:
import pagerduty # REST API v2: client = pagerduty.RestApiV2Client(API_KEY) # REST API v2 with an OAuth2 access token (both are equivalent) client_oauth = pagerduty.RestApiV2Client(OAUTH_TOKEN, auth_type='oauth2') client_oauth = pagerduty.RestApiV2Client(OAUTH_TOKEN, auth_type='bearer') # Events API v2, including change events: events_client = pagerduty.EventsApiV2Client(ROUTING_KEY)
Session objects, being descendants of requests.Session, can also be used as context managers. For example:
with pagerduty.RestApiV2Client(API_KEY) as client: do_application(client)Using Non-US Service Regions
If your PagerDuty account is in the EU or other service region outside the US, set the url
attribute according to the documented API Access URLs, i.e. for the EU:
# REST API client.url = 'https://api.eu.pagerduty.com' # Events API: events_client.url = 'https://events.eu.pagerduty.com' # Slack Integration "Connections" API (and likewise for other # APIs that use apps.pagerduty.com and/or some unique base path): slack_integration_client.url = 'https://apps.eu.pagerduty.com/integration-slack'Performing an OAuth Exchange to Obtain an Access Token
The client class pagerduty.OAuthTokenClient
provides methods implementing the OAuth exchange requests described in Obtaining a User OAuth Token via Code Grant and Obtaining an App OAuth Token.
token_client = pagerduty.OAuthTokenClient(client_secret, client_id)
To generate the URL that the user must visit to authorize the application:
# With a client object: authorize_url = token_client.authorize_url(scope, redirect_uri) # Without a client object: authorize_url = pagerduty.OAuthTokenClient.get_authorize_url(client_id, scope, redirect_uri)
The application must provide a redirect URI at which to receive the authorization code parameter. Once the user visits, has authorized the application and is redirected back to the application at the redirect URI, the code
parameter appended to it will contain the authorization code. The code can then be exchanged for an access token as following:
# auth_code contains the "code" parameter in the redirect URL of the application: auth_response = token_client.get_new_token_from_code(auth_code, scope, redirect_uri) access_token = auth_response['access_token'] refresh_token = auth_response['refresh_token']Performing OAuth Token Refresh Automatically
As of version 3.0.0, the OAuth response dictionary returned by token-getting methods of pagerduty.OAuthTokenClient
will include a property expiration_date
containing a string that is an ISO8601-formatted date/time indicating when the included token will expire. Assuming that your application securely stores this string value in addition to access_token
and refresh_token
, and has the means to retrieve these values, they can be used to call pagerduty.OAuthTokenClient.refresh_client
, which instantiates a new pagerduty.RestApiV2Client
and automatically refreshes the access token as necessary.
# Assume the calling application implements methods securely_get_values and # securely_store_values to recall and store secrets used for API access: access_token, refresh_token, expiration_date = securely_get_values() rest_client, auth_response = token_client.refresh_client( access_token, refresh_token, expiration_date ) # If auth_response == None, the token was not refreshed and does not need # to be updated; otherwise it will be similar to the value returned by # other token-getting methods: if type(auth_response) is dict: securely_store_values( access_token = auth_response['access_token'], refresh_token = auth_response['refresh_token'], expiration_date = auth_response['expiration_date'] )
Note, the current default behavior of OAuthTokenClient
is to refresh the token if it is going to expire less than 24 hours in the future. This “buffer time” (expressed as a positive integer number of seconds in the future) can be controlled by setting the property pagerduty.OAuthTokenClient.early_refresh_buffer
.
Making a request and decoding the response: obtaining a resource’s contents and having them represented as a dictionary object using three different methods:
# Using get: response = client.get('/users/PABC123') user = None if response.ok: user = response.json()['user'] # Using jget (return the full body after decoding): user = client.jget('/users/PABC123')['user'] # Using rget (return the response entity after unwrapping): user = client.rget('/users/PABC123') # >>> user # {"type": "user", "email": "user@example.com", ... }
Using pagination: iter_all
, iter_cursor
, list_all
and dict_all
can be used to obtain results from a resource collection:
# Print each user's email address and name: for user in client.iter_all('users'): print(user['id'], user['email'], user['name'])
Pagination with query parameters: set the params
keyword argument, which is converted to URL query parameters by Requests:
# Get a list of all services with "SN" in their name: services = client.list_all('services', params={'query': 'SN'}) # >>> services # [{'type':'service', ...}, ...]
Searching resource collections: use find
to look up a resource (a user, in this example) exactly matching a string using the query
parameter on an index endpoint:
# Find the user with email address "jane@example35.com" user = client.find('users', 'jane@example35.com', attribute='email') # >>> user # {'type': 'user', 'email': 'jane@example35.com', ...}
Getting a count of records: use get_total
on any endpoint that supports classic pagination:
# Get the total number of users in the whole account: total_users = client.get_total('users') # Get the total number of users on a given team: total_users_on_team_x = client.get_total( 'users', params = {'team_ids[]': ['PGHI789']} )
Updating a resource: use the json
keyword argument to set the body of the request:
# >>> user # {'self':'https://api.pagerduty.com/users/PABC123', 'type': 'user', ...} # (1) using put directly: updated_user = None response = client.put(user['self'], json={ 'user': { 'type':'user', 'name': 'Jane Doe' } }) if response.ok: updated_user = response.json()['user'] # (2) using rput: # - The URL argument may also be a resource / resource reference dict # - The json argument doesn't have to include the "user" wrapper dict # - If an HTTP error is encountered, it will raise an exception updated_user = client.rput(user, json={ 'type':'user', 'name': 'Jane Doe' })
Idempotent create/update: create a user if one doesn’t already exist based on the dictionary object user_data
, using the “email” key/property as the uniquely identifying attribute, and update it if it exists and differs from user_data
:
user_data = {'email': 'user123@example.com', 'name': 'User McUserson'} updated_user = client.persist('users', 'email', user_data, update=True)
Using multi-valued set filters: set the value in the params
dictionary at the appropriate key to a list. Square brackets will then be automatically appended to the names of list-type-value parameters as necessary. For example:
# Query all open incidents assigned to a user incidents = client.list_all( 'incidents', params={ # Both of the following parameter names are valid: 'user_ids[]': ['PHIJ789'], 'statuses': ['triggered', 'acknowledged'] # "[]" will be automatically appended } ) # API calls will look like the following: # GET /incidents?user_ids%5B%5D=PHIJ789&statuses%5B%5D=triggered&statuses%5B%5D=acknowledged&offset=0&limit=100
Get a list of all incident notes submitted by a team within a time range:
notes = list(client.iter_incident_notes(params={ 'team_ids':['PN1T34M'], 'since': '2025-01-01', 'until': '2025-07-01' })) # >>> notes # [{'type': 'annotate_log_entry', 'summary': 'Resolved by reboot' ... }, ... ]
Performing multi-update: for endpoints that support it only, i.e. PUT /incidents
:
# Acknowledge all triggered incidents assigned to a user: incidents = client.list_all( 'incidents', params={'user_ids':['PHIJ789'],'statuses':['triggered']} ) for i in incidents: i['status'] = 'acknowledged' updated_incidents = client.rput('incidents', json=incidents)Events API v2
Trigger and resolve an alert, getting its deduplication key from the API, using pagerduty.EventsApiV2Client
:
dedup_key = events_client.trigger("Server is on fire", 'dusty.old.server.net') # ... events_client.resolve(dedup_key)
Trigger an alert and acknowledge it using a custom deduplication key:
events_client.trigger("Server is on fire", 'dusty.old.server.net', dedup_key='abc123') # ... events_client.acknowledge('abc123')
Submit a change event using a pagerduty.EventsApiV2Client
instance:
events_client.submit("new build finished at latest HEAD", source="automation")Client Classes
For each of the APIs documented in the top-level sections of the API References page, this library supports and provides abstraction for the API if it has a dedicated client class. These classes are documented under “API Client Classes” in Module Reference.
All client classes are based on pagerduty.ApiClient
and inherit its features.
The class pagerduty.RestApiV2BaseClient
defines abstractions for features documented under the generic REST API v2 Overview, and pagerduty.RestApiV2Client
inherits them.
The “Integration” APIs are based loosely on REST API v2, and support several of the same features and conventions to varying degrees. For this reason, they also are based on inherit the features of pagerduty.RestApiV2BaseClient
.
Generally, all of the features of requests.Session are available to the user as they would be if using the Requests Python library directly, since pagerduty.ApiClient
and its subclasses for the REST/Events APIs are descendants of it.
The get
, post
, put
and delete
methods of REST/Events API client classes are similar to the analogous functions in requests.Session. The arguments they accept are the same and they all return requests.Response objects.
Any keyword arguments passed to the j*
or r*
methods will be passed through to the analogous method in Requests, though in some cases the arguments (i.e. json
) are first modified.
For documentation on any generic HTTP client features that are available, refer to the Requests documentation.
URLsThe first argument to most of the client methods is the URL. However, there is no need to specify a complete API URL. Any path relative to the root of the API, whether or not it includes a leading slash, is automatically normalized to a complete API URL. For instance, one can specify users/PABC123
or /users/PABC123
instead of https://api.pagerduty.com/users/PABC123
.
One can also pass the full URL of an API endpoint and it will still work, i.e. the self
property of any object can be used, and there is no need to strip out the API base URL.
The r*
and j*
methods, i.e. pagerduty.RestApiV2Client.rget
, can also accept a dictionary object representing an API resource or a resource reference (see: resource references) in place of a URL, in which case the value at its self
key will be used as the request URL.
As with Requests, there is no need to compose the query string (everything that will follow ?
in the URL). Simply set the params
keyword argument to a dictionary, and each of the key/value pairs will be serialized to the query string in the final URL of the request:
first_dan = client.rget('users', params={ 'query': 'Dan', 'limit': 1, 'offset': 0, }) # GET https://api.pagerduty.com/users?query=Dan&limit=1&offset=0
To specify a multi-value parameter, i.e. include[]
, set the argument to a list. If a list is given, and the key name does not end with []
(which is required for all such multi-valued parameters in REST API v2), then []
will be automatically appended to the parameter name. For example:
# If there are 82 services with name matching "foo" this will return all of # them as a list: foo_services = client.list_all('services', params={ 'query': 'foo', 'include': ['escalation_policies', 'teams'], 'limit': 50, }) # GET https://api.pagerduty.com/services?query=foo&include%5B%5D=escalation_policies&include%5B%5D=teams&limit=50&offset=0 # GET https://api.pagerduty.com/services?query=foo&include%5B%5D=escalation_policies&include%5B%5D=teams&limit=50&offset=50 # >>> foo_services # [{"type": "service" ...}, ... ]Requests and Responses
To set the request body in a post or put request, pass as the json
keyword argument an object that will be JSON-encoded as the body.
To obtain the response from the API, if using plain get
, post
, put
or delete
, use the returned requests.Response object. That object’s json()
method will return the result of JSON-decoding the response body (it will typically of type dict
). Other metadata such as headers can also be obtained:
response = client.get('incidents') # The UUID of the API request, which can be supplied to PagerDuty Customer # Support in the event of server errors (status 5xx): print(response.headers['x-request-id'])
If using the j*
methods, i.e. pagerduty.RestApiV2Client.jget
, the return value will be the full body of the response from the API after JSON-decoding, and the json
keyword argument is not modified.
When using the r*
methods, the json
keyword argument is modified before sending to Requests, if necessary, to encapsulate the body inside an entity wrapper. The response is the decoded body after unwrapping, if the API endpoint returns wrapped entities. For more details, refer to Entity Wrapping.
Main article: Types
Note these analogues in structure between the JSON schema and the object in Python:
If the data type documented in the schema is “object”, then the corresponding type of the Python object will be dict
.
If the data type documented in the schema is “array”, then the corresponding type of the Python object will be list
.
Generally speaking, the data type in the decoded object is according to the design of the json Python library.
For example, consider the example structure of an escalation policy as given in the API reference page for GET /escalation_policies/{id}
(“Get an escalation policy”). To access the name of the second target in level 1, assuming the variable ep
represents the unwrapped escalation policy object:
ep['escalation_rules'][0]['targets'][1]['summary'] # "Daily Engineering Rotation"
To add a new level, one would need to create a new escalation rule as a dictionary object and then append it to the escalation rules
property. Using the example given in the API reference page:
new_rule = { "escalation_delay_in_minutes": 30, "targets": [ { "id": "PAM4FGS", "type": "user_reference" }, { "id": "PI7DH85", "type": "schedule_reference" } ] } ep['escalation_rules'].append(new_rule) # Save changes: ep = client.rput(ep, json=ep)Resource Schemas
Main article: Resource Schemas
The details of any given resource’s schema can be found in the request and response examples from the PagerDuty API Reference pages for the resource’s respective API, as well as the page documenting the resource type itself.
Entity WrappingSee also: Wrapped Entities. Most of PagerDuty’s REST API v2 endpoints respond with their content wrapped inside of another object with a single key at the root level of the (JSON-encoded) response body, and/or require the request body be wrapped in another object that contains a single key. Endpoints with such request/response schemas usually (with few exceptions) support pagination.
Identifying Wrapped-entity EndpointsIf the endpoint’s response schema or expected request schema contains only one property that contains all of the content of the API resource, the endpoint is said to wrap entities. In resource collection endpoints that support pagination, the response schema contains additional pagination-related properties such as more
(for classic pagination) or next_cursor
(for cursor-based pagination) and no other content-bearing properties.
The following methods will automatically extract and return the wrapped content of API responses, and wrap request entities for the user as appropriate:
pagerduty.RestApiV2BaseClient.dict_all
: Create a dictionary of all results from a resource collection
pagerduty.RestApiV2BaseClient.find
: Find and return a specific result of a resource collection that matches a query
pagerduty.RestApiV2BaseClient.iter_all
: Iterate through all results of a resource collection
pagerduty.RestApiV2BaseClient.iter_cursor
: Iterate through all results of a resource collection using cursor-based pagination
pagerduty.RestApiV2BaseClient.list_all
: Create a list of all results from a resource collection
pagerduty.RestApiV2BaseClient.rget
: Get the wrapped entity or resource collection at a given endpoint
pagerduty.RestApiV2BaseClient.rpost
: Send a POST request, wrapping the request entity / unwrapping the response entity
pagerduty.RestApiV2BaseClient.rput
: Send a PUT request, wrapping the request entity / unwrapping the response entity
pagerduty.RestApiV2BaseClient.persist
: Create a resource entity with specified attributes if one that matches them does not already exist
There are some API endpoints that do not follow API schema conventions for entity wrapping. Some do not wrap entities at all. On all endpoints that do not wrap entities, the results for a given r*
method would be the same if using the equivalent j*
method, and the details of request and response schemas are are left to the end user to extract and use as desired. Moreover, on all endpoints that completely lack entity wrapping, pagination is not supported, i.e. pagerduty.RestApiV2BaseClient.iter_all
will raise pagerduty.UrlError
if used with them.
The endpoint “Create Business Service Subscribers”, or POST /business_services/{id}/subscribers
, wraps the response differently from the request. The end user can still pass the content to be wrapped via the json
argument without the subscribers
wrapper, while the return value is the list representing the content inside of the subscriptions
wrapper in the response, and there is no need to hard-code any particular endpoint’s wrapper name into the usage of the client.
Some endpoints are unusual in that the request must be wrapped but the response is not wrapped or vice versa, i.e. creating Schedule overrides (POST /schedules/{id}/overrides
) or to create a status update on an incient (POST /incidents/{id}/status_updates
). In all such cases, the user still does not need to account for this, as the content will be returned and the request entity is wrapped as appropriate.
What that looks like, for the “Create one or more overrides” endpoint:
created_overrides = client.rpost('/schedules/PGHI789/overrides', json=[ { "start": "2023-07-01T00:00:00-04:00", "end": "2023-07-02T00:00:00-04:00", "user": { "id": "PEYSGVA", "type": "user_reference" }, "time_zone": "UTC" }, { "start": "2023-07-03T00:00:00-04:00", "end": "2023-07-01T00:00:00-04:00", "user": { "id": "PEYSGVF", "type": "user_reference" }, "time_zone": "UTC" } ]) # >>> created_overrides # [ # {'status': 201, 'override': {...}}, # {'status': 400, 'errors': ['Override must end after its start'], 'override': {...}} # ]Multi-updating
Multi-update actions can be performed using rput
with some endpoints. For instance, to resolve two incidents with IDs PABC123
and PDEF456
:
client.rput( "incidents", json=[ {'id':'PABC123','type':'incident_reference', 'status':'resolved'}, {'id':'PDEF456','type':'incident_reference', 'status':'resolved'}, ], )
In this way, a single API request can more efficiently perform multiple update actions.
It is important to note, however, that updating incidents requires using a user-scoped access token or setting the From
header to the login email address of a valid PagerDuty user. To set this, pass it through using the headers
keyword argument, or set the pagerduty.RestApiV2Client.default_from
property, or pass the email address as the default_from
keyword argument when constructing the client initially.
The pagerduty.UrlError
is raised prior to making API calls, and it indicates unsupported URLs and/or malformed input.
The base exception class for all errors encountered when making requests is pagerduty.Error
. This includes network / transport issues where there is no response from the API, in which case the exception will inherit from the exception raised by the underlying HTTP library.
All errors that involve a response from the API are instances of pagerduty.HttpError
and will have a response
property containing the requests.Response object. Its subclass pagerduty.HttpServerError
is used for special cases when the API is responding in an unexpected way.
One can thus define specialized error handling logic in which the REST API response data (i.e. headers, code and body) are available in the catching scope. For example, the following code prints “User not found” in the event of a 404, prints out the user’s email if the user exists and raises the underlying exception if it’s any other HTTP error code:
try: user = client.rget("/users/PJKL678") print(user['email']) except pagerduty.HttpError as e: if e.response.status_code == 404: print("User not found") else: raise eLogging
When a client object is instantiated, a Logger object is created as follows:
Its level is unconfigured (logging.NOTSET
) which causes it to defer to the level of the parent logger. The parent is the root logger unless specified otherwise (see Logging Levels).
The logger is initially not configured with any handlers. Configuring handlers is left to the discretion of the user (see logging.handlers)
The logger can be accessed and set through the property pagerduty.ApiClient.log
.
The attribute pagerduty.ApiClient.print_debug
enables sending debug-level log messages from the client to command line output. It is used as follows:
# Method 1: keyword argument, when constructing a new client: client = pagerduty.RestApiV2Client(api_key, debug=True) # Method 2: on an existing client, by setting the property: client.print_debug = True # to disable: client.print_debug = False
What this does is assign a logging.StreamHandler directly to the client’s logger and set the log level to logging.DEBUG
. All log messages are then sent directly to sys.stderr
. The default value for all clients is False
, and it is recommended to keep it that way in production systems.
To configure the client to use a host as a proxy for HTTPS traffic, update the proxies
attribute:
# Host 10.42.187.3 port 4012 protocol https: client.proxies.update({'https': '10.42.187.3:4012'})HTTP Retry Configuration
Session objects support retrying API requests if they receive a non-success response or if they encounter a network error.
This behavior is configurable through the following properties:
pagerduty.ApiClient.retry
: a dictionary that allows defining per-HTTP-status retry limits
pagerduty.ApiClient.max_http_attempts
: The maximum total number of unsuccessful requests to make in the retry loop of pagerduty.ApiClient.request
before returning
pagerduty.ApiClient.max_network_attempts
: The maximum number of retries that will be attempted in the case of network or non-HTTP error
pagerduty.ApiClient.sleep_timer
: The initial cooldown factor
pagerduty.ApiClient.sleep_timer_base
: Factor by which the cooldown time is increased after each unsuccessful attempt
pagerduty.ApiClient.stagger_cooldown
: Randomizing factor for increasing successive cooldown wait times
By default, after receiving a status 429 response, clients will retry an unlimited number of times, increasing the wait time before retry each successive time. When encountering status 401 Unauthorized
, the client will immediately raise pagerduty.HttpError
; this is a non-transient error caused by an invalid credential.
For all other success or error statuses, the underlying request method in the client will return the requests.Response object.
Exponential CooldownAfter each unsuccessful attempt, the client will sleep for a short period that increases exponentially with each retry.
Let:
a = pagerduty.ApiClient.sleep_timer_base
(base of the exponent, default value 2
)
t0 = pagerduty.ApiClient.sleep_timer
(initial sleep timer, default value 1.5
)
tn = Sleep time after n attempts
rn = a randomly-generated real number between 0 and 1, distinct for each n-th request
Assuming ρ = 0 (the default value):
tn = t0 an
If ρ is nonzero:
tn = a (1 + ρ rn) tn-1
Configuring Retry BehaviorThe dictionary property pagerduty.ApiClient.retry
allows customization of HTTP retry limits on a per-HTTP-status basis. This includes the ability to override the above defaults for 401 and 429, although that is not recommended.
Each key in the dictionary represents a HTTP status, and its associated value the number of times that the client will retry the request if it receives that status. Success statuses (2xx) will be ignored.
If a different error status is encountered on a retry, it won’t count towards the limit of the first status, but will be counted separately. However, the total overall number of attempts that will be made to get a success status is limited by pagerduty.ApiClient.max_http_attempts
. This will always supersede the maximum number of retries for any status defined in pagerduty.ApiClient.retry
if it is lower.
Low-level HTTP request functions in client classes, i.e. get
, will return requests.Response objects when they run out of retries. Higher-level functions that require a success status response, i.e. pagerduty.RestApiV2Client.list_all
and pagerduty.EventsApiV2Client.trigger
, will raise instances of pagerduty.HttpError
, but only after the configured retry limits are reached in the underlying HTTP request methods.
Example:
# This will take about 30 seconds plus API request time, carrying out four # attempts with 2, 4, 8 and 16 second pauses between them, before finally # returning the status 404 response object for the user that doesn't exist: client.max_http_attempts = 4 # lower value takes effect client.retry[404] = 5 # this won't take effect client.sleep_timer = 1 client.sleep_timer_base = 2 response = client.get('/users/PNOEXST') # Same as the above, but with the per-status limit taking precedence, so # the total wait time is 62 seconds: client.max_http_attempts = 6 response = client.get('/users/PNOEXST')
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4