Enori API Documentation

The Enori API lets you manage monitors, incidents, alerts, status pages, and more programmatically. All endpoints use REST conventions with JSON request and response bodies.

Base URL: https://api.enori.io

Quick Start

Get monitoring set up in three steps.

1. Get Your API Key

Navigate to the API Keys tab in your dashboard and create a new key. Choose scopes based on what you need:

ScopeAccess
monitors:readList and view monitors
monitors:writeCreate, update, delete, pause/resume monitors
incidents:readList and view incidents
status-pages:readList and view status pages
status-pages:writeCreate, update, delete status pages
teams:readList teams and members
teams:writeManage teams
traces:readRead traces; required for POST /api/traces/{id}/analyze
traces:writeOTLP trace ingestion (POST /api/v1/traces)
alerts:read / alerts:writeAlert records, channels, and rules
maintenance:read / maintenance:writeMaintenance windows
escalation:read / escalation:writeEscalation policies
notifications:read / notifications:writeIn-app notifications
audit:readAudit log (read-only)

There is no * / "full access" scope — the API rejects it with a 400. Grant the smallest set the integration needs; a key can hold any combination of the 19 above.

2. Make Your First Request

bash
curl -H "X-API-Key: YOUR_API_KEY" https://api.enori.io/api/monitors

3. Set Up Alerts

Create a webhook alert channel to receive real-time notifications when monitors go down or recover:

bash
curl -X POST https://api.enori.io/api/alert-channels \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Webhook",
    "type": "Webhook",
    "webhookUrl": "https://your-server.com/webhook"
  }'

Authentication

All API requests require an API key passed in the X-API-Key header:

bash
curl -H "X-API-Key: YOUR_API_KEY" https://api.enori.io/api/monitors

API keys are scoped — choose the minimum permissions needed. Keys can be created and managed on the API Keys tab in Settings or via the /api/keys endpoint.

Never share your API key. Treat it like a password. If compromised, revoke it immediately from the dashboard and create a new one.

Managing API Keys

POST/api/keys

Create a new API key. The full key is returned only once in the response — store it securely.

json
{
  "name": "CI/CD Pipeline",
  "scopes": ["monitors:read", "monitors:write"]
}
GET/api/keys

List all API keys (key values are masked).

DELETE/api/keys/{id}

Revoke and delete an API key.

Rate Limits

Rate limits protect the API from abuse. Limits are applied per API key.

EndpointLimit
Everything without a tighter policy100 / minute
Create monitor30 / minute
Check now5 / minute
Test alert5 / minute
Bulk writes20 / hour
Create report5 / hour
Create maintenance window10 / hour
AI step generation10 / minute
Trace ingestion (OTLP)100 / minute
MCP tool calls60 / minute
Account export1 / day
Account deletion1 / hour

Corrected 2026-07-25: this table previously advertised a 200 / minute read tier and a 10 / hour create-monitor limit. Neither existed — there is no separate read tier, and creating a monitor is 30/minute.

When rate limited, the API returns HTTP 429 Too Many Requests with these headers:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed by the policy governing THIS endpoint
X-RateLimit-RemainingApproximate requests left in the window — see the note below
X-RateLimit-ResetApproximate Unix timestamp when the window resets — see the note below
Retry-AfterSeconds to wait before retrying (exact, present on 429)

About Remaining and Reset. X-RateLimit-Limit is exact — it is the real permit count of the policy governing that endpoint. The other two are good-faith estimates: limiter state is per-server-instance and is not exposed for direct reading, so Remaining can read higher than your true remaining budget, and Reset reports a window boundary rather than the instant your particular window replenishes. Use them for pacing, not for precise budgeting. If you are actually rate limited, Retry-After on the 429 is authoritative — honour that value.

Pagination

List endpoints accept limit and offset. Both are optional — omit them and you get the whole result set, exactly as before pagination existed, so no existing integration changes.

ParameterDefaultMaxDescription
limitnone (all rows)200Page size. A larger value is clamped, not rejected.
offset0Rows to skip.

Every paged response carries the same four fields:

json
{
  "items": [ ... ],
  "totalCount": 137,
  "limit": 50,
  "offset": 100,
  "hasMore": false
}

