Reference › Python SDK

Python SDK

A typed client for the REST API.

pip install "uptimer-python-sdk>=1.5.0"      # or: uv add "uptimer-python-sdk>=1.5.0"

The SDK version tracks the server it targets. 1.5.x speaks to uptimer 1.5.0 and later; patch numbers move independently. So install the SDK whose major.minor matches your server — no compatibility table to look up.

Still on API v1? Pin uptimer-python-sdk<1. The server’s v1 is unchanged and supported, so 0.4.x keeps working against a 1.5.0 server — it just cannot use anything new. There is no v1 surface left in 1.5.x: client.v1, its models and its kinds are gone — client.v2 takes its place.

A complete example

One script, the whole surface: connect, check the server, list what is there, create website monitoring, read it back, change it, look for incidents, and clean up. Set UPTIMER_API_KEY and UPTIMER_BASE_URL and it runs as-is.

"""An end-to-end tour of uptimer-python-sdk 1.5.x."""

import os

from uptimer.client import UptimerClient
from uptimer.errors import DefaultUptimerApiError, IncompatibleServerError
from uptimer.models.v2 import (
    AGREEMENT_MAJORITY,
    STATUS_PENDING,
    CreateWebsiteMonitorRequest,
    UpdateWebsiteMonitorRequest,
    WebsiteMonitorRequest,
    WebsiteMonitorResponse,
    WebsiteMonitorResponseBody,
)

client = UptimerClient(
    api_key=os.environ["UPTIMER_API_KEY"],
    base_url=os.environ.get("UPTIMER_BASE_URL", "http://localhost:2517/api"),
)
# For the hosted product, swap the two lines above for:
#   from uptimer.client import UptimerCloudClient
#   client = UptimerCloudClient(api_key=os.environ["UPTIMER_API_KEY"])

# 1. Fail fast on a server that has no API v2, with a message that names the fix
#    rather than a 404 on the first real call. Both this and client.version()
#    read the shared, unversioned /version.
try:
    print("server:", client.check_compatibility())
except IncompatibleServerError as exc:
    raise SystemExit(str(exc)) from exc

print("version:", client.version())   # same endpoint, no compatibility gate
# client.ensure_compatible() is the same check, run at most once per client.

# 2. Everything the API versions is reached through client.v2.
workspace = client.v2.workspaces.all()[0]
print("workspace:", workspace.id, workspace.name, f"({workspace.role})")

locations = client.v2.locations.all()
for location in locations:
    print("location:", location.name, location.active_workers_count, "worker(s)")

# 3. Create website monitoring. This one call also creates the monitoring
#    subject, its built-in HTTP signal and its Reachability rule.
monitor = client.v2.monitoring.websites.create(
    CreateWebsiteMonitorRequest(
        name="Checkout API",
        interval=60,                   # seconds, in whole minutes (>= 60)
        workspace_id=workspace.id,     # required on create; fixed afterwards
        request=WebsiteMonitorRequest(
            url="https://checkout.example/health",
            method="GET",              # GET, POST, PATCH or OPTIONS
        ),
        response=WebsiteMonitorResponse(
            statuses=[200],
            body=WebsiteMonitorResponseBody(content=""),   # "" = don't check the body
        ),
        locations=[loc.name for loc in locations[:1]],     # names, not ids
        agreement=AGREEMENT_MAJORITY,  # "any" | "majority" | "all"
    ),
)
print("created:", monitor.id, monitor.locations, monitor.agreement)

try:
    # 4. Read it back, in the workspace listing and on its own.
    listed = client.v2.monitoring.websites.all(workspace.id)
    print("monitors here:", [m.name for m in listed])

    fetched = client.v2.monitoring.websites.get(monitor.id)
    print("fetched:", fetched.name, fetched.request.url, fetched.response.statuses)

    # 5. Update replaces the whole configuration — send every field you want to
    #    keep. There is no workspace_id: a monitor cannot change workspace. An
    #    omitted agreement keeps the stored one instead of resetting it.
    updated = client.v2.monitoring.websites.update(
        monitor.id,
        UpdateWebsiteMonitorRequest(
            name="Checkout API (health)",
            interval=120,
            request=WebsiteMonitorRequest(
                url="https://checkout.example/health",
                method="GET",
            ),
            response=WebsiteMonitorResponse(
                statuses=[200, 204],
                body=WebsiteMonitorResponseBody(content="ok"),
            ),
            locations=fetched.locations,   # replaces the stored list
        ),
    )
    print("updated:", updated.interval, updated.agreement)   # agreement survived

    # 6. What is wrong right now. Only open incidents come back, newest trouble
    #    first. A monitor created seconds ago has nothing open yet — this is the
    #    loop you would run against a workspace that has been up for a while.
    for incident in client.v2.incidents.all(workspace.id):
        note = " (nobody notified yet)" if incident.status == STATUS_PENDING else ""
        print(f"{incident.monitor_name}: {incident.status}{note}")
        print("  since   ", incident.trouble_since)
        print("  failing:", incident.locations.failing or "-")
        print("  unknown:", incident.locations.unknown or "-")

    # Narrow it to one monitor:
    mine = client.v2.incidents.all(workspace.id, monitor_id=monitor.id)
    print("open on this monitor:", len(mine))

