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.

Authorization: Bearer your_api_key_here

Free plan accounts will receive a 403 Forbidden response. Upgrade to Pro or Agency for API access.

Base URL

https://xpiry.com/api/v1

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

GET /domains List all domains with SSL, registration and security summaries
POST /domains Add one or more domains
GET /domains/:id Full detail for one domain
PATCH /domains/:id Update settings: paused, port, ssl_enabled, domain_expiry_enabled, uptime_enabled, dns_enabled, redirect_enabled, alert_thresholds
DELETE /domains/:id Remove a domain and all its data
POST /domains/:id/check Queue an immediate check
POST /domains/:id/verify Start ownership verification (TXT record)
POST /domains/:id/acknowledge_downtime Pause uptime re-alerts: duration_minutes of 30, 60, 240 or 1440

Monitoring data

GET /domains/:id/ssl_certificates SSL certificate history (and /:id for one)
GET /domains/:id/check_logs Raw check log entries
GET /domains/:id/security_scan Latest security header / CAA / DNSSEC / CT scan
GET /domains/:id/uptime Uptime status, 24h/7d/30d availability and recent checks
GET /domains/:id/dns Current DNS snapshot and detected changes
GET /domains/:id/redirects Latest redirect chain and history

Status pages

GET /domains/:id/status_page Status page configuration
PATCH /domains/:id/status_page Update: enabled, theme, logo_url, logo_link_url, custom_domain
POST /domains/:id/status_page/verify_custom_domain Re-check the custom domain's CNAME record
GET /domains/:id/incidents List incidents with their status updates
POST /domains/:id/incidents Start an incident: title, plus optional first update body and kind (update, investigating, identified, monitoring)
POST /domains/:id/incidents/:id/status_updates Post an update to an open incident: body and kind; a resolved kind also closes the incident
POST /domains/:id/incidents/:id/resolve Resolve an incident, with an optional closing-message body
DELETE /domains/:id/incidents/:id/status_updates/:id Remove an update
DELETE /domains/:id/incidents/:id Remove an incident and its updates

Service monitors

GET /domains/:id/monitors List a domain's heartbeat and metric monitors
POST /domains/:id/monitors Create a monitor: kind heartbeat or metric
GET /domains/:id/monitors/:id Monitor detail with its recent events
PATCH /domains/:id/monitors/:id Update a monitor's configuration
DELETE /domains/:id/monitors/:id Remove a monitor and its history
POST /domains/:id/monitors/:id/events Record an event (authenticated alternative to the ping URL)
POST /domains/:id/monitors/:id/regenerate_token Mint a fresh ping URL token

Alerting

GET /alert_rules List notification channels (also GET /alert_rules/:id)
POST /alert_rules Create a channel: email, slack, webhook, discord
PATCH /alert_rules/:id Update a channel's name, enabled flag or config
DELETE /alert_rules/:id Remove a channel
GET /alerts List alerts; filter by type, severity, unacknowledged, limit
POST /alerts/:id/acknowledge Mark an alert as handled
POST /alerts/:id/snooze Silence an alert: hours (default 24, max 168)

Account

GET /account Plan, limits and preferences
PATCH /account Update: name, default_alert_thresholds, preferred_alert_hour, preferred_alert_timezone
GET /api/v1/domains

Returns all domains on your account, including their current SSL certificate status and domain registration details.

Example request

curl -H "Authorization: Bearer YOUR_API_KEY" \
  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

ok warning critical expired

warning = expiry within 30 days, critical = within 7 days

POST /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

curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "example.com"}' \
  https://xpiry.com/api/v1/domains

Example — multiple domains

curl -X POST \
  -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

DELETE /api/v1/domains/:id

Permanently delete a domain and all its associated data (SSL certificates, check history, alerts).

Example request

curl -X DELETE \
  -H "Authorization: Bearer YOUR_API_KEY" \
  https://xpiry.com/api/v1/domains/DOMAIN_ID

Example response

{
  "message": "Domain 'example.com' deleted."
}
PATCH /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

curl -X PATCH \
  -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.

Heartbeat
Expects a ping on a schedule. Set 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.
Metric
Watches a number you push. Each ping carries a 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

unknown up degraded down
GET POST /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

# the token (URL) and an API key are both required; set the key once
export XPIRY_API_KEY=xprk_…
# heartbeat — report a successful run
curl -H "Authorization: Bearer $XPIRY_API_KEY" https://xpiry.com/ping/xpm_YOUR_TOKEN
# heartbeat — also report how long the run took (seconds)
curl -H "Authorization: Bearer $XPIRY_API_KEY" "https://xpiry.com/ping/xpm_YOUR_TOKEN?duration=12.5"
# heartbeat — let Xpiry time the run: /start at the beginning, success at the end
curl -H "Authorization: Bearer $XPIRY_API_KEY" https://xpiry.com/ping/xpm_YOUR_TOKEN/start && /opt/job.sh && curl -H "Authorization: Bearer $XPIRY_API_KEY" https://xpiry.com/ping/xpm_YOUR_TOKEN
# report a failure, with an optional reason
curl -H "Authorization: Bearer $XPIRY_API_KEY" "https://xpiry.com/ping/xpm_YOUR_TOKEN/fail?error=OOMKilled"
# metric — push a value to compare against thresholds
curl -H "Authorization: Bearer $XPIRY_API_KEY" "https://xpiry.com/ping/xpm_YOUR_TOKEN?value=42"
# in crontab: only pings on success, so a failed/never-run job alerts
0 3 * * * /opt/backup.sh && curl -fsS -H "Authorization: Bearer $XPIRY_API_KEY" https://xpiry.com/ping/xpm_YOUR_TOKEN

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
POST /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

curl -X POST \
  -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

curl -X POST \
  -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)

GET /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
      }
    }
  ]
}
POST /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

curl -X POST \
  -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.

POST /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.