totalCount is the size of the whole filtered set, not of this page — use it to size a progress bar or decide how many requests you need. Loop on hasMore rather than comparing counts yourself. When you do not page, limit is omitted and hasMore is false.

Available on GET /api/monitors and GET /api/incidents.

API Stability

  • Additive changes ship without notice. New fields on a response, new optional query

parameters, and new enum values can appear at any time. Parse defensively: ignore fields you do not recognise, and do not treat an unknown enum value as an error.

  • Breaking changes are announced first. Removing a field, renaming one, or changing the meaning

of an existing value is announced by email to account owners at least 30 days ahead, and the old behaviour keeps working through that window.

  • Deprecations are visible in the response. A deprecated endpoint returns a

Deprecation header with the date it stops working and a Link header pointing at what to use instead.

  • No version is silently retired. Nothing that works today stops working without the notice

above.

Monitors

List Monitors

GET/api/monitors

Query parameters:

ParameterTypeDescription
tagstringFilter by tag (repeat for AND logic: ?tag=prod&tag=api)
typestringFilter by type: Website, Ping, Port, Dns, Domain, Job
bash
curl -H "X-API-Key: YOUR_API_KEY" \
  "https://api.enori.io/api/monitors?type=Website"

Response:

json
{
  "items": [
    {
      "id": "mon_abc123",
      "name": "Production API",
      "type": "Website",
      "url": "https://api.example.com",
      "status": "Up",
      "uptimePercentage": 99.95,
      "lastCheckedAt": "2026-03-24T10:30:00Z",
      "intervalSeconds": 60
    }
  ],
  "totalCount": 1
}

Get Monitor

GET/api/monitors/{id}

Returns full monitor details including configuration and current state.

Create Monitor

POST/api/monitors
json
{
  "name": "Production API",
  "type": "Website",
  "url": "https://api.example.com",
  "intervalSeconds": 60,
  "timeoutSeconds": 30,
  "httpMethod": "GET",
  "expectedStatusCode": 200,
  "failureThreshold": 2,
  "alertOnDown": true,
  "alertOnRecovered": true,
  "alertChannelIds": ["ch_abc123"],
  "regions": ["UsEast", "EuWest"],
  "tags": ["production", "api"]
}

Monitor types:

TypeValueUse For
Website10HTTP/HTTPS endpoints, APIs, websites
Ping3ICMP ping checks
Port2TCP port availability
Dns4DNS record monitoring
Domain6Domain expiry tracking
Job8Cron job / heartbeat monitoring
Browser11Real-browser (Playwright) flows with steps, screenshots and Core Web Vitals
ApiFlow12Multi-step API flows with assertions and variable extraction

Update Monitor

PUT/api/monitors/{id}

Send the full monitor configuration. Fields not included will be reset to defaults.

Upsert by Name

PUT/api/monitors/by-name/{name}

Idempotent — creates if not exists, updates if exists. Always returns 200. Ideal for infrastructure-as-code and Terraform workflows. The URL name takes precedence over the body name.

Delete Monitor

DELETE/api/monitors/{id}

Permanently deletes the monitor and all associated data (check results, incidents, alerts).

Pause and Resume

POST/api/monitors/{id}/pause
POST/api/monitors/{id}/resume

Pausing stops checks and alerts. Resuming triggers an immediate check.

Trigger Immediate Check

POST/api/monitors/{id}/check

Triggers an on-demand check outside the regular interval. Rate limited to 5 per minute.

Three limits apply, and they return different 429 bodies — branch on the body, not the status:

LimitResponseRetryable
5 requests/minuterate-limit responseafter a minute
Per-monitor cooldown (Base 120s · Pro 60s · Business 30s){ "error": "Please wait before checking again", "retryAfterSeconds", "cooldownSeconds" } + a Retry-After headerafter the cooldown
Per-plan daily cap (Base 20 · Pro 100 · Business 500){ "error": "Daily limit reached (N manual checks per day)", "dailyLimit", "usedToday", "resetsAt" }, no Retry-Afternot until resetsAt (next UTC midnight)

Call GET /api/monitors/check-limits for the current usedToday / remainingToday. The same daily cap applies to the check_now MCP tool.

usedToday counts check results already RECORDED, and a triggered check records its row when the executor runs it — so remainingToday is a lower bound on what you have left, not an exact figure while checks are still in flight (the limits block returned by the trigger call itself does not include the check it just queued).

