Developers · REST API v1
REST API reference
Everything the dashboard knows about your sites, as JSON. Read it from scripts, CI, spreadsheets or your own client portal.
01 — Authentication
Authentication
Create a key under Integrations → API keys in the dashboard. Keys look like eo_live_… and are shown once; we store only a SHA-256 hash. Send the key as a Bearer token on every request:
curl -s https://expiryowl.com/api/v1/monitors \
-H "Authorization: Bearer $EXPIRYOWL_KEY"A key belongs to one organization and sees only its data. Revoking a key in the dashboard stops it immediately. Missing, unknown and revoked keys all get 401.
02 — Plans and rate limits
Plans and rate limits
Free: read-only. Every GET works; POST and DELETE return 403 plan_read_only. Freelancer and above: read and write. Adding a site counts against your plan's domain limit (402 limit_exceeded when full).
Each key may make 120 requests per minute. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds). Over the limit you get 429 with Retry-After.
03 — Conventions
Conventions
Base URL https://expiryowl.com/api/v1. Responses are JSON. Lists come as { data: [...], count }. Dates are ISO-8601 in UTC. daysLeft is whole days from now and goes negative once something has expired. Values we don't know yet are null, never missing. Errors look like { "error": "not_found", "message": "…" }.
04 — Endpoints
Endpoints
| Method | Path | What it does |
|---|---|---|
| GET | /monitors | Every monitored host with its latest certificate, domain and uptime state. |
| GET | /monitors/{id} | One host in full: certificate details, registration, DNS snapshot and open incidents. |
| POST | /monitors | Start watching a hostname. First checks run within a minute. Counts against your plan's domain limit. |
| DELETE | /monitors/{id} | Stop watching a host and delete its history. |
| GET | /clients | Your clients and how many hosts each one has. |
| GET | /incidents | Incidents, newest first (max 500). |
| GET | /expiring | Certificates and domain registrations that expire within N days (expired ones included), soonest first. |
| GET | /public/{token}/summary | Read-only summary behind the widget. Uses an embed token, not an API key. CORS open, cached 60 seconds. |
GET /api/v1/monitors
Every monitored host with its latest certificate, domain and uptime state.
| client | query | Only hosts of this client id. |
| status | query | ok, warning, critical or unknown. |
Returns { data: Monitor[], count }
{
"data": [
{
"id": "Vx3kQ9mPz1aB7cD2eF4gH",
"hostname": "shop.northwind.example",
"port": 443,
"clientId": "c8Lr2Tq6Wn0Ys4Uv9Xz1A",
"clientName": "Northwind Bakery",
"status": "warning",
"paused": false,
"lastCheckedAt": "2026-09-25T08:00:12.000Z",
"createdAt": "2026-06-02T10:14:00.000Z",
"ssl": {
"daysLeft": 12,
"validTo": "2026-10-07T23:59:59.000Z",
"issuer": "Let's Encrypt",
"renewalHealth": "overdue",
"readiness": "ready",
"error": null,
"checkedAt": "2026-09-25T08:00:12.000Z"
},
"domain": {
"registrableDomain": "northwind.example",
"daysLeft": 211,
"expiresAt": "2027-04-24T00:00:00.000Z",
"registrar": "Cloudflare, Inc.",
"error": null,
"checkedAt": "2026-09-25T02:00:00.000Z"
},
"uptime": {
"up": true,
"statusCode": 200,
"responseMs": 184,
"checkedAt": "2026-09-25T08:03:00.000Z"
}
}
],
"count": 1
}GET /api/v1/monitors/{id}
One host in full: certificate details, registration, DNS snapshot and open incidents.
| id | path | Monitor id. |
Returns { data: MonitorDetail }
{
"data": {
"id": "Vx3kQ9mPz1aB7cD2eF4gH",
"hostname": "shop.northwind.example",
"port": 443,
"clientId": "c8Lr2Tq6Wn0Ys4Uv9Xz1A",
"clientName": "Northwind Bakery",
"status": "warning",
"paused": false,
"lastCheckedAt": "2026-09-25T08:00:12.000Z",
"createdAt": "2026-06-02T10:14:00.000Z",
"ssl": {
"daysLeft": 12,
"validTo": "2026-10-07T23:59:59.000Z",
"issuer": "Let's Encrypt",
"renewalHealth": "overdue",
"readiness": "ready",
"error": null,
"checkedAt": "2026-09-25T08:00:12.000Z",
"issuerOrg": "Let's Encrypt",
"issuerCn": "R11",
"subjectCn": "shop.northwind.example",
"sans": [
"shop.northwind.example"
],
"validFrom": "2026-07-09T00:00:00.000Z",
"chainValid": true,
"authError": null,
"acme": true,
"ariWindow": {
"start": "2026-09-06T00:00:00.000Z",
"end": "2026-09-08T00:00:00.000Z"
}
},
"domain": {
"registrableDomain": "northwind.example",
"daysLeft": 211,
"expiresAt": "2027-04-24T00:00:00.000Z",
"registrar": "Cloudflare, Inc.",
"error": null,
"checkedAt": "2026-09-25T02:00:00.000Z",
"statuses": [
"client transfer prohibited"
],
"transferLocked": true,
"nameservers": [
"ada.ns.cloudflare.com",
"bob.ns.cloudflare.com"
]
},
"uptime": {
"up": true,
"statusCode": 200,
"responseMs": 184,
"checkedAt": "2026-09-25T08:03:00.000Z"
},
"checks": {
"ssl": true,
"domain": true,
"uptime": true,
"dns": true
},
"uptimeUrl": "https://shop.northwind.example/",
"keyword": null,
"dns": {
"ns": [
"ada.ns.cloudflare.com",
"bob.ns.cloudflare.com"
],
"a": [
"104.21.32.10"
],
"aaaa": [],
"mx": [
{
"exchange": "mx.northwind.example",
"priority": 10
}
],
"caa": [
{
"critical": 0,
"tag": "issue",
"value": "letsencrypt.org"
}
],
"checkedAt": "2026-09-25T06:00:00.000Z"
},
"openIncidents": [
{
"id": "In7aP2sD9fG4hJ6kL1zX0",
"monitorId": "Vx3kQ9mPz1aB7cD2eF4gH",
"hostname": "shop.northwind.example",
"kind": "renewal_overdue",
"severity": "warning",
"title": "Renewal overdue: shop.northwind.example",
"summary": "Let's Encrypt certificates normally renew 30 days before expiry. This one has 12 days left.",
"openedAt": "2026-09-19T08:00:12.000Z",
"resolvedAt": null
}
]
}
}POST /api/v1/monitors
Start watching a hostname. First checks run within a minute. Counts against your plan's domain limit. Needs a paid plan.
| hostname | body | Required. A public hostname, e.g. shop.example.com. |
| client | body | Optional client id or exact client name. |
| port | body | Optional TLS port, default 443. |
| keyword | body | Optional text the homepage must contain. |
Returns 201 { data: Monitor, warning }
{
"data": {
"id": "Vx3kQ9mPz1aB7cD2eF4gH",
"hostname": "shop.northwind.example",
"port": 443,
"clientId": "c8Lr2Tq6Wn0Ys4Uv9Xz1A",
"clientName": "Northwind Bakery",
"status": "unknown",
"paused": false,
"lastCheckedAt": null,
"createdAt": "2026-06-02T10:14:00.000Z",
"ssl": null,
"domain": null,
"uptime": null
},
"warning": null
}DELETE /api/v1/monitors/{id}
Stop watching a host and delete its history. Needs a paid plan.
| id | path | Monitor id. |
Returns { deleted: true, id }
{
"deleted": true,
"id": "Vx3kQ9mPz1aB7cD2eF4gH"
}GET /api/v1/clients
Your clients and how many hosts each one has.
Returns { data: Client[], count }
{
"data": [
{
"id": "c8Lr2Tq6Wn0Ys4Uv9Xz1A",
"name": "Northwind Bakery",
"monitorCount": 3,
"createdAt": "2026-06-02T10:10:00.000Z"
}
],
"count": 1
}GET /api/v1/incidents
Incidents, newest first (max 500).
| status | query | open (default), resolved or all. |
Returns { data: Incident[], count }
{
"data": [
{
"id": "In7aP2sD9fG4hJ6kL1zX0",
"monitorId": "Vx3kQ9mPz1aB7cD2eF4gH",
"hostname": "shop.northwind.example",
"kind": "renewal_overdue",
"severity": "warning",
"title": "Renewal overdue: shop.northwind.example",
"summary": "Let's Encrypt certificates normally renew 30 days before expiry. This one has 12 days left.",
"openedAt": "2026-09-19T08:00:12.000Z",
"resolvedAt": null
}
],
"count": 1
}GET /api/v1/expiring
Certificates and domain registrations that expire within N days (expired ones included), soonest first.
| days | query | 1 to 365, default 30. |
Returns { data: ExpiringItem[], count, days }
{
"data": [
{
"monitorId": "Vx3kQ9mPz1aB7cD2eF4gH",
"hostname": "shop.northwind.example",
"clientId": "c8Lr2Tq6Wn0Ys4Uv9Xz1A",
"clientName": "Northwind Bakery",
"kind": "ssl",
"expiresAt": "2026-10-07T23:59:59.000Z",
"daysLeft": 12
}
],
"count": 1,
"days": 30
}GET /api/v1/public/{token}/summary
Read-only summary behind the widget. Uses an embed token, not an API key. CORS open, cached 60 seconds. No API key: the embed token in the path is the credential.
| token | path | Embed token from Integrations. |
Returns PublicSummary
{
"title": "Northwind Bakery",
"scope": {
"type": "client",
"clientName": "Northwind Bakery"
},
"branding": null,
"poweredBy": {
"name": "ExpiryOwl",
"url": "https://expiryowl.com"
},
"generatedAt": "2026-09-25T08:05:00.000Z",
"counts": {
"total": 1,
"ok": 0,
"warning": 1,
"critical": 0,
"unknown": 0
},
"hosts": [
{
"hostname": "shop.northwind.example",
"status": "warning",
"ssl": {
"daysLeft": 12,
"validTo": "2026-10-07T23:59:59.000Z"
},
"domain": {
"daysLeft": 211,
"expiresAt": "2027-04-24T00:00:00.000Z"
},
"uptime": {
"up": true
}
}
]
}05 — Adding a site
Adding a site
curl -s -X POST https://expiryowl.com/api/v1/monitors \
-H "Authorization: Bearer $EXPIRYOWL_KEY" \
-H "Content-Type: application/json" \
-d '{"hostname":"shop.example.com","client":"Northwind Bakery"}'We refuse IP addresses and names that point at private or reserved networks (400). A name that doesn't resolve yet is accepted with a warning, so you can add a site before it goes live. The first certificate, domain and uptime checks run within a minute.
06 — Errors
Errors
| Status | error | When |
|---|---|---|
| 400 | invalid_request | Bad JSON, unknown filter value, or a hostname we won't monitor (IP address, private or reserved name). |
| 401 | unauthorized | Missing, unknown or revoked API key. |
| 402 | limit_exceeded | Adding the host would go over your plan's domain limit. |
| 403 | plan_read_only | The Free plan's API is read-only; POST and DELETE need a paid plan. |
| 404 | not_found | No such resource in your organization. |
| 409 | conflict | That hostname and port are already monitored. |
| 429 | rate_limited | More than 120 requests in a minute with one key. See Retry-After. |
07 — Also available
Also available
The same data reaches AI assistants through our MCP server, and we push incidents to you with signed webhooks. Questions: [email protected].