# Waity

Live waits for ERs, urgent care, DMV offices, airport security lanes and land border crossings: the current posted wait, the usual wait for this hour, when it should drop, and nearby alternatives.

Coverage right now: live waits: er (471 of 577 places live), urgent-care (64 of 64 places live), dmv (73 of 73 places live), airport (5 of 5 places live), border (145 of 145 places live). GET /v1/categories has the current counts. coverage none means nothing Waity tracks matched: read the warning, which says whether the category isn't covered yet or nothing is tracked near that place, and never say nothing exists. coverage partial means the nearest tracked place is far away: say closer ones likely exist that Waity doesn't track, and never call the results the nearest.

Base URL: https://api.waity.ai
OpenAPI: https://api.waity.ai/openapi.json
MCP: https://api.waity.ai/mcp (tools find_waits and get_wait)

## What each number means (say this when it matters)
ER: the wait is the time until a provider first sees you, not the whole visit, which takes hours. Each hospital system measures it differently (HCA: a 4-hour average; Inova: a median; Northwell: an estimate it posts as 60+ over an hour), so never compare minutes across systems. People with serious symptoms are seen first: for chest pain, stroke signs, trouble breathing or a serious injury, tell the user to call 911 or go to the nearest ER, whatever the waits say. A freestanding ER (place.facility freestanding_ed) has no inpatient beds or surgery.
Urgent care: for minor illness or injury only, never for an emergency.
Border: waits are for entering the US only (time in line to reach the CBP booth). Waity has no waits for leaving the US; don't use them for a southbound or outbound trip.
DMV (Virginia): the number is the driver's license and ID queue, the longest current wait after check-in, not every service at the office.
Airport (DCA): each checkpoint serves specific gates. Terminal 1 (Checkpoint A) serves only gates A1-A9; the Terminal 2 checkpoints (North and South) serve gates B10-E59. Never send a traveler to a checkpoint that doesn't serve their gate.

## Keys and limits
Plans: Free $0 a month, 1,000 calls a day, 1 key; Pro $29 a month, 50,000 calls a day, 5 keys; Team $199 a month, 500,000 calls a day, 25 keys. GET https://api.waity.ai/v1/plans lists them; accounts and paid plans are at https://account.waity.ai, where account keys share their plan's daily limit.

Keyless: 100 successful calls a day. A free key gives 1000 a day: POST https://api.waity.ai/v1/keys with JSON {"email":"you@example.com"}, then send X-Api-Key: <key> (or Authorization: Bearer <key>). The key is shown once. /v1/keys is limited to 5 requests a minute per IP and 3 a day per email; asking again for the same email emails a rotate link (valid 30 minutes) when email delivery is set up, otherwise it answers 409 key_exists. The link opens a page that, on confirm, calls POST https://api.waity.ai/v1/keys/rotate with JSON {"token":"<token from the link>"} and shows the new key once; the old key stops working. Failed calls and HEAD requests never count. A short burst limit also applies (about 60 requests per 10 seconds per IP without a key; 300 a minute per key or keyless MCP session) and answers 429 rate_limited with Retry-After. It is best-effort: Cloudflare counts it per edge location and only eventually, so a fast burst can pass it. Keep under it even when a burst gets through. /v1/categories, /llms.txt, /openapi.json and /health are not counted. The quota is soft. Each edge location re-reads your shared count at least every 5 seconds, so RateLimit-Remaining can lag your true total by up to about 5 seconds of your own traffic, and two answers close together can differ by that much. Keyless callers are counted per IP address (per /64 for IPv6) per UTC day, except keyless MCP sessions (see MCP below). Repeating category or ids merges the values; repeating any other parameter with different values is a 400.

## One shape for every place
Every place, in a list, in instead, or as an airport or border lane, is the same object:
  { place: {id, name, category, parent, open_now, opens_at, closes_at, timezone, sample, audience, facility, lane, url, api_url}, summary, now, usual, distance, best_on_day, vs_requested, links }
The single-place call adds next and instead, and a fuller place block (address, phone, lat, lng, h3_r8, hours, lanes_not_tracked).
Every key is always present; null means "doesn't apply" or "unknown", never zero. Two exceptions: now.people_waiting (only when the place posts a live count) and now.value_low (only when now.bound is range).
distance is {km, from} and null when no reference point was given; from is "you", "this place", or the city or ZIP label q was read as (query.resolved.label, e.g. "ZIP 92173"). No drive time is ever returned.
All times are ISO 8601 in the place's local time with its offset, e.g. 2026-10-29T20:43:16-04:00; place.timezone names the zone.