Check Results

GET/api/monitors/{id}/checks?limit=50

Returns recent check results with response time, status code, and error details.

Statistics

GET/api/monitors/{id}/stats?days=30
GET/api/monitors/{id}/response-time-stats?days=7
GET/api/monitors/{id}/stats/hourly
GET/api/monitors/{id}/stats/daily

Incidents

List Incidents

GET/api/incidents

Returns { items: [...], totalCount: N }.

ParameterTypeDescription
monitorIdstringFilter by monitor
statusstringFilter by status
limitintPage size. Omit for all rows (see Pagination above); max 200, clamped.
offsetintRows to skip.

This endpoint does not accept date filters. An earlier version of this page listed from and to; they were never implemented, so a request carrying them silently returned unfiltered data. To narrow by date, filter startedAt client-side. (GET /api/incidents/recent takes a count, not a date range, so it is not a substitute.)

Recent and Active Incidents

GET/api/incidents/recent
GET/api/incidents/active

Get Incident

GET/api/incidents/{id}

Create Manual Incident

POST/api/incidents

Acknowledge

POST/api/incidents/{id}/acknowledge

Stops escalation timers for this incident.

Resolve

POST/api/incidents/{id}/resolve

Add Update

POST/api/incidents/{id}/updates

Post a status update to the incident timeline.

Incident Statistics

GET/api/incidents/stats

Incidents by Monitor

GET/api/incidents/by-monitor/{monitorId}

Alerts

List Alert Records

GET/api/alerts
ParameterTypeDescription
monitorIdstringFilter by monitor
statusstringFilter by delivery status
typestringAlert type (Down, Recovered, P95, etc.)
fromdatetimeStart date
todatetimeEnd date
limitintMax results
offsetintPagination offset

Alert Channels

Manage where alerts are delivered.

GET/api/alert-channels
POST/api/alert-channels
PUT/api/alert-channels/{id}
DELETE/api/alert-channels/{id}
POST/api/alert-channels/{id}/test

Supported channel types: Email, Slack, Discord, Teams, Webhook

Alert Rules

Configure which monitors trigger which channels.

GET/api/alerts/rules
POST/api/alerts/rules
PUT/api/alerts/rules/{id}
DELETE/api/alerts/rules/{id}
POST/api/alerts/rules/{id}/enable
POST/api/alerts/rules/{id}/disable
POST/api/alerts/rules/{id}/test

Job Monitoring

Enori's Job monitor type tracks cron jobs and scheduled tasks using heartbeat pings.

How It Works

  1. Create a Job monitor with a cron expression defining your expected schedule
  2. Get the unique ping token from the monitor details
  3. Add ping calls to your job script — Enori alerts you if pings stop arriving

Ping Endpoints

These endpoints require no authentication — just the unique token.

GET/ping/{token}

Report job success (heartbeat).

GET/ping/{token}/fail

Report job failure.

GET/ping/{token}/start

Report job start (enables runtime tracking).

All three endpoints accept GET, HEAD, and POST methods.

Example: Shell Cron Job

bash
#!/bin/bash
# Notify Enori the job has started
curl -fsS -m 10 https://api.enori.io/ping/YOUR_TOKEN/start

# Run the actual job
/usr/bin/backup-database.sh

# Report success or failure based on exit code
if [ $? -eq 0 ]; then
  curl -fsS -m 10 https://api.enori.io/ping/YOUR_TOKEN
else
  curl -fsS -m 10 https://api.enori.io/ping/YOUR_TOKEN/fail
fi

Example: GitHub Actions

yaml
name: Nightly Build
on:
  schedule:
    - cron: '0 2 * * *'
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: curl -fsS https://api.enori.io/ping/${{ secrets.ENORI_TOKEN }}/start
      - run: npm ci && npm run build
      - run: curl -fsS https://api.enori.io/ping/${{ secrets.ENORI_TOKEN }}
        if: success()
      - run: curl -fsS https://api.enori.io/ping/${{ secrets.ENORI_TOKEN }}/fail
        if: failure()

Example: Python Script

python
import requests
import sys

TOKEN = "YOUR_TOKEN"
BASE = "https://api.enori.io/ping"

