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

MethodPathWhat it does
GET/monitorsEvery 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/monitorsStart 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/clientsYour clients and how many hosts each one has.
GET/incidentsIncidents, newest first (max 500).
GET/expiringCertificates and domain registrations that expire within N days (expired ones included), soonest first.
GET/public/{token}/summaryRead-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.

clientqueryOnly hosts of this client id.
statusqueryok, warning, critical or unknown.

Returns { data: Monitor[], count }

Example response
{
  "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.

idpathMonitor id.

Returns { data: MonitorDetail }

Example response
{
  "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.

hostnamebodyRequired. A public hostname, e.g. shop.example.com.
clientbodyOptional client id or exact client name.
portbodyOptional TLS port, default 443.
keywordbodyOptional text the homepage must contain.

Returns 201 { data: Monitor, warning }

Example response
{
  "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.

idpathMonitor id.

Returns { deleted: true, id }

Example response
{
  "deleted": true,
  "id": "Vx3kQ9mPz1aB7cD2eF4gH"
}

GET /api/v1/clients

Your clients and how many hosts each one has.

Returns { data: Client[], count }

Example response
{
  "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).

statusqueryopen (default), resolved or all.

Returns { data: Incident[], count }

Example response
{
  "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.

daysquery1 to 365, default 30.

Returns { data: ExpiringItem[], count, days }

Example response
{
  "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.

tokenpathEmbed token from Integrations.

Returns PublicSummary

Example response
{
  "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
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

StatuserrorWhen
400invalid_requestBad JSON, unknown filter value, or a hostname we won't monitor (IP address, private or reserved name).
401unauthorizedMissing, unknown or revoked API key.
402limit_exceededAdding the host would go over your plan's domain limit.
403plan_read_onlyThe Free plan's API is read-only; POST and DELETE need a paid plan.
404not_foundNo such resource in your organization.
409conflictThat hostname and port are already monitored.
429rate_limitedMore 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].