## view=minimal
Add view=minimal to either call for the essentials. Minimal is a strict subset of full: same keys, same paths, fewer of them.
  item:   { place: {id, name, category, parent, open_now, opens_at, closes_at, timezone, sample, audience, facility}, summary, now: {status, usable, value, value_low, unit, bound, reason, trend}, distance, links: {details, airport, group} }
  (now.value_low only when now.bound is range.)
  single: place, summary, now (as above), usual {value, compared}, next {under, under_status, under_by, best, summary}, distance, links
  forecast=true with view=minimal adds next.forecast as [{at, value}].
Every API link spells out view. Response links: self, full, minimal, details, page, plan_day (the next day's plan), alternatives (a list of nearby options), airport (the lane's airport, else null), group (the airport, border port or hospital campus a lane or department belongs to, else null). Every item carries details, airport and group.

## Places
GET https://api.waity.ai/v1/places with one of:
  near      lat,lng: places near a point, nearest first. Or send the header X-Waity-Location: lat,lng instead (preferred: it keeps the location out of URLs and logs).
  cell      an H3 cell index instead of a point.
  q         a place name, city, airport code or ZIP. query.match is exact or fuzzy, or location when q is read as a US city or ZIP (any of them, from the Census gazetteer): the list is then the places nearest that point, query.resolved gives {label, lat, lng}, and each distance.from is that label (e.g. "ZIP 92173"). A ZIP that does not exist returns a warning. near (or X-Waity-Location) still wins over q for distances. A bare city name in several states ("Brooklyn") is read as the one near the caller's location, else the most populous; without a location, the first warning names the one assumed and the others ("Waity read "Brooklyn" as Brooklyn, NY. There is also a Brooklyn in ..."): add the state ("Brooklyn, IN") for another. A q of only filler words ("near me", "nearby", "closest") counts as no q: the list is around near or X-Waity-Location.
  ids       up to 25 place ids, comma-separated, returned in that order unless sort is given. An airport or port id stands for its tracked lanes.
Filters:
  category  er, urgent-care, dmv, airport, border. Comma-separate several; omit for all.
  open      true or false. true = open right now.
  sort      distance (default with a point), wait (places without a usable wait last), total (drive estimate plus wait, needs a location: near, X-Waity-Location, or a city or ZIP in q; the estimate ranks but is never returned).
  limit     1-25, default 10.
  view      full (default) or minimal.
A near search widens 15, 30, 60, 120 km until it finds enough places; query.searched_km says how far it looked, query.furthest_km how far the furthest result is. Coordinates in query and links are rounded to 3 decimals.
coverage: live, no_live_data, all_closed (every place returned is closed), partial (the nearest place Waity tracks in a category is far away, e.g. an ER over 40 km: closer places very likely exist that Waity doesn't track; the first warning says so, so never call the results the nearest), none_open (places exist but none is open; see warnings), none (nothing Waity tracks matched: the warning says whether the category isn't covered yet, nothing is tracked within 120 km, or a location is missing; never say nothing exists).

## One place
GET https://api.waity.ai/v1/places/{id}
  under       minutes threshold for next.under_by. Defaults: er 30, urgent-care 30, dmv 20, airport 15, border 30.
  travel_min  minutes to get there; adds next.leave_by.
  date        YYYY-MM-DD local to the place, today to 7 days ahead: plan that day.
  near        lat,lng of the user (or X-Waity-Location), for distances.
  forecast    true adds a compact hourly forecast to view=minimal.
  view        full (default) or minimal.
An airport or border port id returns {place, summary, coverage, lanes[], distance, ...}: place is the group with open_now (any lane open), opens_at (the earliest lane opening), lanes_tracked and lanes_not_tracked. A hospital campus id (category er or urgent-care, e.g. one with an adult and a pediatric ER) has the same shape: lanes[] holds its departments, all-ages first, and summary names each ("Adult ER about 7 min; Pediatric ER not reporting."). A department's place.parent is its campus.