# Signal start
requests.get(f"{BASE}/{TOKEN}/start", timeout=10)

try:
    # Your job logic here
    run_etl_pipeline()
    requests.get(f"{BASE}/{TOKEN}", timeout=10)
except Exception:
    requests.get(f"{BASE}/{TOKEN}/fail", timeout=10)
    sys.exit(1)

Ping History

GET/api/monitors/{id}/pings

Returns the heartbeat history for a Job monitor including execution times, status, and messages.

Status Pages

Authenticated Endpoints

GET/api/status-pages
POST/api/status-pages
PUT/api/status-pages/{id}
DELETE/api/status-pages/{id}
POST/api/status-pages/{id}/custom-domain
POST/api/status-pages/{id}/verify-domain
GET/api/status-pages/check-slug/{slug}

Public Endpoints (No Auth)

GET/api/status-pages/public/{slug}

View a public status page — returns component statuses, uptime data, and incident history.

POST/api/status-pages/public/{slug}/subscribe

Subscribe to status page updates via email.

Webhooks

When you create a Webhook alert channel, Enori will POST JSON payloads to your URL whenever monitor events occur.

Payload Format

json
{
  "event": "monitor.down",
  "timestamp": "2026-03-24T14:30:00Z",
  "monitor": { "id": "mon_abc123", "name": "Production API" },
  "alert": {
    "type": "Down",
    "title": "Production API is down",
    "message": "HTTP 503 Service Unavailable"
  },
  "episode": {
    "id": "ep_def456",
    "cause": "availability",
    "kind": "opened",
    "severity": "critical",
    "openedAt": "2026-03-24T14:30:00Z",
    "lastFiredAt": "2026-03-24T14:30:00Z",
    "resolvedAt": null,
    "actions": {
      "acknowledge": "https://api.enori.io/api/alert-actions/ack?...",
      "resolve": "https://api.enori.io/api/alert-actions/resolve?..."
    }
  }
}

timestamp is top-level, not inside alert. episode is present only for alerts that belong to an episode and is null otherwise; its actions are pre-signed URLs your system can call to acknowledge or resolve without a login.

Event Types

event is either monitor.{type} — the alert type lowercased — or monitor.episode.{kind} for episode-driven alerts.

EventFires when
monitor.downThe monitor is confirmed down
monitor.recoveryIt comes back up
monitor.slowresponseResponse time crosses your P95 threshold
monitor.performancedegradedSustained performance degradation
monitor.sslexpiring / monitor.sslexpiredTLS certificate nearing or past expiry
monitor.domainexpiring / monitor.domainexpiredDomain registration nearing or past expiry
monitor.domainregistrarchanged / monitor.domainnameserverchangedRegistrar or nameservers changed
monitor.securityheaderdegraded / monitor.securityheaderrecoveredSecurity-header grade dropped or recovered
monitor.reputationflagged / monitor.reputationclearedDomain reputation flagged or cleared
monitor.trialexpiring / monitor.trialexpiredAccount trial nearing or past expiry
monitor.episode.opened / .escalated / .resolvedEpisode lifecycle transitions

Treat unknown event names as forward-compatible: new alert types add new values, so match on the prefix rather than on an exhaustive list.

Security

Verify webhook authenticity by checking:

  • The User-Agent header contains Enori-Webhook/1.0
  • The request arrives within seconds of the event timestamp
  • Your endpoint URL uses HTTPS

Retry Policy

Failed webhook deliveries (non-2xx response or timeout) are retried:

  • 30 seconds after the first failure, then every 120 seconds
  • Up to 3 attempts per delivery
  • After the final attempt the delivery is marked failed and visible in your alert history

Badges

Embed a live uptime, status or response-time badge for one monitor in your README, documentation, or dashboard. Three endpoints, all anonymous, all returning SVG.

For the product-side walkthrough — where the switch lives, what the snippet builder does, troubleshooting — see the Status Badges guide.

Turn badges on first

Badges are off by default and must be enabled per monitor. Until you do, all three endpoints return a gray unknown badge — that is the most common reason a badge looks stuck.

The fastest way is the product: open the monitor, expand Status badges, flip the switch, and copy the ready-made Markdown or HTML from the same panel. It fills in the monitor id for you and previews the exact badge your readers will see.