except DefaultUptimerApiError as exc:
    # The API answered with an error envelope. Branch on .code — see Errors below.
    print("api error:", exc.code, exc.error_type, exc.message, exc.details)
finally:
    # 7. Clean up whatever happened above. Deleting the monitor removes its
    #    subject, signal, rule and history with it.
    deleted = client.v2.monitoring.websites.delete(monitor.id)
    print(deleted.message, deleted.monitor_id)

The rest of this page explains the pieces.

What is available

Resources live under the API version that serves them. The REST API is versioned by path, and the SDK keeps that version visible rather than hiding it: everything v2 offers is reached through client.v2, and there are no root-level aliases.

The types those calls take and return are versioned the same way — import them from uptimer.models.v2:

from uptimer.models.v2 import CreateWebsiteMonitorRequest, Incident, Location

They are not exported from uptimer.models, and there are no flat aliases, so a stale import fails loudly rather than binding to something else.

Two things stay off the version namespaces, because GET /version is shared by both API versions rather than belonging to either:

The deserialization exceptions (ModelError, TypeMismatchError, …) stay on uptimer.models for the same reason: the same error is raised whichever API version produced the payload.

Website monitoring sits under client.v2.monitoring because it is a built-in template, not the general monitor model.

Every model carries the API’s kind, and the SDK strips kind out of anything it sends: it is the server telling you what an object is, not a field you set.

Checking the server first

check_compatibility() reads /version — the one unversioned endpoint, so it works against a server too old for the rest of the SDK — and raises IncompatibleServerError if that server predates API v2. The bar is the SDK’s own major.minor: 1.5.x needs uptimer 1.5.0+ (or myuptime.info 15.1.0+). A server reporting something that is not a release number, such as a dev build from source, is treated as usable rather than locked out.

Locations and agreement

Assign locations with the locations field. It takes location names, as listed by client.v2.locations.all() — not ids. A monitor with none is never checked and stays at no data.

agreement is how many of those locations must report a problem before the monitor does: AGREEMENT_ANY, AGREEMENT_MAJORITY or AGREEMENT_ALL from uptimer.models.v2 (the wire values are "any", "majority" and "all").

Two things behave differently between CreateWebsiteMonitorRequest and UpdateWebsiteMonitorRequest:

Incident status

client.v2.incidents.all() returns only open incidents, newest trouble first. Pass monitor_id= to narrow it to one monitor. Closed incidents are history and live on the subject timeline in the dashboard; there is no incident-history endpoint in this release.

status carries the same words the dashboard shows — STATUS_OK, STATUS_PENDING, STATUS_PROBLEM, STATUS_NO_DATA, STATUS_RECOVERING. pending is the one to watch: the monitor is failing but still inside the confirm hold, so nobody has been notified yet.

incident.locations splits the evidence into .failing, .unknown and .ok. A location that has never reported stays in unknown and still counts toward the agreement — that is a real state, not a missing one.

Errors

All of these subclass UptimerError, in uptimer.errors:

Migrating from 0.4.x

0.4.x (API v1)1.5.0 (API v2)
client.v1.workspacesclient.v2.workspaces
client.v1.regionsclient.v2.locations
client.v1.rulesclient.v2.monitoring.websites
RegionLocation
Rule / CreateRuleRequestWebsiteMonitor / CreateWebsiteMonitorRequest
regions=[...]locations=[...]
agreement=..., client.v2.incidents
client.check_compatibility()
from uptimer.models import …from uptimer.models.v2 import …

The version namespace is the shape you already know. 0.4.x reached API v1 through client.v1; 1.5.x reaches API v2 through client.v2. What moves is the version, not the pattern — and the types moved with it: uptimer.models becomes uptimer.models.v2.

client.version() is unchanged and still sits on the client, not under client.v2/version is a shared global endpoint, not a versioned one. UptimerClient(api_key=…, base_url=…) and UptimerCloudClient(api_key=…) are constructed exactly as before, and both expose the same client.v2.

The rename is not only the namespace: regions=[...] becomes locations=[...], and what 0.4.x called a rule is a website monitor. What the server stores is the same object, so a monitor created with 0.4.x is the one 1.5.x reads back.

Source and releases on PyPI and GitHub.