Developers
A free, read-only JSON API for the same data as the website, and the webhooks we send when one of your monitors goes down.
The API
Base address: https://api.netoutages.com/api/v1
- Everything is a GET request and needs no key or account.
- Answers are JSON, wrapped in
data. Lists addlinksandmetafor paging. - Answers are cacheable for 15 seconds (
Cache-Control: public, max-age=15). - The limit is 300 requests a minute per address. Over it you get HTTP 429.
- An unknown service is HTTP 404, a bad parameter is HTTP 422 with the reasons in
errors. - Statuses are
up,degraded,downandunknown.monitoringislimitedwhen a site blocks our checks.
| Endpoint | Parameters | Returns |
|---|---|---|
| GET /stats | — | Counts: services monitored, down, degraded, open incidents, and the time of the last check. |
| GET /categories | — | Every category with its number of services and how many have problems. |
| GET /services | category, status (up, degraded, down, unknown), q (name), problems (1), reported (1), per_page (1–100), page | A page of services, problems first. Each has its status, last check, response time and, here, the reports of the last hour. |
| GET /services/{slug} | — | One service with its latest check, uptime (24 hours, 7 days, 30 days), open incident and recent reports. |
| GET /services/{slug}/history | range (1h, 24h, 7d, 30d) | Response times and failures in fixed time buckets, with empty buckets included. |
| GET /services/{slug}/reports | — | What users reported in the last 24 hours: volume, kinds of problem, countries and cities. |
| GET /incidents | state (open, resolved, all), service (slug), per_page, page | Outages found by our checks or by users' reports (confirmed_by_probes and confirmed_by_reports say which), newest first. |
| GET /heatmap | — | Where users report problems right now, and the services reported most in the last hour. |
Example
curl https://api.netoutages.com/api/v1/services/discord{
"data": {
"slug": "discord",
"name": "Discord",
"status": "up",
"monitoring": "active",
"last_checked_at": "2026-10-25T14:02:11+00:00",
"response_ms": 480,
"uptime": { "24h": 100, "7d": 99.94, "30d": 99.9 },
"open_incident": null
}
}Webhooks
When you add a webhook channel in your alert settings, we send a POST with a JSON body when a monitor of yours goes down and when it recovers. A test button sends the same request with the type test.
typeismonitor.down,monitor.recoveredortest.- Headers:
X-NetOutages-EventandX-NetOutages-Delivery(the delivery id). - Answer with any 2xx status. We wait 10 seconds, do not follow redirects and try again twice (after 30 seconds and after 2 minutes) if the answer is not a success.
- The address must be public: private networks are refused.
{
"id": "evt_1042",
"type": "monitor.down",
"created_at": "2026-10-25T14:05:00+00:00",
"monitor": { "id": 7, "name": "My shop", "url": "https://shop.example.com" },
"incident": {
"id": 31,
"started_at": "2026-10-25T14:04:58+00:00",
"resolved_at": null,
"duration_seconds": 2,
"error": "Timed out after 10s."
},
"message": "My shop is down. Our checks keep failing. Latest result: Timed out after 10s."
}Check that a request is really from us
If you set a signing secret on the channel, every request carries X-NetOutages-Signature: sha256=<hex>, the HMAC-SHA256 of the exact request body with your secret. Compute it over the raw body, not over parsed JSON, and compare in constant time.
import { createHmac, timingSafeEqual } from "node:crypto";
// `rawBody` is the request body exactly as received, before any JSON parsing.
function isFromNetOutages(rawBody, signatureHeader, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}$expected = 'sha256='.hash_hmac('sha256', $request->getContent(), $secret);
if (! hash_equals($expected, $request->header('X-NetOutages-Signature', ''))) {
abort(401);
}