Through the API:

bash
curl -X PUT https://api.enori.io/api/monitors/mon_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"badgesEnabled": true}'

badgesEnabled is also accepted on POST /api/monitors and is returned on every monitor response, so you can check the current state with GET /api/monitors/{monitorId}. Omitting it on PUT leaves it unchanged.

This publishes the monitor. While it is on, anyone who has a badge URL can read that monitor's uptime percentage, current status and average response time without signing in — that is what makes the badge work in a public README. Nothing else is exposed: not the monitored URL, not the monitor's name, not the check history, not your other monitors. Turning it back off takes effect on our side immediately — the publication check is never cached, so the very next request gets the gray badge. But every badge response carries Cache-Control: public, max-age=300, so an image already delivered can keep rendering for up to about five minutes in a reader's browser, a docs CDN, or an image proxy. Plan for that tail.

Uptime Badge

GET/api/badges/{monitorId}/uptime.svg

Renders the uptime percentage over the window, to two decimals (99.87%). Settled results only, maintenance excluded from both sides of the ratio — the same number GET /api/monitors/{id}/range-stats returns for the same range. Colour bands: >= 99.9 green, >= 99 olive, >= 95 amber, >= 90 orange, below that red.

Status Badge

GET/api/badges/{monitorId}/status.svg

Renders the monitor's current status as one word. The full value domain:

ValueColourMeaning
upgreenlast check passed
downredorigin unreachable
degradedamberresponding, but not fully healthy
failedredbrowser monitors — the site loaded, a flow step broke
maintenanceblueinside a declared maintenance window
runningblueCron Job monitors — a run has started and not yet reported finishing
expiring soonorangeDomain monitors — registration expires within 30 days
pausedgraythe monitor is paused
unknowngraysee "Why is my badge gray?" below

A paused monitor still serves its badges if badgesEnabled is true — the status badge reads paused and the uptime badge keeps reporting the window it can measure. Pausing is not unpublishing; badgesEnabled: false is.

Response Time Badge

GET/api/badges/{monitorId}/response-time.svg

Renders the average response time over the window, in whole milliseconds (183 ms). Same population as the uptime badge and as range-stats' avgResponseMs: successful checks with a measured response time, outside maintenance.

Always rendered in one colour — a "good" response time is workload-dependent, so Enori does not publish a performance verdict you never configured.

Domain and Cron Job monitors have no response time, so this badge always reads no data for them. Neither type makes a timed request of its own: a Domain check reads stored registry data, and a Cron Job monitor receives a heartbeat rather than calling out. (This is the same reason a latency SLO is refused on those two types.)

Parameters

ParameterApplies toDefaultOptionsDescription
perioduptime, response time30d24h, 7d, 30d, 90dTime range. Case-insensitive. An unrecognised value is refused — see below.
labelall threeuptime / status / responseLetters, digits, spaces, -, _, . — max 30 charactersLeft side label text. See the note below on what gets removed.
styleall threeflatflat, flat-square, plastic, for-the-badgeBadge style. Case-insensitive; an unrecognised value falls back to flat.

period is refused, style is coerced — deliberately. A wrong style costs you the wrong corners; a wrong period would publish a different number under your own label. So an unrecognised period renders a red invalid period badge (still HTTP 200 — a 4xx would show up in a README as a broken-image glyph) rather than quietly falling back to 30 days. ?period=24H is fine; ?period=30 and ?period=1y are not. Sending period= empty is the same as omitting it.

What label accepts. Characters outside the set above are silently removed, not rejected — the badge still renders, just without them. Letters and digits from any alphabet are kept, so café, приложение and 日本 come through intact. Punctuation and symbols do not:

You sendBadge shows
?label=API:%20prodAPI prod
?label=uptime%20(eu)uptime eu
?label=99%25%20target99 target
?label=web%20%2B%20apiweb api

The removal happens first and the 30-character limit is applied afterwards. If nothing survives, the label falls back to that endpoint's own defaultuptime, status or response respectively. The Status badges panel in the app previews this for you as you type.

Embed in Markdown

