Endpoint Cards
/healthPublic load-balancer health check.
curl http://127.0.0.1:8000/health{"status":"ok","service":"pagerpal","version":"0.1.0"}/api/v1/system/jobsRetry and escalation worker status.
curl -u '<account-email>:<account-password>' http://127.0.0.1:8000/api/v1/system/jobs{"jobs":[{"name":"notification_retry","enabled":true}]}/api/v1/alert-sourcesCreate a monitored alert source.
curl -u '<admin-email>:<admin-password>' -X POST http://127.0.0.1:8000/api/v1/alert-sources{"id":1,"masked_api_key":"abc12345...wxyz"}/api/v1/alert-sourcesList alert sources with masked keys.
curl -u '<account-email>:<account-password>' http://127.0.0.1:8000/api/v1/alert-sources[{"id":1,"source_type":"grafana","is_active":true}]/api/v1/alert-sources/{source_id}/regenerate-keyRotate a source key.
curl -u '<admin-email>:<admin-password>' -X POST http://127.0.0.1:8000/api/v1/alert-sources/1/regenerate-key{"id":1,"api_key":"<revealed-once-alert-source-key>"}/api/v1/alertsGeneric alert ingestion.
curl -X POST http://127.0.0.1:8000/api/v1/alerts -H 'X-API-Key: <alert-source-key>'{"id":42,"status":"triggered","severity":"critical"}/api/v1/webhooks/grafanaGrafana alerting webhook.
curl -X POST http://127.0.0.1:8000/api/v1/webhooks/grafana -H 'X-API-Key: <alert-source-key>'{"id":42,"external_id":"monitoring-rule-id"}/api/v1/webhooks/cloudwatchCloudWatch/SNS webhook.
curl -X POST http://127.0.0.1:8000/api/v1/webhooks/cloudwatch -H 'X-API-Key: <alert-source-key>'{"status":"subscription_confirmed"}/api/v1/incidentsPaginated incident list.
curl -u '<account-email>:<account-password>' http://127.0.0.1:8000/api/v1/incidents{"items":[],"total":125,"page":1,"total_pages":3}/api/v1/incidents/{incident_id}Incident detail.
curl -u '<account-email>:<account-password>' http://127.0.0.1:8000/api/v1/incidents/42{"id":42,"status":"triggered","notification_count":0}/api/v1/incidents/{incident_id}/ackAcknowledge an incident.
curl -u '<account-email>:<account-password>' -X POST http://127.0.0.1:8000/api/v1/incidents/42/ack{"id":42,"status":"acknowledged","acknowledged_by":1}/api/v1/incidents/{incident_id}/resolveResolve an incident.
curl -u '<account-email>:<account-password>' -X POST http://127.0.0.1:8000/api/v1/incidents/42/resolve{"id":42,"status":"resolved","resolved_by":1}/api/v1/incidents/{incident_id}/reopenReopen a resolved incident.
curl -u '<account-email>:<account-password>' -X POST http://127.0.0.1:8000/api/v1/incidents/42/reopen{"id":42,"status":"triggered"}/api/v1/incidents/{incident_id}/escalateManually escalate an incident.
curl -u '<account-email>:<account-password>' -X POST http://127.0.0.1:8000/api/v1/incidents/42/escalate{"id":42,"current_escalation_level":2}/api/v1/incidents/{incident_id}/eventsRead incident timeline events.
curl -u '<account-email>:<account-password>' http://127.0.0.1:8000/api/v1/incidents/42/events[{"event_type":"acknowledged","message":"Incident acknowledged"}]/api/v1/incidents/{incident_id}/notification-logsRead notification logs.
curl -u '<account-email>:<account-password>' http://127.0.0.1:8000/api/v1/incidents/42/notification-logs[{"status":"sent","channel":"sms"}]/api/v1/incidents/{incident_id}/notification-logs/retryRetry failed notifications.
curl -u '<account-email>:<account-password>' -X POST http://127.0.0.1:8000/api/v1/incidents/42/notification-logs/retry{"retried_count":1,"queued_count":1}PagerPal API Reference#
PagerPal exposes three API groups:
- Operator/management API, used by the UI and administrative tooling.
- Incident API, used for incident reads/actions and wrapped by the server-rendered UI.
- Alert ingestion API, used by monitoring systems to create/update/resolve incidents.
The OpenAPI browser is available at /docs when the application is running, but it is not a replacement for the security notes below.
Authentication summary#
| Endpoint group | Auth required | Credential |
|---|---|---|
/health |
No | None |
/login, /logout, /bootstrap/admin |
Yes for logout only | Signed session cookie after login |
/api/v1/auth/password |
Yes | User Account session or Basic auth |
/api/v1/users* |
Yes | User Account session or Basic auth; writes require admin |
/api/v1/teams* |
Yes | User Account session or Basic auth; writes require admin |
/api/v1/schedules* |
Yes | User Account session or Basic auth; writes require admin |
/api/v1/escalation-policies* |
Yes | User Account session or Basic auth; writes require admin |
/api/v1/alert-sources* |
Yes | User Account session or Basic auth; writes require admin |
/api/v1/system/jobs |
Yes | User Account session or Basic auth |
/api/v1/incidents* |
Yes | Reads require any account; actions require responder or admin |
/api/v1/alerts |
Yes | Alert source API key |
/api/v1/webhooks/grafana |
Yes | Alert source API key |
/api/v1/webhooks/cloudwatch |
Yes | Alert source API key |
/api/v1/notifications/infobip/receipts |
Conditional | X-Infobip-Receipt-Token when configured; required in production |
Note: API examples use placeholders only. Never commit or paste real API keys, passwords, tokens, or connection strings.
Operator API examples#
Management API routes accept HTTP Basic auth against real User Accounts. Use the account email as the username:
curl -u '<account-email>:<account-password>' \
http://127.0.0.1:8000/api/v1/teams
The server-rendered UI uses /login to issue a signed pagerpal_session cookie. API Basic auth is retained for scripts and smoke checks, but it no longer uses shared process credentials.
Health check#
GET /health
Response:
{
"status": "ok",
"service": "pagerpal",
"version": "0.1.0"
}
System worker status#
GET /api/v1/system/jobs
Authorization: Basic <base64-account-credentials>
Use this to confirm retry/escalation worker status from automation or deployment checks.
Account and password routes#
First-run admin bootstrap is exposed through the /login page when no login-enabled admin exists. For automation, use:
python scripts/create_admin.py --email '<admin-email>'
Change the current account password:
POST /api/v1/auth/password
Authorization: Basic <base64-account-credentials>
Content-Type: application/json
{
"current_password": "old-password",
"new_password": "new-long-password"
}
User create/update responses include role and last_login_at; password hashes are never returned. Creating a responder without a password remains valid and creates a responder-only identity.
Alert sources#
Create an alert source:
curl -u '<admin-email>:<admin-password>' \
-X POST http://127.0.0.1:8000/api/v1/alert-sources \
-H 'Content-Type: application/json' \
-d '{
"team_id": 1,
"name": "Production Grafana",
"source_type": "grafana",
"config": null
}'
Supported source_type values are generic, grafana, and cloudwatch. Generic sources send alerts to /api/v1/alerts; Grafana and CloudWatch sources use /api/v1/webhooks/grafana and /api/v1/webhooks/cloudwatch.
List alert sources:
curl -u '<account-email>:<account-password>' \
http://127.0.0.1:8000/api/v1/alert-sources
Create and regenerate responses reveal the full api_key once so an operator can configure the monitoring system. Default list/get responses expose masked_api_key, not the full webhook secret:
[
{
"id": 1,
"team_id": 1,
"name": "Production Grafana",
"source_type": "grafana",
"config": null,
"masked_api_key": "abc12345…wxyz",
"is_active": true,
"created_at": "2026-05-21T12:00:00"
}
]
Regenerate a source key if a key is lost or suspected compromised:
curl -u '<admin-email>:<admin-password>' \
-X POST http://127.0.0.1:8000/api/v1/alert-sources/1/regenerate-key
After regeneration, update the monitoring system with the new key.
The UI also includes a Send test alert action on /alert-sources. It creates a critical test incident using the selected source so operators can verify routing and notification logs without configuring an external monitoring system first.
Alert ingestion authentication#
Webhook endpoints accept an active alert source API key from any of these locations:
| Location | Example |
|---|---|
X-API-Key header |
X-API-Key: <alert-source-key> |
X-Alert-Source-Key header |
X-Alert-Source-Key: <alert-source-key> |
| Bearer token | Authorization: Bearer <alert-source-key> |
| Query string | ?api_key=<alert-source-key> or ?token=<alert-source-key> |
Prefer headers. Query-string keys often end up in web server logs, shell history, and screenshots.
Infobip delivery receipts#
Infobip delivery reports can update existing NotificationLog rows by messageId:
POST /api/v1/notifications/infobip/receipts
X-Infobip-Receipt-Token: <receipt-token>
Content-Type: application/json
{
"results": [
{
"messageId": "infobip-message-id",
"status": {
"name": "DELIVERED",
"description": "Delivered to handset"
}
}
]
}
DELIVERED marks the outbox row delivered; UNDELIVERABLE, EXPIRED, REJECTED, and FAILED mark it failed. Production deployments should set INFOBIP_RECEIPT_TOKEN and configure Infobip to send the same value as X-Infobip-Receipt-Token.
Generic alert ingestion#
POST /api/v1/alerts
X-API-Key: <alert-source-key>
Content-Type: application/json
Request body:
{
"title": "CPU > 95% on prod-api-03",
"description": "CPU has been above 95% for 10 minutes",
"severity": "critical",
"team_id": 1,
"external_id": "prod-api-03-cpu-high",
"source_id": 1
}
severity must be one of:
criticalwarninginfo
external_id is important for deduplication. Repeated alerts with the same authenticated source and external ID update the open incident instead of creating a new duplicate incident.
Example:
curl -X POST http://127.0.0.1:8000/api/v1/alerts \
-H 'Content-Type: application/json' \
-H 'X-API-Key: <alert-source-key>' \
-d '{
"title": "CPU > 95% on prod-api-03",
"description": "CPU has been above 95% for 10 minutes",
"severity": "critical",
"team_id": 1,
"external_id": "prod-api-03-cpu-high",
"source_id": 1
}'
Successful response shape:
{
"id": 42,
"title": "CPU > 95% on prod-api-03",
"description": "CPU has been above 95% for 10 minutes",
"severity": "critical",
"external_id": "prod-api-03-cpu-high",
"source_id": 1,
"team_id": 1,
"status": "triggered",
"current_escalation_level": 0,
"assigned_user_id": 1,
"acknowledged_by": null,
"acknowledged_at": null,
"resolved_by": null,
"resolved_at": null,
"first_notified_at": null,
"last_notified_at": null,
"notification_count": 0,
"created_at": "2026-05-21T12:00:00"
}
Grafana webhook#
POST /api/v1/webhooks/grafana
X-API-Key: <alert-source-key>
Content-Type: application/json
Example alerting payload:
{
"title": "RDS connection pool exhausted",
"message": "Database connection pool is saturated",
"state": "alerting",
"ruleId": "rds-connection-pool",
"labels": {
"team_id": "1"
}
}
PagerPal uses provider IDs/labels to derive a stable external ID. state: ok resolves the matching open incident when the alert source and external ID match.
CloudWatch/SNS webhook#
POST /api/v1/webhooks/cloudwatch
X-API-Key: <alert-source-key>
Content-Type: application/json
PagerPal accepts SNS SubscriptionConfirmation and Notification payloads. Confirmation messages must include an HTTPS AWS SNS SubscribeURL; PagerPal validates the host, does not follow redirects, fetches the URL, and returns:
{
"status": "subscription_confirmed"
}
Notification messages are expected to include Message as a JSON-encoded CloudWatch alarm message.
Minimal example:
{
"Type": "Notification",
"Message": "{\"AlarmName\":\"prod-api-cpu-high\",\"NewStateValue\":\"ALARM\",\"NewStateReason\":\"CPU above threshold\",\"Trigger\":{}}",
"MessageId": "example-message-id"
}
NewStateValue: ALARM creates or updates an incident. NewStateValue: OK resolves the matching open incident.
Incident API#
GET /api/v1/incidents supports:
| Query parameter | Default | Notes |
|---|---|---|
page |
1 |
1-based page number. |
page_size |
50 |
Capped at 200. |
team_id |
none | Optional filter. |
status |
none | Optional filter, for example triggered or resolved. |
severity |
none | Optional filter, for example critical. |
source_id |
none | Optional filter. |
List response shape:
{
"items": [],
"total": 125,
"page": 1,
"page_size": 50,
"total_pages": 3
}
Core incident read/action endpoints:
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/incidents |
List incidents. |
GET |
/api/v1/incidents/{incident_id} |
Get incident detail. |
POST |
/api/v1/incidents/{incident_id}/ack |
Acknowledge incident. |
POST |
/api/v1/incidents/{incident_id}/resolve |
Resolve incident. |
POST |
/api/v1/incidents/{incident_id}/reopen |
Reopen a resolved incident. |
POST |
/api/v1/incidents/{incident_id}/escalate |
Manually escalate incident. |
GET |
/api/v1/incidents/{incident_id}/events |
Read timeline events. |
GET |
/api/v1/incidents/{incident_id}/notification-logs |
Read notification logs. |
POST |
/api/v1/incidents/{incident_id}/notification-logs/retry |
Retry failed/exhausted notifications. |
Acknowledge, resolve, reopen, and manual escalation actions require a user_id body:
{
"user_id": 1
}
The server-rendered UI wraps these actions with CSRF protection and a visible Acting as selector.
Schedule entries#
POST /api/v1/schedules/{schedule_id}/entries requires user_id to reference an active user and rejects normal entries whose time window overlaps another entry on the same schedule. Send is_override: true when the entry is intended to cover an existing window; override entries may overlap and are preferred by the current on-call lookup.
Route inventory#
Management resources#
| Resource | Base path |
|---|---|
| Users | /api/v1/users |
| Teams | /api/v1/teams |
| Team members | /api/v1/teams/{team_id}/members |
| Current team on-call | /api/v1/teams/{team_id}/oncall |
| Schedules | /api/v1/schedules |
| Schedule entries | /api/v1/schedules/{schedule_id}/entries |
| Schedule timeline | /api/v1/schedules/{schedule_id}/timeline |
| Escalation policies | /api/v1/escalation-policies |
| Escalation levels | /api/v1/escalation-policies/{policy_id}/levels |
| Escalation level assignees | /api/v1/escalation-policies/{policy_id}/levels/{level_id}/assignees |
| Alert sources | /api/v1/alert-sources |
| Worker status | /api/v1/system/jobs |
| Account password | /api/v1/auth/password |
| Infobip delivery receipts | /api/v1/notifications/infobip/receipts |
Team member creation requires user_id to reference an active user. Existing inactive memberships stay visible for history/audit and can be removed.
Escalation level numbers must be unique within a policy. Duplicate level numbers return 409 because they make paging order ambiguous. Direct user assignees must reference active users; inactive user assignees return 400.
UI routes#
UI routes use a signed session cookie issued by /login; unauthenticated UI requests redirect to /login.
| Path | Purpose |
|---|---|
/login |
User Account login; first-run admin bootstrap when needed. |
/logout |
Clear the UI session cookie. |
/ |
Redirect-equivalent dashboard entry route. |
/dashboard |
Operational landing page and active incidents. |
/dashboard/live |
HTMX live dashboard partial. |
/_partials/gstrip |
Global active incident strip partial. |
/on-call |
Current on-call coverage view. |
/incidents |
Incident list. |
/incidents/{incident_id} |
Incident detail, timeline, notification logs, and actions. |
/teams |
Teams and team creation. |
/teams/{team_id} |
Team detail and member management. |
/schedules |
On-call schedules. |
/schedules/{schedule_id} |
Schedule entries. |
/escalation-policies |
Escalation policies. |
/escalation-policies/{policy_id} |
Escalation policy levels. |
/alert-sources |
Alert source setup, key regeneration, and test-alert trigger. |
/settings |
Safe config and worker health view. |
Configuration form posts under teams, users, schedules, escalation policies, and alert sources require admin. Incident action posts require responder or admin. Read routes accept any active login-enabled account.
Error handling notes#
Common status codes:
| Status | Meaning |
|---|---|
200 / 201 |
Request succeeded. |
204 |
Delete succeeded; no response body. |
400 |
Bad request or invalid state transition. |
401 |
Missing/invalid account credentials or missing alert source API key. |
403 |
Role is not allowed, or alert source key is invalid/inactive. |
404 |
Resource not found. |
409 |
Conflict, such as overlapping non-override schedule entries or duplicate escalation level numbers. |
422 |
Request body did not match the expected JSON schema. |
For browser forms, use UI routes rather than posting raw HTML form data directly to JSON API routes. The UI routes translate form submissions into application/API commands and redirect/render HTML responses.