Introduction
Severe weather alert API — CAP-based alerts, geo queries, signed webhooks, and usage metering.
Severe Alerts normalizes severe weather alerts from multiple national sources into a single JSON shape. Integrate by:
- Creating an account and API key (control plane)
- Pulling alerts over HTTPS (data plane)
- Optionally receiving signed webhook POSTs when alerts change
Quick start
# 1. Register (returns a Sanctum control-plane token)
curl -s -X POST "$BASE_URL/api/v1/auth/register" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"name":"Acme","email":"[email protected]","password":"your-password"}'
# 2. Create an API key (requires Sanctum token + active subscription)
curl -s -X POST "$BASE_URL/api/v1/keys" \
-H "Authorization: Bearer $SANCTUM_TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"label":"production"}'
# 3. Fetch active alerts
curl -s "$BASE_URL/api/v1/alerts/active?country=US&per_page=25" \
-H "Authorization: Bearer $API_KEY" -H "Accept: application/json"
Save the API key from step 2 immediately — it is shown once.
Auth model
| Token | Header | Used for |
|---|---|---|
| Sanctum (register/login) | Authorization: Bearer <sanctum> |
API keys, billing, logout |
| API key | Authorization: Bearer <API_KEY> |
Alerts, usage, webhooks |
Default key scopes: alerts:read, webhooks:write, usage:read.
| HTTP | Meaning |
|---|---|
| 401 | Missing or invalid credentials |
| 402 | No active subscription |
| 403 | Missing API key scope (error.details.required) |
| 429 | Rate or monthly quota — honor Retry-After |
Successful data-plane responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (unix seconds). Defaults: 60 req/min and 100,000 req/month (per account; overridable).
Response conventions
Success
{
"data": { },
"meta": { }
}
datais always present.metaappears on geo/coverage endpoints (degraded,degraded_reasons). Cursor-paginated lists also include Laravellinks+ paginationmeta.- List/show alert endpoints use Laravel JsonResource wrapping (
{ "data": ... }). - Webhook delivery bodies use a top-level envelope (not REST
data/meta). HMAC signs the raw body.
Errors
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded",
"details": { "retry_after": 12 }
}
}
error.code |
HTTP | Notes |
|---|---|---|
missing_api_key / invalid_api_key |
401 | |
unauthenticated |
401 | Control plane |
invalid_credentials |
401 | Login |
subscription_required |
402 | |
insufficient_scope |
403 | details.required |
not_found |
404 | |
validation_failed |
422 | Field → messages in details |
invalid_bbox / invalid_points / invalid_webhook_url |
422 | |
webhook_limit_exceeded |
422 | Max webhooks per account |
rate_limit_exceeded |
429 | details.retry_after |
monthly_quota_exceeded |
429 | details.used, quota, retry_after |
too_many_requests |
429 | Auth throttle |
Alert shapes
Webhook bodies, show, geo endpoints, and ?include=full share the same full document.
Full resource includes geometry, bbox, and infos. Summary (default list/active/history) omits those and includes headline instead. Always present: id, cap_id, cancelled.
Timestamps are unix seconds (or null). When an alert is cancelled: message_type is "Cancel" and cancelled is true — upsert on cap_id and treat as inactive.
| Field | Values |
|---|---|
severity |
Extreme, Severe, Moderate, Minor, Unknown |
message_type |
Alert, Update, Cancel, Ack, Error, Unknown |
urgency |
Immediate, Expected, Future, Past, Unknown |
certainty |
Observed, Likely, Possible, Unlikely, Unknown |
geo_type |
polygon, PUB, FIRE, MARINE, OFFSHORE, FIPS, NUTS2, NUTS3, EMMA, CITY, iso, unknown |
Pagination & caching
List / active / history support:
| Param | Notes |
|---|---|
country |
ISO country code, e.g. US |
severity |
Exact match |
event_type |
Exact match |
updated_since |
Unix int — rows updated at/after this time |
include |
full for full resource rows |
per_page |
Max 100, default 25 |
cursor |
Cursor pagination |
Pass If-None-Match with a previous ETag to receive 304 when unchanged.
After an initial bootstrap, prefer deltas:
curl -s "$BASE_URL/api/v1/alerts/active?country=US&updated_since=$LAST_SYNC_UNIX&per_page=100" \
-H "Authorization: Bearer $API_KEY" -H "Accept: application/json"
History window is approximately 30 days.
Webhooks (overview)
Register an HTTPS URL with optional filters (country, severity, event_type). On create you receive a one-time secret under data.secret — store it; list endpoints never return it.
Deliveries are POST with header X-Alert-Signature: t=<unix>,v1=<hex_hmac>:
signed_payload = "{t}." + raw_request_body
expected_v1 = HMAC_SHA256(secret, signed_payload) // hex
Reject if clock skew exceeds ~300 seconds; compare with constant-time equality using the raw body. Non-2xx responses are retried with backoff; after 20 consecutive failures the webhook is disabled. See the Webhooks group for payload fields and delivery history.
Downloads
Authenticating requests
This API uses Bearer tokens in the Authorization header.
Authorization: Bearer {YOUR_API_KEY}
Accept: application/json
Data plane (API key)
Use an API key for alerts, usage, and webhooks.
- Register or log in to receive a Sanctum control-plane token
POST /api/v1/keyswith that token to issue an API key (requires an active subscription)- Call data-plane endpoints with
Authorization: Bearer <API_KEY>
Scopes on newly issued keys default to alerts:read, webhooks:write, and usage:read. Pass a subset via scopes when creating a key.
| HTTP | Meaning |
|---|---|
| 401 | Missing/invalid API key |
| 402 | No active subscription |
| 403 | Missing required scope (error.details.required) |
| 429 | Per-minute rate limit or monthly quota — honor Retry-After |
Control plane (Sanctum)
Use the token from POST /api/v1/auth/register or POST /api/v1/auth/login for:
- Creating, listing, and revoking API keys
- Billing checkout / portal
- Logout
Login revokes prior control-plane tokens for that user. Logout deletes the current Sanctum token.
Public metadata routes (/regions, /event-types, /severity-levels, /status) require no authentication.
Metadata
Public catalog and health endpoints. No authentication required.
List regions
Distinct ISO country codes present in the alert read model.
Example request:
curl --request GET \
--get "http://localhost/api/v1/regions" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/regions"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/regions'
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": [
"CA",
"CN",
"IR",
"US"
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List event types
Distinct event types present in the alert read model.
Example request:
curl --request GET \
--get "http://localhost/api/v1/event-types" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/event-types"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/event-types'
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": [
"Flood",
"Thunderstorm",
"Wind"
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List severity levels
Distinct severity values present in the alert read model.
Example request:
curl --request GET \
--get "http://localhost/api/v1/severity-levels" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/severity-levels"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/severity-levels'
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": [
"Extreme",
"Minor",
"Moderate",
"Severe",
"Unknown"
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Service status
Lightweight health check with the latest alert update timestamp.
Example request:
curl --request GET \
--get "http://localhost/api/v1/status" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/status"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/status'
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": {
"status": "ok",
"latest_alert_updated_at": 1724592000
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Alerts
Query active and historical severe weather alerts. Requires scope alerts:read.
List / active / history default to summary rows; pass include=full for the full document
(geometry, bbox, infos). Supports cursor pagination and If-None-Match / ETag → 304.
Alerts by location
requires authentication
Point query returning full alert documents. Optional bbox CSV
(min_lat,min_lng,max_lat,max_lng) switches to a bounding-box search.
Response meta may include degraded / degraded_reasons (e.g. geocoder issues).
Example request:
curl --request GET \
--get "http://localhost/api/v1/alerts/by-location?lat=39.0&lng=-76.6&iso=US&bbox=architecto" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"lat\": -89,
\"lng\": -179,
\"iso\": \"gz\",
\"bbox\": \"architecto\"
}"
const url = new URL(
"http://localhost/api/v1/alerts/by-location"
);
const params = {
"lat": "39.0",
"lng": "-76.6",
"iso": "US",
"bbox": "architecto",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"lat": -89,
"lng": -179,
"iso": "gz",
"bbox": "architecto"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/alerts/by-location'
payload = {
"lat": -89,
"lng": -179,
"iso": "gz",
"bbox": "architecto"
}
params = {
'lat': '39.0',
'lng': '-76.6',
'iso': 'US',
'bbox': 'architecto',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, json=payload, params=params)
response.json()Example response (200, Success):
{
"data": [],
"meta": {
"degraded": false,
"degraded_reasons": []
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Batch alerts by locations
requires authentication
Up to 50 points per request (counts as one usage hit). Returns matching alert ids per point (not full documents). No single ETag / 304 on this endpoint.
Example request:
curl --request POST \
"http://localhost/api/v1/alerts/by-locations" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"points\": [
{
\"lat\": \"39.0\",
\"lng\": \"-76.6\",
\"ref\": \"device-1\"
}
]
}"
const url = new URL(
"http://localhost/api/v1/alerts/by-locations"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"points": [
{
"lat": "39.0",
"lng": "-76.6",
"ref": "device-1"
}
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/alerts/by-locations'
payload = {
"points": [
{
"lat": "39.0",
"lng": "-76.6",
"ref": "device-1"
}
]
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()Example response (200, Success):
{
"data": [
{
"ref": "device-1",
"alert_ids": [
7,
12
]
}
],
"meta": {
"degraded": false,
"degraded_reasons": []
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Alerts by bounding box
requires authentication
Returns full alert documents whose area intersects the bounding box.
Example request:
curl --request GET \
--get "http://localhost/api/v1/alerts/by-bbox?min_lat=38.7&max_lat=39.2&min_lng=-76.8&max_lng=-76.4" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"min_lat\": -89,
\"max_lat\": -89,
\"min_lng\": -180,
\"max_lng\": -179
}"
const url = new URL(
"http://localhost/api/v1/alerts/by-bbox"
);
const params = {
"min_lat": "38.7",
"max_lat": "39.2",
"min_lng": "-76.8",
"max_lng": "-76.4",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"min_lat": -89,
"max_lat": -89,
"min_lng": -180,
"max_lng": -179
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/alerts/by-bbox'
payload = {
"min_lat": -89,
"max_lat": -89,
"min_lng": -180,
"max_lng": -179
}
params = {
'min_lat': '38.7',
'max_lat': '39.2',
'min_lng': '-76.8',
'max_lng': '-76.4',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, json=payload, params=params)
response.json()Example response (200, Success):
{
"data": [],
"meta": {
"degraded": false,
"degraded_reasons": []
}
}
Example response (422, Invalid bbox):
{
"error": {
"code": "invalid_bbox",
"message": "min_* must be less than or equal to max_*",
"details": {}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Whether an alert covers given points
requires authentication
Useful after a webhook: check which devices fall inside an alert. Max 100 points.
Prefer POST with a JSON body for large batches (avoids URL length limits).
GET still accepts a pipe-separated points query string for small requests.
Example request:
curl --request GET \
--get "http://localhost/api/v1/alerts/12345/covers?points=39.0%2C-76.6%2Cdev1%7C38.9%2C-77.0" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"points\": [
{
\"lat\": \"39.0\",
\"lng\": \"-76.6\",
\"ref\": \"dev1\"
}
]
}"
const url = new URL(
"http://localhost/api/v1/alerts/12345/covers"
);
const params = {
"points": "39.0,-76.6,dev1|38.9,-77.0",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"points": [
{
"lat": "39.0",
"lng": "-76.6",
"ref": "dev1"
}
]
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/alerts/12345/covers'
payload = {
"points": [
{
"lat": "39.0",
"lng": "-76.6",
"ref": "dev1"
}
]
}
params = {
'points': '39.0,-76.6,dev1|38.9,-77.0',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, json=payload, params=params)
response.json()Example response (200, Success):
{
"data": [
{
"lat": 39,
"lng": -76.6,
"ref": "dev1",
"covered": true
}
],
"meta": {
"degraded": false,
"degraded_reasons": []
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Webhooks
Register HTTPS endpoints to receive signed POSTs when alerts change.
- Control plane:
/api/v1/account/webhooks*with a Sanctum token (dashboard; no API key required). - Data plane:
/api/v1/webhooks*with an API key that includes scopewebhooks:write.
List webhooks
requires authentication
Secrets are never included in list responses.
Example request:
curl --request GET \
--get "http://localhost/api/v1/account/webhooks" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/account/webhooks"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/account/webhooks'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": [
{
"id": 1,
"url": "https://example.com/hooks/alerts",
"filters": {
"country": "US"
},
"active": true,
"created_at": 1724586400
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Register webhook
requires authentication
URL must be public HTTPS (SSRF guard blocks private/loopback). Filters are applied at
dispatch (country → snapshot ISO, severity exact include, exclude_severity exact skip,
event_type exact). Empty filters = all alerts. The secret is returned once in this response.
Example request:
curl --request POST \
"http://localhost/api/v1/account/webhooks" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"url\": \"https:\\/\\/example.com\\/hooks\\/alerts\",
\"filters\": {
\"country\": \"US\",
\"exclude_severity\": \"Minor\",
\"severity\": \"Severe\",
\"event_type\": \"Wind\"
}
}"
const url = new URL(
"http://localhost/api/v1/account/webhooks"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"url": "https:\/\/example.com\/hooks\/alerts",
"filters": {
"country": "US",
"exclude_severity": "Minor",
"severity": "Severe",
"event_type": "Wind"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/account/webhooks'
payload = {
"url": "https:\/\/example.com\/hooks\/alerts",
"filters": {
"country": "US",
"exclude_severity": "Minor",
"severity": "Severe",
"event_type": "Wind"
}
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()Example response (201, Created):
{
"data": {
"id": 1,
"url": "https://example.com/hooks/alerts",
"filters": {
"country": "US",
"severity": "Severe"
},
"active": true,
"secret": "a1b2c3d4e5f6…",
"created_at": 1724586400
}
}
Example response (422, Invalid URL):
{
"error": {
"code": "invalid_webhook_url",
"message": "URL host is not allowed",
"details": {}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List deliveries
requires authentication
Recent delivery attempts for a webhook (newest first, max 100).
Example request:
curl --request GET \
--get "http://localhost/api/v1/account/webhooks/16/deliveries" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/account/webhooks/16/deliveries"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/account/webhooks/16/deliveries'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": [
{
"id": 891234,
"status": "delivered",
"http_status": 200,
"attempt": 1,
"created_at": 1724592000
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete webhook
requires authentication
Example request:
curl --request DELETE \
"http://localhost/api/v1/account/webhooks/16" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/account/webhooks/16"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/account/webhooks/16'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('DELETE', url, headers=headers)
response.json()Example response (200, Deleted):
{
"data": {
"status": "deleted"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List webhooks
requires authentication
Secrets are never included in list responses.
Example request:
curl --request GET \
--get "http://localhost/api/v1/webhooks" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/webhooks"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/webhooks'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": [
{
"id": 1,
"url": "https://example.com/hooks/alerts",
"filters": {
"country": "US"
},
"active": true,
"created_at": 1724586400
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Register webhook
requires authentication
URL must be public HTTPS (SSRF guard blocks private/loopback). Filters are applied at
dispatch (country → snapshot ISO, severity exact include, exclude_severity exact skip,
event_type exact). Empty filters = all alerts. The secret is returned once in this response.
Example request:
curl --request POST \
"http://localhost/api/v1/webhooks" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"url\": \"https:\\/\\/example.com\\/hooks\\/alerts\",
\"filters\": {
\"country\": \"US\",
\"exclude_severity\": \"Minor\",
\"severity\": \"Severe\",
\"event_type\": \"Wind\"
}
}"
const url = new URL(
"http://localhost/api/v1/webhooks"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"url": "https:\/\/example.com\/hooks\/alerts",
"filters": {
"country": "US",
"exclude_severity": "Minor",
"severity": "Severe",
"event_type": "Wind"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/webhooks'
payload = {
"url": "https:\/\/example.com\/hooks\/alerts",
"filters": {
"country": "US",
"exclude_severity": "Minor",
"severity": "Severe",
"event_type": "Wind"
}
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()Example response (201, Created):
{
"data": {
"id": 1,
"url": "https://example.com/hooks/alerts",
"filters": {
"country": "US",
"severity": "Severe"
},
"active": true,
"secret": "a1b2c3d4e5f6…",
"created_at": 1724586400
}
}
Example response (422, Invalid URL):
{
"error": {
"code": "invalid_webhook_url",
"message": "URL host is not allowed",
"details": {}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List deliveries
requires authentication
Recent delivery attempts for a webhook (newest first, max 100).
Example request:
curl --request GET \
--get "http://localhost/api/v1/webhooks/16/deliveries" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/webhooks/16/deliveries"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/webhooks/16/deliveries'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": [
{
"id": 891234,
"status": "delivered",
"http_status": 200,
"attempt": 1,
"created_at": 1724592000
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete webhook
requires authentication
Example request:
curl --request DELETE \
"http://localhost/api/v1/webhooks/16" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/webhooks/16"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/webhooks/16'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('DELETE', url, headers=headers)
response.json()Example response (200, Deleted):
{
"data": {
"status": "deleted"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Usage
Monthly request usage against your account quota.
- Control plane:
GET /api/v1/account/usagewith a Sanctum token (dashboard). - Data plane:
GET /api/v1/usagewith an API key that includes scopeusage:read.
Get usage
requires authentication
Returns the current calendar month usage and quota.
Example request:
curl --request GET \
--get "http://localhost/api/v1/account/usage" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/account/usage"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/account/usage'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": {
"period": "2026-09",
"used": 12840,
"persisted": 12000,
"quota": 100000
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get usage
requires authentication
Returns the current calendar month usage and quota.
Example request:
curl --request GET \
--get "http://localhost/api/v1/usage" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/usage"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/usage'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": {
"period": "2026-09",
"used": 12840,
"persisted": 12000,
"quota": 100000
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Account
Control-plane auth. Register/login return a Sanctum token used to manage API keys and billing. Data-plane calls (alerts, webhooks, usage) use an API key — not this token.
Register
Creates an account and returns a Sanctum control-plane token.
Example request:
curl --request POST \
"http://localhost/api/v1/auth/register" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Acme Weather\",
\"email\": \"[email protected]\",
\"password\": \"|]|{+-\"
}"
const url = new URL(
"http://localhost/api/v1/auth/register"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Acme Weather",
"email": "[email protected]",
"password": "|]|{+-"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/auth/register'
payload = {
"name": "Acme Weather",
"email": "[email protected]",
"password": "|]|{+-"
}
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()Example response (201, Created):
{
"data": {
"user": {
"id": 1,
"name": "Acme Weather",
"email": "[email protected]"
},
"token": "1|plainTextSanctumToken…"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Login
Returns a Sanctum control-plane token. Revokes prior control-plane tokens for the user.
Example request:
curl --request POST \
"http://localhost/api/v1/auth/login" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"email\": \"[email protected]\",
\"password\": \"|]|{+-\"
}"
const url = new URL(
"http://localhost/api/v1/auth/login"
);
const headers = {
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"email": "[email protected]",
"password": "|]|{+-"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/auth/login'
payload = {
"email": "[email protected]",
"password": "|]|{+-"
}
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()Example response (200, Success):
{
"data": {
"user": {
"id": 1,
"name": "Acme Weather",
"email": "[email protected]"
},
"token": "2|plainTextSanctumToken…"
}
}
Example response (401, Invalid credentials):
{
"error": {
"code": "invalid_credentials",
"message": "Invalid credentials",
"details": {}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Current account
requires authentication
Returns the authenticated user plus subscription, limits, and resource counts. Use this to bootstrap the customer dashboard after login.
Example request:
curl --request GET \
--get "http://localhost/api/v1/auth/me" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/auth/me"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/auth/me'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": {
"user": {
"id": 1,
"name": "Acme Weather",
"email": "[email protected]",
"created_at": "2026-10-01T12:00:00+00:00"
},
"subscription": {
"status": "active",
"interval": "monthly",
"on_trial": false,
"ends_at": null,
"trial_ends_at": null,
"stripe_price": "price_…"
},
"limits": {
"rate_limit_per_minute": 60,
"monthly_quota": 100000
},
"counts": {
"api_keys": 2,
"webhooks": 1
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Logout
requires authentication
Deletes the current Sanctum token.
Example request:
curl --request POST \
"http://localhost/api/v1/auth/logout" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/auth/logout"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/auth/logout'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers)
response.json()Example response (200, Logged out):
{
"data": {
"status": "logged_out"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List API keys
requires authentication
Secrets/tokens are never included in list responses — only metadata and prefix.
Example request:
curl --request GET \
--get "http://localhost/api/v1/keys" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/keys"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/keys'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Success):
{
"data": [
{
"id": 1,
"name": "production",
"prefix": "sa_live_",
"scopes": [
"alerts:read",
"webhooks:write",
"usage:read"
],
"last_used_at": null,
"revoked_at": null,
"created_at": 1724586400
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create API key
requires authentication
The plaintext token is returned once. Store it securely.
Example request:
curl --request POST \
"http://localhost/api/v1/keys" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"label\": \"production\",
\"scopes\": [
\"alerts:read\",
\"usage:read\"
]
}"
const url = new URL(
"http://localhost/api/v1/keys"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"label": "production",
"scopes": [
"alerts:read",
"usage:read"
]
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/keys'
payload = {
"label": "production",
"scopes": [
"alerts:read",
"usage:read"
]
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()Example response (201, Created):
{
"data": {
"id": 1,
"name": "production",
"prefix": "sa_live_",
"scopes": [
"alerts:read",
"webhooks:write",
"usage:read"
],
"token": "sa_live_…"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Revoke API key
requires authentication
Soft-revokes the key. It can no longer authenticate data-plane requests.
Example request:
curl --request DELETE \
"http://localhost/api/v1/keys/16" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/keys/16"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/keys/16'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('DELETE', url, headers=headers)
response.json()Example response (200, Revoked):
{
"data": {
"status": "revoked"
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Billing
Stripe checkout and customer portal. Requires a Sanctum control-plane token. An active subscription is required before API keys can call the data plane.
Create checkout session
requires authentication
Returns a Stripe Checkout URL. success_url and cancel_url hosts must be allow-listed.
Example request:
curl --request POST \
"http://localhost/api/v1/billing/checkout" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"interval\": \"monthly\",
\"success_url\": \"https:\\/\\/app.example.com\\/billing\\/success\",
\"cancel_url\": \"https:\\/\\/app.example.com\\/billing\\/cancel\"
}"
const url = new URL(
"http://localhost/api/v1/billing/checkout"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"interval": "monthly",
"success_url": "https:\/\/app.example.com\/billing\/success",
"cancel_url": "https:\/\/app.example.com\/billing\/cancel"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/billing/checkout'
payload = {
"interval": "monthly",
"success_url": "https:\/\/app.example.com\/billing\/success",
"cancel_url": "https:\/\/app.example.com\/billing\/cancel"
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('POST', url, headers=headers, json=payload)
response.json()Example response (200, Success):
{
"data": {
"url": "https://checkout.stripe.com/c/pay/cs_test_…"
}
}
Example response (422, Invalid return URL):
{
"error": {
"code": "invalid_return_url",
"message": "success_url host is not allowed",
"details": {
"field": "success_url"
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Billing portal
requires authentication
Returns a Stripe Customer Portal URL for the authenticated user.
Optional return_url host must be allow-listed (same rules as checkout).
When omitted, returns to the API status endpoint.
Example request:
curl --request GET \
--get "http://localhost/api/v1/billing/portal?return_url=https%3A%2F%2Fapp.example.com%2Fapp%2Fbilling" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"return_url\": \"http:\\/\\/www.bailey.biz\\/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html\"
}"
const url = new URL(
"http://localhost/api/v1/billing/portal"
);
const params = {
"return_url": "https://app.example.com/app/billing",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"return_url": "http:\/\/www.bailey.biz\/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/billing/portal'
payload = {
"return_url": "http:\/\/www.bailey.biz\/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html"
}
params = {
'return_url': 'https://app.example.com/app/billing',
}
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers, json=payload, params=params)
response.json()Example response (200, Success):
{
"data": {
"url": "https://billing.stripe.com/p/session/…"
}
}
Example response (422, Invalid return URL):
{
"error": {
"code": "invalid_return_url",
"message": "return_url host is not allowed",
"details": {
"field": "return_url"
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Subscription status
requires authentication
Returns the Cashier default subscription summary for the authenticated user.
Example request:
curl --request GET \
--get "http://localhost/api/v1/billing/subscription" \
--header "Authorization: Bearer {YOUR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://localhost/api/v1/billing/subscription"
);
const headers = {
"Authorization": "Bearer {YOUR_API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());import requests
import json
url = 'http://localhost/api/v1/billing/subscription'
headers = {
'Authorization': 'Bearer {YOUR_API_KEY}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request('GET', url, headers=headers)
response.json()Example response (200, Active):
{
"data": {
"status": "active",
"interval": "monthly",
"on_trial": false,
"ends_at": null,
"trial_ends_at": null,
"stripe_price": "price_…"
}
}
Example response (200, None):
{
"data": {
"status": "none",
"interval": null,
"on_trial": false,
"ends_at": null,
"trial_ends_at": null,
"stripe_price": null
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.