markdown
[![Uptime](https://api.enori.io/api/badges/mon_abc123/uptime.svg)](https://app.enori.io/dashboard/monitors/mon_abc123)
[![Status](https://api.enori.io/api/badges/mon_abc123/status.svg?style=flat-square)](https://app.enori.io/dashboard/monitors/mon_abc123)
[![Response](https://api.enori.io/api/badges/mon_abc123/response-time.svg?style=for-the-badge)](https://app.enori.io/dashboard/monitors/mon_abc123)

Linking the badge costs nothing extra and lets a reader who sees a red badge click through instead of guessing. A bare !Uptime works too.

Embed in HTML

html
<a href="https://app.enori.io/dashboard/monitors/mon_abc123">
  <img src="https://api.enori.io/api/badges/mon_abc123/uptime.svg" alt="Uptime" />
</a>

Response contract

Every outcome the badge endpoint itself decides returns HTTP 200 with an SVG — monitor not found, not opted in, bad period, empty window. Failure is expressed in the badge's TEXT, because the response is consumed as an and a 4xx renders as a broken-image glyph. The exceptions come from outside the endpoint: exceeding the rate limit returns 429, and an API-side fault returns a 5xx.

Headers on every response:

text
Content-Type: image/svg+xml; charset=utf-8
Cache-Control: public, max-age=300
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox

Badges are cached for 5 minutes on both sides: the uptime and response-time figures are computed at most once per 5 minutes per monitor and window, and the response tells caches the same. The publication check itself is not cached.

Badge requests are rate-limited to 60 requests per minute per IP address.

Why is my badge gray?

A gray badge carries one of two words, and they mean different things.

A gray unknown badge means one of these, in the order worth checking:

  1. Badges are not enabled for that monitor — see "Turn badges on first" above. This is by far the

most common cause.

  1. The monitor id is wrong or the monitor was deleted. Ids look like mon_ followed by 32

hexadecimal characters; copy it from the Status badges panel rather than retyping it.

  1. You switched badges off within the last 5 minutes and are seeing a cached image.
  2. On status.svg only: the monitor genuinely has no verdict yet — never checked, or the last

check could not be classified.

Causes 1 and 2 are deliberately indistinguishable on the wire: an opted-out monitor and a nonexistent monitor id return byte-identical responses, so a badge URL cannot be used to discover which monitor ids exist. That also means the badge itself cannot tell you which one applies — check the panel.

A gray no data badge is different and is not an error: the monitor is enabled and running, but the window you asked for contains nothing to measure — a brand-new monitor, or one whose every check in that window fell inside a maintenance window. Widen period or wait for the next check. On response-time.svg it is also permanent for Domain and Cron Job monitors, as described above.

Badges from other services (Shields.io)

If you already use Shields.io and want a badge in its exact house style, point its dynamic/json badge at a public status page's uptime history, which is plain JSON and needs no API key:

text
https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.enori.io%2Fapi%2Fstatus-pages%2Fpublic%2FYOUR-SLUG%2Fuptime-history&query=%24.overallUptimePct&label=uptime

Colour, style and any prefix/suffix are Shields' own options — consult their documentation for those. On our side, GET /api/status-pages/public/{slug}/uptime-history returns overallUptimePct (uptime across every monitor on the page, weighted by check count) plus a days array. Use overallUptimePct: entries in days carry a placeholder of 100 for days with no recorded checks, flagged by hasData: false, so a badge built on a single day can show a perfect score for a day nobody watched. overallUptimePct is null when the whole window has no checks.

The page must be public and not password-protected. There is no equivalent per-monitor JSON endpoint — a monitor is published through its own badge opt-in or through a status page, and both are choices you make deliberately.

Error Responses

The API uses standard HTTP status codes and returns JSON error bodies.

Status Codes

CodeDescription
200Success
201Created
202Accepted (async operation started)
204No Content (successful deletion)
400Bad Request — validation error
401Unauthorized — missing or invalid API key
403Forbidden — insufficient scope or plan limit exceeded
404Not Found
429Too Many Requests — rate limit exceeded
500Internal Server Error

Error Format

json
{
  "error": "Monitor limit exceeded",
  "message": "Your Base plan allows 10 monitors. Upgrade to Pro for up to 50.",
  "code": "PLAN_LIMIT_EXCEEDED"
}

Common Errors

ScenarioCodeWhat to Do
Invalid API key401Check the key is correct and not revoked
Missing required scope403Create a new key with the required scope
Plan limit exceeded403Upgrade your plan or remove unused resources
Rate limited429Wait for Retry-After seconds, then retry
Invalid request body400Check the error message for field-level details

Plan Limits

LimitBase (€5.99/mo)Pro (€14.99/mo)Business (€29.99/mo)
Monitors1050200
Check Interval60s60s60s
Data Retention30 days60 days90 days
API Keys21050
Alert Channels515Unlimited
Teams210Unlimited
Members per Team31025
Status Pages31050
Escalation Policies21020
Maintenance Windows210Unlimited
Reports per Month310Unlimited

Add-ons (available on any plan):

Add-onPriceEffect
Extra Monitor€0.50/month each+1 to your plan's monitor limit
30s Check Interval€3.00/monthUnlock 30-second checks (account-wide)

All plans include a 14-day free trial.

Code Examples

cURL

bash
# List all monitors
curl -H "X-API-Key: YOUR_API_KEY" \
  https://api.enori.io/api/monitors

# Create a website monitor
curl -X POST https://api.enori.io/api/monitors \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production API",
    "type": "Website",
    "url": "https://api.example.com/health",
    "intervalSeconds": 60,
    "expectedStatusCode": 200,
    "alertOnDown": true,
    "alertOnRecovered": true
  }'

# Pause a monitor
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  https://api.enori.io/api/monitors/mon_abc123/pause

# Trigger an immediate check
curl -X POST -H "X-API-Key: YOUR_API_KEY" \
  https://api.enori.io/api/monitors/mon_abc123/check

Python

python
import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.enori.io"
HEADERS = {"X-API-Key": API_KEY, "Content-Type": "application/json"}

# Create a monitor
monitor = requests.post(f"{BASE}/api/monitors", headers=HEADERS, json={
    "name": "Staging API",
    "type": "Website",
    "url": "https://staging.example.com",
    "intervalSeconds": 300,
    "expectedStatusCode": 200,
}).json()
print(f"Created monitor: {monitor['id']}")

# List all monitors
data = requests.get(f"{BASE}/api/monitors", headers=HEADERS).json()
for m in data["items"]:
    print(f"  {m['name']}: {m['status']} ({m['uptimePercentage']:.2f}%)")

# Get recent incidents
incidents = requests.get(
    f"{BASE}/api/incidents/recent", headers=HEADERS
).json()
for inc in incidents:
    print(f"  [{inc['status']}] {inc['title']}")

Node.js

javascript
const API_KEY = "YOUR_API_KEY";
const BASE = "https://api.enori.io";

async function enori(path, options = {}) {
  const res = await fetch(`${BASE}${path}`, {
    ...options,
    headers: {
      "X-API-Key": API_KEY,
      "Content-Type": "application/json",
      ...options.headers,
    },
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  return res.json();
}

// Create a monitor
const monitor = await enori("/api/monitors", {
  method: "POST",
  body: JSON.stringify({
    name: "Checkout Service",
    type: "Website",
    url: "https://checkout.example.com/health",
    intervalSeconds: 60,
  }),
});
console.log(`Created: ${monitor.id}`);

// List all monitors
const { items } = await enori("/api/monitors");
items.forEach((m) => console.log(`${m.name}: ${m.status}`));

GitHub Actions

yaml
name: Deploy and Verify Uptime
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build && npm run deploy

      - name: Verify monitors after deploy
        env:
          ENORI_KEY: ${{ secrets.ENORI_API_KEY }}
        run: |
          sleep 30
          RESPONSE=$(curl -s -H "X-API-Key: $ENORI_KEY" \
            "https://api.enori.io/api/monitors?tag=production")
          echo "$RESPONSE" | jq '.items[] | "\(.name): \(.status)"'

          DOWN=$(echo "$RESPONSE" | jq '[.items[] | select(.status == "Down")] | length')
          if [ "$DOWN" -gt 0 ]; then
            echo "::error::$DOWN production monitors are down!"
            exit 1
          fi

Interactive API Explorer

For a complete interactive reference with all endpoints, request/response schemas, and the ability to try requests directly, visit the Scalar API documentation:

Open API Explorer

The OpenAPI 3.0 specification is also available programmatically:

bash
curl https://api.enori.io/openapi/v1.json