Official Python SDK for Openstatus — the open-source status page and uptime monitoring platform.
Status: pre-alpha. APIs may change without notice until v1.0.
- Features
- Installation
- Quick start
- Authentication
- Sync and async
- Custom HTTP client
- Services
- Reference
- Error handling
- Recipes
- Migration from the Node SDK
- Development
- License
- Sync and async clients —
OpenstatusClientandAsyncOpenstatusClient, both backed byhttpx. - Typed messages — request and response types are generated from the
upstream protobuf schema, with
.pyistubs for editor and pyright support. - All services covered — Health, Monitor, StatusReport, StatusPage, Maintenance, and Notification.
- JSON over HTTP — uses Connect-RPC's JSON mode; no binary protobuf required at runtime.
- Predictable errors — Connect-style error envelopes are mapped to a typed exception hierarchy.
pip install openstatus
# or
uv add openstatusRequires Python 3.10 or newer.
from openstatus import OpenstatusClient
from openstatus._gen.openstatus.monitor.v1.assertions_pb2 import (
NumberComparator,
StatusCodeAssertion,
)
from openstatus._gen.openstatus.monitor.v1.http_monitor_pb2 import (
HTTPMethod,
HTTPMonitor,
)
from openstatus._gen.openstatus.monitor.v1.monitor_pb2 import Periodicity, Region
from openstatus._gen.openstatus.monitor.v1.service_pb2 import (
CreateHTTPMonitorRequest,
ListMonitorsRequest,
)
with OpenstatusClient() as client:
created = client.monitor.v1.MonitorService.create_http_monitor(
CreateHTTPMonitorRequest(
monitor=HTTPMonitor(
name="My API",
url="https://api.example.com/health",
method=HTTPMethod.HTTP_METHOD_GET,
periodicity=Periodicity.PERIODICITY_1M,
regions=[Region.REGION_FLY_AMS, Region.REGION_FLY_IAD],
active=True,
status_code_assertions=[
StatusCodeAssertion(
comparator=NumberComparator.NUMBER_COMPARATOR_EQUAL,
target=200,
)
],
)
)
)
print(f"Created monitor id={created.monitor.id}")
monitors = client.monitor.v1.MonitorService.list_monitors(ListMonitorsRequest())
print(f"Total monitors: {monitors.total_size}")The SDK reads OPENSTATUS_API_KEY from the environment by default:
export OPENSTATUS_API_KEY="..."Or pass it explicitly:
from openstatus import ClientOptions, OpenstatusClient
client = OpenstatusClient(ClientOptions(api_key="..."))Override the base URL with OPENSTATUS_API_URL or ClientOptions(base_url=...).
The default is https://api.openstatus.dev.
Both clients expose the same nested namespace path
(client.<service>.v1.<ServiceName>.<method>):
from openstatus import OpenstatusClient
from openstatus._gen.openstatus.health.v1.health_pb2 import CheckRequest
with OpenstatusClient() as client:
health = client.health.v1.HealthService.check(CheckRequest())import asyncio
from openstatus import AsyncOpenstatusClient
from openstatus._gen.openstatus.health.v1.health_pb2 import CheckRequest
async def main() -> None:
async with AsyncOpenstatusClient() as client:
health = await client.health.v1.HealthService.check(CheckRequest())
print(health.status)
asyncio.run(main())Supply your own httpx.Client to control timeouts, transports, proxies, or
TLS configuration:
import httpx
from openstatus import ClientOptions, OpenstatusClient
http = httpx.Client(
timeout=httpx.Timeout(connect=2.0, read=10.0, write=10.0, pool=10.0),
transport=httpx.HTTPTransport(retries=3),
)
client = OpenstatusClient(ClientOptions(http_client=http))When you pass a client in, the SDK does not close it; manage its lifetime yourself.
Each method takes a typed protobuf request and returns a typed protobuf
response. Pass per-call headers via the keyword-only headers= argument.
from openstatus._gen.openstatus.health.v1.health_pb2 import CheckRequest
health = client.health.v1.HealthService.check(CheckRequest())from openstatus._gen.openstatus.monitor.v1.service_pb2 import (
GetMonitorRequest,
ListMonitorsRequest,
TriggerMonitorRequest,
)
client.monitor.v1.MonitorService.list_monitors(ListMonitorsRequest())
client.monitor.v1.MonitorService.get_monitor(GetMonitorRequest(id="abc"))
client.monitor.v1.MonitorService.trigger_monitor(TriggerMonitorRequest(id="abc"))from openstatus._gen.openstatus.status_report.v1.service_pb2 import ListStatusReportsRequest
client.status_report.v1.StatusReportService.list_status_reports(ListStatusReportsRequest())from openstatus._gen.openstatus.status_page.v1.service_pb2 import ListStatusPagesRequest
client.status_page.v1.StatusPageService.list_status_pages(ListStatusPagesRequest())from openstatus._gen.openstatus.maintenance.v1.service_pb2 import ListMaintenancesRequest
client.maintenance.v1.MaintenanceService.list_maintenances(ListMaintenancesRequest())from openstatus._gen.openstatus.notification.v1.service_pb2 import ListNotificationsRequest
client.notification.v1.NotificationService.list_notifications(ListNotificationsRequest())Region constants live in openstatus._gen.openstatus.monitor.v1.monitor_pb2
as Region. Use the descriptor API to enumerate them:
from openstatus._gen.openstatus.monitor.v1.monitor_pb2 import Region
print(list(Region.keys())) # ["REGION_UNSPECIFIED", "REGION_FLY_AMS", ...]
print(Region.Name(1)) # "REGION_FLY_AMS"
print(Region.Value("REGION_FLY_AMS"))Common enums:
HTTPMethod(openstatus.monitor.v1.http_monitor_pb2) — request method for HTTP monitors.Periodicity(openstatus.monitor.v1.monitor_pb2) — check frequency.MonitorStatus,Region(openstatus.monitor.v1.monitor_pb2).NumberComparator,StringComparator,RecordComparator(openstatus.monitor.v1.assertions_pb2) — assertion operators.HTTPResponseLogRequestStatus,HTTPResponseLogTrigger,TimeRange(openstatus.monitor.v1.service_pb2).
All transport errors derive from OpenstatusError:
from openstatus import (
AuthenticationError,
NotFoundError,
OpenstatusClient,
OpenstatusError,
)
from openstatus._gen.openstatus.monitor.v1.service_pb2 import GetMonitorRequest
with OpenstatusClient() as client:
try:
client.monitor.v1.MonitorService.get_monitor(GetMonitorRequest(id="missing"))
except NotFoundError as err:
print("monitor not found:", err.connect_code, err.http_status)
except AuthenticationError:
print("check OPENSTATUS_API_KEY")
except OpenstatusError as err:
print("openstatus failed:", err.connect_code, err.details)Each exception carries connect_code, http_status, details, and
raw_body attributes for inspection.
| Connect code | Exception |
|---|---|
unauthenticated |
AuthenticationError |
not_found |
NotFoundError |
invalid_argument |
InvalidArgumentError |
permission_denied |
PermissionDeniedError |
resource_exhausted |
RateLimitError |
unavailable |
ServiceUnavailableError |
| anything else | OpenstatusError |
When the server response is not a Connect error envelope (e.g. a 502 HTML
body from a proxy), ServiceUnavailableError is raised with the raw body
preserved on .raw_body.
from fastapi import Depends, FastAPI
from openstatus import OpenstatusClient
app = FastAPI()
_client = OpenstatusClient()
def get_client() -> OpenstatusClient:
return _client
@app.on_event("shutdown")
def shutdown() -> None:
_client.close()
@app.get("/monitors")
def monitors(client: OpenstatusClient = Depends(get_client)):
from openstatus._gen.openstatus.monitor.v1.service_pb2 import ListMonitorsRequest
res = client.monitor.v1.MonitorService.list_monitors(ListMonitorsRequest())
return {"total": res.total_size}# myapp/openstatus.py
from django.conf import settings
from openstatus import ClientOptions, OpenstatusClient
_singleton: OpenstatusClient | None = None
def client() -> OpenstatusClient:
global _singleton
if _singleton is None:
_singleton = OpenstatusClient(ClientOptions(api_key=settings.OPENSTATUS_API_KEY))
return _singleton- Method names:
MonitorService.listMonitors(...)→MonitorService.list_monitors(...). - Message field access:
monitor.fooBar→monitor.foo_bar. - Enum lookups:
Region[value](Node) →Region.Name(value)(Python). - Headers per call: pass as keyword argument
headers={...}. - Authentication: same
OPENSTATUS_API_KEYenv var andClientOptions(api_key=...)parameter.
uv sync
uv run pytest tests/unit
uv run pyright
uv run ruff check .Integration tests against the live API:
OPENSTATUS_API_KEY=... uv run pytest tests/integrationRegenerating the protobuf message classes:
bash scripts/regen.shSee CONTRIBUTING.md for details.
For the design rationale (why JSON over HTTP, why a pinned Buf archive, why sync+async from day one), see docs/decisions.md.
MIT — see LICENSE.