## Reading the answer
place.facility     ERs only: hospital_ed (an ER in a hospital) or freestanding_ed (a freestanding ER: no inpatient beds or surgery, so a serious case is transferred to a hospital); null for every other place, urgent cares included (category urgent-care already says it: minor illness or injury only). Say "freestanding" whenever you name one.
place.lane         airport and border lanes only, else null: general, precheck, clear (airports); standard, ready, sentri, nexus (border). sentri is a SENTRI lane (Mexican border), nexus a NEXUS lane (Canadian border); the programs are enrolled separately. CBP's feed posts both as one lane type, so check place.name, which always names the program.
place.audience     all; children: a children-only ER or urgent care (a pediatric ER, a children's hospital). Never send an adult to one; it is never offered in instead for an all-ages place, and the notes say so when one is listed. women: an obstetric ER at a women's hospital, for pregnancy emergencies only; never treat it as a general ER.
summary            One line safe to repeat: "About 40 min, posted by the hospital, updated 5 min ago." / "Closed. Opens Fri 8:00 AM." / "No delay, posted by CBP, updated 12 min ago."
now.usable         true only for a fresh live reading. now.value is set only then.
now.value          minutes, or null. Null is never zero. 0 appears only where the source posts a real zero (CBP "no delay", a DMV queue with nobody waiting).
now.reason         why there is no usable value: closed, feed_paused (the source stopped posting), source_not_reporting (the source posts a "no data" marker; measured_at is then null and read_ago_s counts from Waity's last read of the source), not_published (the place doesn't post waits), out_of_date, no_recent_data. Null when usable. None of these is a gap in Waity: say what summary says.
now.status         live, stale, paused, closed, unknown.
now.bound          exact, upper (say "under N min"), lower (say "over N min": Northwell posts any ER wait over 60 min as "60+", read it as "Over 60 min", never an exact figure), range (now.value_low to now.value: say "6-9 min").
now.trend          rising, falling or steady over the last 30 minutes; change_30m is the change in minutes (full view). Null without an earlier reading, for feeds that post less often than every 15 minutes (CBP), and when bound is lower or range (a capped or ranged figure has no exact minutes to compare).
now.last_value     (full view) the last number posted, for context when not usable. Never present it as the current wait.
now.basis          median, average, estimate or max (the current longest wait): what the source posts. Different bases are not strictly comparable.
now.measures       what the number spans, in the source's words; null if unpublished.
now.source         {name, kind, attribution_required}. kind: operator (hospital system, airport authority, CBP), place (the place posts its own wait), visitors (reports from people who were there).
now.people_waiting present only when the place posts a live count of people in line (some DMVs). Absent otherwise; absence is not a gap.
usual              the typical wait for this weekday and hour, or null when Waity has no history for this hour (never an object with confidence none and 0 days). usual.value is null below medium confidence; compared (worse, normal, better) only at medium or better. confidence is none, low, medium or high; days_of_history counts the days behind this hour's value. Waity's history is young: expect usual.value, forecast values, under_by and best to be null for most places for now. Quote live waits.
next.forecast      hourly slots {at, open, open_minutes, value}. Values only at medium or high confidence.
next.under_status  already (live wait at or below the threshold now), usually_under (no live reading, but this hour usually is), expected (under_by says when), not_expected, not_now (a live wait above the threshold and no forecast yet to say when it drops), no_data (no live reading and no forecast), closed_all_day, closed_rest_of_day, not_published.
next.best          {from, to, value}: the quietest hour open at least 45 minutes. Null for ERs and urgent care (nobody should time an emergency) and when the day is flat.
next.leave_by      with travel_min: now when the line is already (or usually) short, else when to leave to arrive as it drops; null if it isn't expected to drop.
instead            alternatives (same item shape), ranked by drive estimate plus wait. vs_requested {wait_diff_min, extra_km, note}: the wait at the place asked about minus this one's (positive means this one is shorter), and the extra distance. wait_diff_min is null when the two waits come from different systems (they measure differently) or either is unknown or capped ("Over 60 min"); note says why, or what to know about the alternative (a freestanding ER, urgent care). Alternatives never cross a reach boundary (DCA Terminal 1 vs Terminal 2, Point Roberts vs the mainland). ER and urgent-care alternatives are offered as shorter only from the same system (their minutes measure the same thing); a children's ER gets pediatric ERs, then hospital ERs (never a freestanding ER); a hospital ER lists hospital ERs before freestanding ones. When none qualifies, instead is empty and a diagnostic says why. Cross-system places appear only when the place asked about is closed. Waits only; no drive time. Never a PreCheck, CLEAR, Ready or SENTRI lane for someone asking about a general or standard lane. For an ER, up to two urgent cares follow the ER alternatives (never on their own, and never with wait_diff_min: an urgent care is not a substitute for an ER visit).
notes              lines to repeat. For ERs: the 911 and serious-symptoms line. For urgent care: minor illness or injury only. For a freestanding ER: what it lacks. For border lanes: entering the US only. For a Virginia DMV: the number is the license and ID queue. Last, each hand-curated place note (a NEXUS-only crossing, a part-time lane's hours, the gates a checkpoint serves), prefixed with the place's name.
warnings           sentences to read to the user.
diagnostics        for you, not the user: unknown parameters, ids that don't exist, lanes from more than one airport.
attribution        lines you must show. Alternatives' sources get their own line.
answer_id          cites this answer later.

## Errors
{"error":{"code","message","request_id","param"?}}. Codes: invalid_parameter (400, param names the field), missing_location (400), invalid_json (400), invalid_key (401), unauthorized (401, account routes: no or bad session), place_not_found (404), not_found (404), method_not_allowed (405), gone (410, retired routes), key_exists (409), key_limit (409, the plan's key limit is reached), already_subscribed (409, the account already has a paid plan), no_subscription (409, the account has no paid plan), unsupported_media_type (415), rate_limited (429, Retry-After), quota_exceeded (429, until UTC midnight), server_error (500), unavailable (503), accounts_closed (503, accounts are not open yet; POST /v1/keys still gives a free key). Retry only 429, 500 and 503 (not accounts_closed).

## Health
GET https://api.waity.ai/health: {status, time, live_data: {status, freshest_age_s, delayed_count}, api, conditions: {status}}. status is ok or degraded (some live feeds are failing or late; answers still come, with those places stale or not live). live_data.status is ok, delayed or down (every feed failing); freshest_age_s is the seconds since the newest good update; delayed_count appears only when some feeds are delayed. conditions never changes status. Not counted against any limit.

## Changelog
GET https://api.waity.ai/v1/changelog: what changed in Waity, newest first: {rev, updated_at, links, entries: [{id, version, date, title, areas, breaking, url, html, markdown, media}]}. areas are api, mcp, data, site and accounts; breaking: true marks a change that can break a caller. ?limit=1-100. Not counted, cached 60 s with an ETag. The same as Markdown: https://api.waity.ai/changelog.md; as an Atom feed: https://api.waity.ai/changelog.xml.

## MCP
POST https://api.waity.ai/mcp (JSON-RPC). Tools find_waits and get_wait mirror the two GET routes, default to view minimal, and return links as ready-made tool calls: {"tool":"get_wait","arguments":{...}}. place.api_url stays a REST URL.
Using a key: send X-Api-Key (or Authorization: Bearer) on the HTTP request, or, for hosted connectors that take only a URL (claude.ai, ChatGPT), put it in the URL: https://api.waity.ai/mcp?key=wk_... Waity never logs query strings. Treat that URL like the key itself.
Protocol versions: 2025-03-26, 2025-06-18 and 2025-11-25 (initialize, then tools/list and tools/call), and 2026-07-28 (no initialize; every request names its version in params._meta and the MCP-Protocol-Version header). initialize answers a version it doesn't support, such as 2024-11-05, with 2025-11-25; a client that can't speak that should upgrade its SDK or mcp-remote.
Browser origins: a request carrying an Origin header is accepted only from an allowlist (https://claude.ai, https://chatgpt.com, https://waity.ai) and gets 403 "Origin not allowed." otherwise, the DNS-rebinding guard MCP requires. Servers and desktop clients send no Origin and are unaffected. A browser app should call the REST API, which allows any origin.
Without a key: initialize returns an Mcp-Session-Id header; send it back on later requests (MCP clients do this on their own) and each session gets its own 100 successful calls a day and its own burst limit, up to 5000 a day for all keyless sessions from one network. Nothing about the session is stored. A request without a session id (including the 2026-07-28 era, which has no initialize) is counted per IP address, so a connector on a shared address should use ?key=.
