Skip to content
  • There are no suggestions because the search field is empty.

Calling the API from Python: Parameters, Pagination and Errors

Every op takes path values positionally, sends keyword fields as the JSON body for writes and as query parameters for reads, follows all four AlertOps paging styles through .iter() and .all(), and raises one APIError subclass per HTTP status.

Overview

Relevant for Developers writing their first real integration

Every endpoint in the SDK is an Op bound to a client, and every op follows the same call convention, so once you know it you can call any of the 784 ops from docs/REFERENCE.md without reading its code. This article covers how arguments map to the HTTP request, how pagination works, what the SDK raises on failure, which failures it retries, and how to observe calls for logging or metrics.

Key benefits:

  • One convention — path values, body fields, query parameters, bare-array bodies and multipart uploads all use the same signature.
  • Paging handled — the four AlertOps paging styles hide behind .iter() and .all().
  • One exception tree — three different error body formats become e.messages, a plain list of strings.
  • Safe retries — idempotent requests retry on throttling and gateway errors, and POSTs never retry when they could have reached the server.

The call convention

Relevant for Developers mapping an API reference row to a Python call

Every op has this signature:

op(*path_values, [body], body=None, params=None, files=None, headers=None, **fields)
ArgumentWhat it does
*path_valuesFill the {placeholders} in the path, in template order. Values are URL-quoted, so a user name with @ is safe.
path params by keywordao.groups.get(group_id=12345) works the same as ao.groups.get(12345).
one extra positionalThe request body. Use it for endpoints that take a bare JSON array.
body=The request body, given by keyword. Keyword **fields merge into it when it's a dict.
**fieldsFor POST, PUT and PATCH (and DELETEs that take a body): merged into the JSON body. For GET and DELETE: query parameters.
params=Always query parameters, whatever the method.
files=Send multipart/form-data instead of JSON. Body fields become form fields.
headers=Extra headers for this call only.

Query values are normalised: None is dropped, True/False become true/false, and lists are joined with commas.

ao.groups.get(12345)                                        # GET /groups/12345
ao.groups.list(search="NOC", limit=25) # GET /groups?search=NOC&limit=25
ao.groups.update_basic(12345, group_name="NOC Primary", # PUT /groups/12345/basic
description=["Network operations"]) # with a JSON body
ao.groups.members.add(12345, [ # bare JSON array body
{"member_type": "User", "member": "101", "sequence": 1, "roles": ["Primary"]},
])
ao.users.list(params={"include": "roles"}, search="ann") # params= and fields combine on GET

Some ops carry defaults that are merged under your fields. For example, ao.escalation_policies.create defaults priority="Critical", enabled=True and quick_launch=False, and anything you pass wins.

Multipart uploads

Pass files= with the same tuple format httpx uses: {"file": (filename, content, content_type)}.

with open("members.csv", "rb") as f:
ao.groups.members.import_csv(files={"file": ("members.csv", f.read(), "text/csv")})

ao.alerts.attachments.upload(98765, files={"file": ("log.txt", b"disk full", "text/plain")})
ao.status_hub.pages.assets.upload_logo(42, files={"file": ("logo.png", png_bytes, "image/png")})

Argument errors raised before any request

ErrorCause
TypeError: <Op>() missing path parameter 'group_id'A path placeholder got no value.
ValueError: <Op>(): path parameter 'group_id' is emptyA path value is None or "". This stops a call such as DELETE /groups/ from hitting the wrong route.
TypeError: <Op>() does not take a request bodyYou passed a body to a GET (or a DELETE without a body).
TypeError: <Op>() got too many positional argumentsMore positionals than path values plus one body.
TypeError: <Op>(): cannot merge keyword fields into a list bodyYou passed a bare-array body and keyword fields together.

Methods on every op

MethodHTTPWhat it does
op(...)the op's methodRuns one request and returns the parsed result.
op.iter(...)the op's method, once per pageYields every item across all pages. On AsyncAlertOps it's an async iterator.
op.all(...)the op's method, once per pageCollects every item across pages into an AOList.
op.lazy(...)noneReturns a Call to yield from a flow (see Async Client and Composable Flows).
op.lazy_all(...)noneSame, but the flow receives every item across pages.
op.opnoneThe underlying Op: .method, .path, .base, .path_params, .op_id.
ao.request(method, path, base=…, auth=…)anyCalls a path the SDK doesn't model, with the same convention.

Pagination

Relevant for Developers listing more than one page of anything

Call the op directly for one page, .iter() to stream every item, or .all() to collect them. Filters you pass to .iter() or .all() are sent on every page.

