Skip to content

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.

FieldWhat it takes
place_idOne 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.
underWhole minutes, from 1 to 600.
webhook_urlWhere Waity POSTs. https only; see the rules below.
repeatfalse (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_atOptional, 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:

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"
StatusWhat it takes
armedWaiting for the wait to drop under the threshold.
firedA one-time alert that fired.
expiredPast its expires_at without firing.
failingThe last delivery attempt failed; see last_delivery. Retries follow while any are left.
disabled20 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:

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.

PlanActive alerts
Free1
Plus10
Pro100
Team1,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.

Essential cookies keep the site working. Optional analytics cookies help us improve it. See our Privacy Policy and Terms.