Documentation
REST API
Manage your monitored domains programmatically. Available on Pro and Agency plans.
Authentication
All API requests require a Bearer token in the Authorization header.
Generate your API key from your Account settings.
Free plan accounts will receive a 403 Forbidden response. Upgrade to Pro or Agency for API access.
Base URL
Endpoints
Everything you can configure in the dashboard is available through the API. All endpoints are scoped to the account that owns the API key.
Domains
/domains
List all domains with SSL, registration and security summaries
/domains
Add one or more domains
/domains/:id
Full detail for one domain
/domains/:id
Update settings: paused, port, ssl_enabled, domain_expiry_enabled, uptime_enabled, dns_enabled, redirect_enabled, alert_thresholds
/domains/:id
Remove a domain and all its data
/domains/:id/check
Queue an immediate check
/domains/:id/verify
Start ownership verification (TXT record)
/domains/:id/acknowledge_downtime
Pause uptime re-alerts: duration_minutes of 30, 60, 240 or 1440
Monitoring data
/domains/:id/ssl_certificates
SSL certificate history (and /:id for one)
/domains/:id/check_logs
Raw check log entries
/domains/:id/security_scan
Latest security header / CAA / DNSSEC / CT scan
/domains/:id/uptime
Uptime status, 24h/7d/30d availability and recent checks
/domains/:id/dns
Current DNS snapshot and detected changes
/domains/:id/redirects
Latest redirect chain and history
Status pages
/domains/:id/status_page
Status page configuration
/domains/:id/status_page
Update: enabled, theme, logo_url, logo_link_url, custom_domain
/domains/:id/status_page/verify_custom_domain
Re-check the custom domain's CNAME record
/domains/:id/incidents
List incidents with their status updates
/domains/:id/incidents
Start an incident: title, plus optional first update body and kind (update, investigating, identified, monitoring)
/domains/:id/incidents/:id/status_updates
Post an update to an open incident: body and kind; a resolved kind also closes the incident
/domains/:id/incidents/:id/resolve
Resolve an incident, with an optional closing-message body
/domains/:id/incidents/:id/status_updates/:id
Remove an update
/domains/:id/incidents/:id
Remove an incident and its updates
Service monitors
/domains/:id/monitors
List a domain's heartbeat and metric monitors
/domains/:id/monitors
Create a monitor: kind heartbeat or metric
/domains/:id/monitors/:id
Monitor detail with its recent events
/domains/:id/monitors/:id
Update a monitor's configuration
/domains/:id/monitors/:id
Remove a monitor and its history
/domains/:id/monitors/:id/events
Record an event (authenticated alternative to the ping URL)
/domains/:id/monitors/:id/regenerate_token
Mint a fresh ping URL token
Alerting
/alert_rules
List notification channels (also GET /alert_rules/:id)
/alert_rules
Create a channel: email, slack, webhook, discord
/alert_rules/:id
Update a channel's name, enabled flag or config
/alert_rules/:id
Remove a channel
/alerts
List alerts; filter by type, severity, unacknowledged, limit
/alerts/:id/acknowledge
Mark an alert as handled
/alerts/:id/snooze
Silence an alert: hours (default 24, max 168)
Account
/account
Plan, limits and preferences
/account
Update: name, default_alert_thresholds, preferred_alert_hour, preferred_alert_timezone
/api/v1/domains
Returns all domains on your account, including their current SSL certificate status and domain registration details.
Example request
https://xpiry.com/api/v1/domains
Example response
{
"domains": [
{
"id": "abc-123",
"domain": "example.com",
"verified": true,
"verification_status": "verified",
"paused": false,
"last_checked_at": "2026-04-04T12:00:00Z",
"created_at": "2026-03-01T08:30:00Z",
"ssl": {
"status": "ok",
"issuer": "/O=Let's Encrypt/CN=R3",
"expires_at": "2026-07-01T00:00:00Z",
"days_until_expiry": 88,
"chain_valid": true
},
"domain_registration": {
"registrar": "Namecheap, Inc.",
"expires_at": "2027-03-01T00:00:00Z",
"days_until_expiry": 331
}
}
]
}
SSL status values
warning = expiry within 30 days, critical = within 7 days
/api/v1/domains
Add one or more domains to your account. Returns TXT record values you'll need to set on each domain for ownership verification.
Request body
| Parameter | Type | Description |
|---|---|---|
| domain | string | A single domain name |
| domains | array | Multiple domain names (use instead of domain) |
Example — single domain
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domain": "example.com"}' \
https://xpiry.com/api/v1/domains
Example — multiple domains
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["example.com", "example.org"]}' \
https://xpiry.com/api/v1/domains
Example response
{
"created": [
{
"id": "abc-123",
"domain": "example.com",
"verified": false,
"verification_status": "pending",
"verification": {
"txt_record_host": "_xpiry.example.com",
"txt_record_value": "xpiry-verify=a1b2c3d4e5f6..."
}
}
],
"errors": []
}
Verification: After creating a domain, add the TXT record shown in the response to your DNS.
The record should be set at _xpiry.<your-domain>
with the provided value. Then trigger verification from the dashboard or wait for automatic checking.
Response status codes
201 — All domains created successfully
207 — Some domains created, some failed (partial success)
422 — All domains failed to create
/api/v1/domains/:id
Permanently delete a domain and all its associated data (SSL certificates, check history, alerts).
Example request
-H "Authorization: Bearer YOUR_API_KEY" \
https://xpiry.com/api/v1/domains/DOMAIN_ID
Example response
{
"message": "Domain 'example.com' deleted."
}
/api/v1/domains/:id/status_page
Configure the public status page, including serving it on your own hostname.
Setting custom_domain starts verification;
the response includes the CNAME record to add at your DNS provider.
Example request
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status_page": {"enabled": true, "custom_domain": "status.example.com"}}' \
https://xpiry.com/api/v1/domains/DOMAIN_ID/status_page
Example response
{
"status_page": {
"enabled": true,
"slug": "example-com-1a2b3c4d",
"url": "https://xpiry.com/status/example-com-1a2b3c4d",
"theme": "dark",
"custom_domain": {
"hostname": "status.example.com",
"status": "pending",
"live": false,
"cname": {
"host": "status.example.com",
"target": "status.xpiry.dev"
}
}
},
"message": "Verification started. Add the CNAME record from custom_domain.cname to activate status.example.com."
}
Verification: add the CNAME record, then poll
GET /status_page until
custom_domain.status is
verified, or trigger a re-check with
POST /status_page/verify_custom_domain.
We keep checking automatically for 48 hours and email you if verification keeps failing.
Service monitors
Service monitors watch the things Xpiry can't reach from outside: cron jobs, queue workers, backups, internal metrics. There are two kinds.
expected_interval_seconds plus a grace window; a missed window flips the monitor to down. Report a run duration — or ping /start when the job begins and success when it ends, and Xpiry times it for you — to flag slow runs as degraded.value compared against your warn_threshold and critical_threshold in whichever direction is bad for that metric.Create and configure monitors with the Bearer API below. Your systems then report in through each monitor's tokenized ping URL. Service monitors are available on every plan, differing only by per-domain limit: 1 on Free, 3 on Hobby, 5 on Pro, and 20 on Agency.
Monitor status values
/ping/:token
The high-volume ingest path. Two credentials are required: the path token identifies the
monitor, and an Authorization: Bearer <API key> header authorizes the account that
owns it (the same keys this REST API uses). Each monitor has its own token; find the full URL in the dashboard or in the
ping_url field of the monitor API responses below. Both GET and POST work. Ingest is subject to a per-plan daily event budget; over it the endpoint returns 429.
Examples
Parameters
| Parameter | Type | Description |
|---|---|---|
| :state | path | Append /start to mark the beginning of a run (heartbeats only), or /fail to report a failure (e.g. /ping/:token/fail). The bare URL, or /ok, reports success and ends the run. |
| value | number | Metric monitors: the value to evaluate against the thresholds. Required for metric pings. |
| duration | number | Heartbeat run length in seconds. Runs over max_duration_seconds are marked degraded. Omit it if you ping /start first — Xpiry measures the run itself. |
| duration_ms | integer | Same as duration, in milliseconds (use one or the other). |
| error | string | Optional failure reason, stored with the event. |
Measuring run duration automatically
Ping /start when a job begins and the success URL when it finishes; Xpiry records the elapsed time server-side, so you don't have to time the job yourself. A /start is not a check-in on its own, so a job that starts but never finishes still trips the missed-heartbeat alert. A failure, or an orphaned /start with no matching finish, never produces a measured duration.
Responses
200 — event recorded (response body is OK)
403 — monitor disabled, or the plan no longer includes service monitors
404 — unknown token
422 — invalid value or state (body is the reason)
429 — over 60 pings per minute for the token
Example response
The ping URL returns plain text, not JSON.
# 200 — success OK # 422 — a metric ping with no value value is required for metric monitors
/api/v1/domains/:id/monitors
Create a heartbeat or metric monitor. The response includes the monitor's ping_url.
Heartbeats require expected_interval_seconds; metrics require at least one of
warn_threshold / critical_threshold.
Request body
Wrap the fields in a monitor object.
| Parameter | Type | Description |
|---|---|---|
| name | string | Required. Display name (max 120 chars). |
| kind | string | Required. heartbeat or metric. |
| component_group | string | Optional grouping label on the status page. |
| enabled | boolean | Default true. |
| show_on_status_page | boolean | Default true. |
| expected_interval_seconds | integer | Heartbeat, required. How often you expect a ping. |
| grace_period_seconds | integer | Heartbeat. Extra time before a missed ping is down. Default 60. |
| max_duration_seconds | integer | Heartbeat. Runs longer than this report as degraded. |
| unit | string | Metric. Label for the value, e.g. ms, jobs. |
| value_direction | string | Metric. up_bad (default; higher is worse) or down_bad. |
| warn_threshold | number | Metric. Crossing it marks the monitor degraded. |
| critical_threshold | number | Metric. Crossing it marks the monitor down. |
| stale_after_seconds | integer | Metric. Mark stale (down) if no value arrives within this window. |
| show_value_on_status_page | boolean | Metric. Show the latest value publicly. Default false. |
Example — heartbeat
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"monitor": {"name": "Nightly backup", "kind": "heartbeat", "expected_interval_seconds": 86400, "grace_period_seconds": 3600, "component_group": "Jobs"}}' \
https://xpiry.com/api/v1/domains/DOMAIN_ID/monitors
Example — metric
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"monitor": {"name": "Queue depth", "kind": "metric", "unit": "jobs", "value_direction": "up_bad", "warn_threshold": 100, "critical_threshold": 500}}' \
https://xpiry.com/api/v1/domains/DOMAIN_ID/monitors
Example response
{
"monitor": {
"id": "mon-789",
"name": "Nightly backup",
"kind": "heartbeat",
"status": "unknown",
"enabled": true,
"component_group": "Jobs",
"show_on_status_page": true,
"ping_url": "https://xpiry.com/ping/xpm_aB3dE4fG...",
"last_event_at": null,
"down_since": null,
"acknowledged_until": null,
"availability": { "last_24h": null, "last_7d": null, "last_30d": null },
"heartbeat": {
"expected_interval_seconds": 86400,
"grace_period_seconds": 3600,
"max_duration_seconds": null,
"last_duration_ms": null,
"start_url": "https://xpiry.com/ping/xpm_aB3dE4fG.../start"
},
"recent_events": []
}
}
Response status codes
201 — monitor created
403 — monitor limit reached for this domain on your plan
422 — validation failed (response includes a details array)
/api/v1/domains/:id/monitors
Lists a domain's monitors. Each carries a heartbeat or
metric block depending on its kind, plus 24h/7d/30d availability.
GET /monitors/:id returns the same shape with a
recent_events array (the last 20 pings).
Example response
{
"domain_id": "abc-123",
"monitors": [
{
"id": "mon-456",
"name": "Queue depth",
"kind": "metric",
"status": "up",
"enabled": true,
"component_group": "Workers",
"show_on_status_page": true,
"ping_url": "https://xpiry.com/ping/xpm_aB3dE4fG...",
"last_event_at": "2026-06-16T11:59:00Z",
"down_since": null,
"acknowledged_until": null,
"availability": { "last_24h": 100.0, "last_7d": 99.8, "last_30d": 99.9 },
"metric": {
"unit": "jobs",
"value_direction": "up_bad",
"warn_threshold": 100.0,
"critical_threshold": 500.0,
"stale_after_seconds": null,
"last_value": 42.0,
"show_value_on_status_page": true
}
}
]
}
/api/v1/domains/:id/monitors/:id/events
Record an event over the authenticated API instead of the public ping URL: useful for clients already
holding a Bearer token. Accepts the same fields as the ping URL: state
(ok/fail), value,
duration_ms, error. Returns the updated monitor.
Example — push a metric value
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"value": 87}' \
https://xpiry.com/api/v1/domains/DOMAIN_ID/monitors/MONITOR_ID/events
Request body
| Parameter | Type | Description |
|---|---|---|
| state | string | ok (default) or fail. |
| value | number | Metric monitors: required. |
| duration_ms | integer | Heartbeat run length in milliseconds. |
| error | string | Optional failure reason. |
Example response
Returns the monitor with its new status and value (no recent_events block).
{
"monitor": {
"id": "mon-456",
"name": "Queue depth",
"kind": "metric",
"status": "up",
"enabled": true,
"component_group": "Workers",
"show_on_status_page": true,
"ping_url": "https://xpiry.com/ping/xpm_aB3dE4fG...",
"last_event_at": "2026-06-16T12:00:00Z",
"down_since": null,
"acknowledged_until": null,
"availability": { "last_24h": 100.0, "last_7d": 99.8, "last_30d": 99.9 },
"metric": {
"unit": "jobs",
"value_direction": "up_bad",
"warn_threshold": 100.0,
"critical_threshold": 500.0,
"stale_after_seconds": null,
"last_value": 87.0,
"show_value_on_status_page": true
}
}
}
Rotating a token:
POST /monitors/:id/regenerate_token
mints a fresh ping URL and returns the monitor with the new ping_url.
The previous URL stops working immediately, so update your cron jobs and scripts.
/ping/:token/metrics
Push a batch of labeled metric samples into a monitor: the data behind the query
builder and custom dashboards. Same credentials as the ping URL (path token plus an
Authorization: Bearer key). Unlike a metric ping, these samples are pure data:
they carry a series name and optional
labels, and never change the monitor's up/down status. Ingest counts
against the same per-plan daily event budget (each point in the batch counts as one); over it the endpoint returns
429.
Example: a batch of labeled points
curl -X POST \ -H "Authorization: Bearer $XPIRY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"points": [ {"name": "latency_ms", "value": 42.5, "labels": {"region": "us-east"}}, {"name": "latency_ms", "value": 51.0, "labels": {"region": "eu-west"}}, {"name": "queue_depth", "value": 12} ]}' \ https://xpiry.com/ping/xpm_YOUR_TOKEN/metrics
Each point
| Field | Type | Description |
|---|---|---|
| name | string | Required. The series name, e.g. latency_ms. |
| value | number | Required. The numeric sample. |
| labels | object | Optional flat map of string keys to string values, e.g. {"region": "us-east"}. The dimensions you group and filter by. Up to 20 per point. |
| at | string | Optional ISO 8601 timestamp; defaults to the time of ingest. |
A single point may also be sent as top-level form params
(?name=latency_ms&value=42.5). Up to 1000 points per request. On success the
body is OK <count>. An existing metric monitor's threshold value is recorded
as the value series automatically, so you can chart it without any code changes.
Error responses
All errors return a JSON object with an error key.
| Status | Description |
|---|---|
| 401 Unauthorized | Missing or invalid API key |
| 403 Forbidden | Account does not have API access (free plan) |
| 404 Not Found | Domain not found or does not belong to your account |
| 422 Unprocessable | Invalid request (bad domain name, limit reached, etc.) |
Example error
{
"error": "Invalid API key."
}
Rate limits
API requests are limited to 60 requests per minute per API key.
If you exceed this limit, you'll receive a 429 Too Many Requests response.