page = ao.users.list(limit=25, offset=1)            # one page: rows 26-50
for u in ao.users.list.iter(search="ann"): # every page, lazily
print(u.user_name)
everything = ao.escalation_policies.list.all() # AOList of every policy

AlertOps uses four paging styles. The SDK knows which one each list op uses:

StyleUsed byRequestStops when
Page indexv2 lists: users.list, groups.list, escalation_policies.list, response_plays.list, inbound.list, workflows.list, technical_services.list, admin.audit_trail.listlimit + offset, where offset is 0, 1, 2 …A page has fewer than limit rows.
v3 cursorstatus_hub.incidents.list, status_hub.maintenance.list, status_hub.services.list, status_hub.pages.subscribers.listpageSize + after=<nextCursor>; response {data, nextCursor, hasMore}nextCursor is empty or hasMore is false.
Page numberstatus_hub.notifications.incidents, status_hub.notifications.maintenance_windowspageSize + pageNumber, starting at 1A page has fewer than pageSize rows.
Alerts cursoralerts.listlimit + next=<metadata.next>; response {alerts[], metadata{limit, previous, next}}metadata.next is empty.

A fifth, single-page style covers inbound.log.list: it sends limit only, so .iter() yields one page.

offset is a page index, not a row offset
Verified live: with limit=3, offset=1 returns rows 4–6, not rows 2–4. If you page by hand, add 1 to offset per page. .iter() and .all() already do this.

Alert lists need a date window and viewBy="A"
GET /alerts filters on createdFrom / createdTo (YYYY-MM-DD), and createdTo is exclusive, so pass tomorrow to include today. Without viewBy="A" you only see the caller's own alerts. ao.alerts.list_recent(days=1) sets both for you.

import datetime as dt

today = dt.date.today()
for alert in ao.alerts.list.iter(viewBy="A", createdFrom=str(today - dt.timedelta(days=7)),
createdTo=str(today + dt.timedelta(days=1))):
print(alert.alert_id, alert.status)

.iter() on an op that isn't paginated still works: it runs the request once and yields the items of the result (the unwrapped list, or the data / items / results list of an envelope).

Errors

Relevant for Developers handling failures

Every non-2xx response raises a subclass of APIError, chosen by status. All SDK exceptions inherit from AlertOpsError.

StatusExceptionTypical cause
400, 422BadRequestErrorInvalid body or unknown name ("Invalid User : <name>").
401AuthenticationErrorMissing or invalid API key.
402PaymentRequiredErrorFeature not on the plan, for example call routing.
403PermissionDeniedErrorThe key's user isn't entitled, or the route rejects API keys.
404NotFoundErrorUnknown id or route.
409ConflictErrorConflicts with existing state.
429RateLimitErrorThrottled, after automatic retries.
other 4xxAPIErrorAny status without its own class.
5xxServerErrorServer failure.
networkTransportErrorDNS, connect or timeout failure after retries.
before sendingConfigurationError, UnsupportedAuthErrorNo API key, no base URL, or a session-only op without a session token.
from alertops import APIError, BadRequestError, NotFoundError

try:
ao.groups.get(999)
except NotFoundError as e:
print(e.status, e.messages) # 404 ['Group: 999 not found']
print(e.op, e.method, e.url) # Groups.get GET https://app.alertops.com/api/v2/groups/999
except APIError as e:
print(e.status, e.body) # the raw parsed body, for anything unexpected
AttributeDescription
statusHTTP status code.
messagesList of human-readable messages, normalised from any of the error shapes below.
opOp id, for example Groups.get (Resource.op_name).
method, urlThe request that failed.
bodyThe parsed response body (dict, list or text).

str(e) is "<status> <method> <url> [<op>]: <messages joined by '; '>", which is what you see in a traceback.

Error body shapes

AlertOps answers errors in several formats. e.messages flattens all of them:

SourceBodye.messages
v2{"errors": [{"message": "Group: 999 not found"}]}["Group: 999 not found"]
v2{"reason": "Invalid API Key"}["Invalid API Key"]
v3{"message": "Status page with ID 9 not found"}["Status page with ID 9 not found"]
v3 problem+json{"title": "One or more validation errors", "errors": {"name": ["required"]}}["name: required"]

A plain-text body becomes a one-item list, and the AIOps add-on's {"error": [{"Message": …}]} is handled too.

Retries

Relevant for Developers running the SDK under load or on flaky networks

The client retries up to max_retries times (default 3, so at most 4 attempts) with exponential backoff: backoff_factor * 2**attempt seconds (0.5 s, 1 s, 2 s …), capped at backoff_max (8 s), with ±25% jitter. A Retry-After header replaces the computed delay, capped at four times backoff_max (32 s by default).

