Features
Wait alerts
Waity calls your webhook when a place's live wait drops under your threshold, so an agent doesn't have to keep asking. Every plan has them, Free included.
How it works
Create an alert for one place with a threshold in minutes and an https webhook. After every ingest minute, Waity checks the places that just got new readings and POSTs a signed JSON body to your webhook when the wait is at or under the threshold.
An alert reads the same live answer as GET /v1/places/{id}, so it never disagrees with the API. It fires only when now.usable is true, the unit is minutes and now.value is at most under. A lower bound ("over 60"), a stale or paused number, a closed place or no data never fires.
Create an alert
POST /v1/alerts with an API key and a JSON body. The response is the alert, with its id and the signing secret. The secret is shown once: store it where your webhook can read it.
| Field | What it takes |
|---|---|
place_id | One place, lane or department. An airport, border port or hospital campus has no single wait, so its id is refused with its lane or department ids. |
under | Whole minutes, from 1 to 600. |
webhook_url | Where Waity POSTs. https only; see the rules below. |
repeat | false (the default) fires once, then the alert is fired. true fires again after the wait rises above the threshold plus 5 minutes, at most once every 30 minutes. |
expires_at | Optional, ISO 8601. The default is 24 hours from now, the longest 30 days. |
curl -X POST "https://api.waity.ai/v1/alerts" \
-H "X-Api-Key: $WAITY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"place_id":"pl_nvk0wkb3ps","under":30,"webhook_url":"https://example.com/waity/webhook"}'
201Response
{
"id": "al_3k9qx2m7t4p8w1za",
"place": {
"id": "pl_nvk0wkb3ps",
"name": "Ysleta Passenger Vehicles, Standard",
"category": "border"
},
"under": 30,
"unit": "min",
"repeat": false,
"webhook_url": "https://example.com/waity/webhook",
"status": "armed",
"created_at": "2026-10-31T19:02:11Z",
"expires_at": "2026-11-01T19:02:11Z",
Webhook URL rules
To keep alerts from reaching private networks, Waity refuses a webhook URL that:
- doesn't use https on the default port (443), or carries a user name or password;
- uses an IP address instead of a host name, so private, loopback, link-local and cloud metadata addresses are all refused;
- uses localhost, a single-label name, or a private or reserved name such as .local, .internal, .lan or .home.arpa;
- points at Waity itself.
Waity resolves no names itself and never follows a redirect: a 3xx answer counts as a failed delivery.
List, read and delete
GET /v1/alerts returns your alerts, newest first, with their status and last delivery. An account's keys share one list; a key without an account has its own. GET /v1/alerts/{id} reads one, and DELETE /v1/alerts/{id} deletes it and stops its deliveries.
curl
curl "https://api.waity.ai/v1/alerts" \
-H "X-Api-Key: $WAITY_API_KEY"
curl -X DELETE "https://api.waity.ai/v1/alerts/al_3k9qx2m7t4p8w1za" \
-H "X-Api-Key: $WAITY_API_KEY"
| Status | What it takes |
|---|---|
armed | Waiting for the wait to drop under the threshold. |
fired | A one-time alert that fired. |
expired | Past its expires_at without firing. |
failing | The last delivery attempt failed; see last_delivery. Retries follow while any are left. |
disabled | 20 failed delivery attempts in a row. Create a new alert once your webhook works. |
The webhook
Each delivery is a POST with a JSON body. Read warnings to the user first, then summary. now.value holds until now.usable_until; after that, ask GET /v1/places/{id} again.
POST
{
"type": "wait.under",
"alert": {
"id": "al_3k9qx2m7t4p8w1za",
"under": 30
},
"place": {
"id": "pl_nvk0wkb3ps",
"name": "Ysleta Passenger Vehicles, Standard",
"category": "border",
"url": "https://waity.ai/places/ysleta-passenger-vehicles-standard-kb3ps"
},
"now": {
"value": 25,
"unit": "min",
"measured_at": "2026-10-31T13:58:00-06:00",
"usable_until": "2026-10-31T15:28:00-06:00",
"basis": "estimate"
},
"summary": "About 25 min to enter the US, estimated by the border agency, updated 2 min ago.",
"warnings": [],
"links": {
"place": "https://api.waity.ai/v1/places/pl_nvk0wkb3ps",
"page": "https://waity.ai/places/ysleta-passenger-vehicles-standard-kb3ps",
"alert": "https://api.waity.ai/v1/alerts/al_3k9qx2m7t4p8w1za",
"docs": "https://waity.ai/docs/alerts"
}
}
Each request carries these headers:
Waity-Signature: t=<unix>,v1=<hex>:tis the time it was signed, in unix seconds;v1is the HMAC-SHA256 of the stringt, a dot and the raw body, keyed with the alert's whole secret.Waity-Alert: the alert's id.Waity-Delivery: the delivery's id, the same on every retry, so you can drop duplicates.
Verify the signature
Check the signature over the raw bytes before you parse the JSON, compare in constant time, and reject a t more than 5 minutes old.
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.WAITY_ALERT_SECRET; // whsec_…
// The raw body: the signature covers the exact bytes Waity sent.
app.post("/waity/webhook", express.raw({ type: "application/json" }), (req, res) => {
const parts = Object.fromEntries(
(req.get("Waity-Signature") ?? "").split(",").map((p) => p.split("=", 2)),
);
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${req.body}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
const ok =
fresh &&
typeof parts.v1 === "string" &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body);
// Read event.warnings first, then event.summary.
res.status(204).end();
});
app.listen(3000);
Retries
Answer with any 2xx within 10 seconds. Anything else is a failed attempt, retried about 1, 5, 25 minutes later. After 20 failed attempts in a row the alert is disabled.
Limits per plan
Each plan holds a number of active alerts at once. One more answers 409 alert_limit with a message naming your plan's limit. Each alerts request counts as one call; deliveries never count. A key without an account counts as Free.
| Plan | Active alerts |
|---|---|
| Free | 1 |
| Plus | 10 |
| Pro | 100 |
| Team | 1,000 |
ER and urgent care
For ER and urgent-care alerts, warnings[0] of every delivery is the 911 line, said once, as on every ER answer.
Related
Next says when a wait should drop, from history. Reading an answer explains every field, and every field is in the API reference.