RequestRetried onNever retried on
GET, HEAD, PUT, DELETE, OPTIONS429, 502, 503, 504, and any network error500, and every 4xx except 429
POST, PATCH429, and connect errors or connect timeouts (the request never reached the server)5xx and read timeouts, because the server may already have acted

PUT and DELETE count as idempotent
AlertOps PUTs replace whole records, so repeating one is safe. A POST is retried only when it can't have created anything, so a retry never opens a duplicate alert or creates a second group.

Set max_retries=0 to turn retries off, for example in tests that assert on the first response.

Listeners for logging and metrics

Relevant for Developers adding observability

add_listener(fn) registers a callable that receives one event per completed request. The live test suite uses the same hook to record coverage.

import logging

def record(event: dict) -> None:
# {"op": "Groups.list", "method": "GET", "url": "...", "status": 200, "elapsed": 0.21}
# Network failures: {"op", "method", "url", "status": 0, "error": "ConnectError(...)"}
logging.info("%s %s -> %s", event["op"], event["method"], event["status"])

ao.add_listener(record)
ao.groups.list(limit=5)
ao.remove_listener(record)
  • Events fire once per final response, not once per retry attempt.
  • An exception inside a listener is logged and swallowed, so it never breaks a call.
  • The SDK also logs each request at DEBUG on the alertops logger: GET <url> -> 200 (210 ms).

Best Practices

Relevant for Anyone running SDK code in production

Pagination

  • Use .iter() for large lists so rows stream instead of loading at once.
  • Don't page by hand. If you must, remember offset counts pages, not rows.
  • Always send viewBy="A" and a date window to alerts.list, or use alerts.list_recent.

Errors

  • Catch the narrowest class you can handle (NotFoundError for "already gone"), then APIError as a fallback.
  • Log e.op and e.messages, not only str(e): they name the op and the server's reason.
  • Treat PaymentRequiredError and PermissionDeniedError as configuration facts about the tenant, not as transient failures.

Writes

  • Many AlertOps PUTs replace the whole record. Prefer the read-merge-write helpers (update, update_options, modify, patch) listed in docs/REFERENCE.md over a raw PUT with a partial body.

Configuration Examples

Relevant for Starting templates for common call patterns

Example 1 — Delete if it exists

from alertops import NotFoundError

try:
ao.groups.delete(12345)
except NotFoundError:
pass # already gone

ao.groups.delete_if_exists(12345) # the same thing, as a built-in helper (returns True/False)

Example 2 — Export every user to a list of dicts

rows = [{"id": u.user_id, "name": u.user_name}
for u in ao.users.list.iter(params={"include": "roles"})]

Example 3 — Status Hub incidents through the v3 cursor

for inc in ao.status_hub.incidents.list.iter(includeResolved=True, pageSize=50):
print(inc.get("incidentId"), inc.get("title"))

Example 4 — Plan-gated feature check

from alertops import PaymentRequiredError

try:
numbers = ao.call_routing.list()
except PaymentRequiredError as e:
print("Call routing not on this plan:", e.messages)

Troubleshooting

Relevant for Anyone debugging failed calls

PaymentRequiredError: "Package settings do not have CallRouting enabled."

  • The tenant hasn't purchased the call routing add-on. Every /call_routing* route answers 402, even for an unknown id. The /app/call_routing/* pickers still work.
  • Quick start updates answer 402 "Package settings do not have Quickstart enabled" on plans without it.

PermissionDeniedError: "The API key doesn't have permissions to perform the request"

  • Some routes reject API keys even for the account Owner (location and holiday writes). Make the change in the UI.
  • Business services and v2 business incidents answer 403 when the tenant isn't entitled to them.

BadRequestError: "Invalid User : <name>" but NotFoundError for an id

  • GET /users/{x} accepts an id or a user name. An unknown id is 404 "User does not exist", an unknown name is 400. Use ao.users.find_by_username(name), which returns None for both.

Only one page comes back

  • You called the op directly, which returns one page. Use .iter() or .all().
  • For alerts, the default viewBy shows only your alerts, and a new alert can take a few seconds to appear in the list.

TypeError: does not take a request body

  • You passed a positional body or body= to a GET. Pass filters as keyword fields or params=.

TransportError after a long wait

  • The network failed on every attempt. With the defaults a GET makes 4 attempts with up to 8 s between them. Check connectivity and timeout, or lower max_retries to fail faster.

Related Articles

Need help?

Email support@alertops.com or contact your AlertOps representative.