# Waity: full guide Waity gives AI agents live wait times for places with a line. The short guide is https://api.waity.ai/llms.txt; this file has every detail. Current coverage (it grows; GET /v1/categories has details): live waits for er, urgent-care, dmv, airport, border. Base URL: https://api.waity.ai OpenAPI: https://api.waity.ai/openapi.json MCP: https://api.waity.ai/mcp (tools find_waits, get_wait and get_history) Use of Waity is subject to the Terms of Service (https://waity.ai/terms) and Privacy Policy (https://waity.ai/privacy). ## Safety rules - ER and urgent care: Waity tracks only some, so their lists are coverage partial: never call the results the nearest. 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; serious symptoms are seen first. An ER wait is the time until a provider first sees you, not the whole visit; sources measure it differently, so never compare minutes across sources. 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 crossings: waits are for entering the US only; Waity has none for leaving the US. - Place-specific rules (the gates a checkpoint serves, what a DMV number counts) come in each answer's notes: repeat them. ## Keys and limits Plans (Free $0 a month, 1,000 calls a day, 1 key; Plus $5 a month, 10,000 calls a day, 2 keys; Pro $29 a month, 50,000 calls a day, 5 keys; Team $199 a month, 500,000 calls a day, 25 keys): https://waity.ai/account; GET https://api.waity.ai/v1/plans has them as JSON. 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":"YOUR_EMAIL"}, then send X-Api-Key: (or Authorization: Bearer ). The key is shown once. /v1/keys is limited to 5 requests a minute per IP, 3 a day per email and 5 new keys a day per network (an IPv4 address or an IPv6 /64). Reserved example and test domains (example.com, .net and .org, and names under .test, .invalid, .example or .localhost) and common disposable inboxes get 400 invalid_parameter: use an address you read. 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, whose message says how to get a working key (sign in to an account with that email and create one there, or write to ops@waity.ai). The link opens a page that, on confirm, calls POST https://api.waity.ai/v1/keys/rotate with JSON {"token":""} 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: it is counted per location (region) and per machine and shares counts between machines only eventually, so a burst spread over many connections can pass it (measured: one key at 20 requests a second passed about 800-1,200 a minute before the first 429), and the request right after a 429 can succeed. Over one kept-alive connection it trips near the configured number. The daily quota is exact and is the hard limit: pace at or under 5 a second per key and wait Retry-After on any 429. A burst 429 carries RateLimit-Limit (300 or 60), RateLimit-Remaining 0, RateLimit-Reset (60 or 10), RateLimit-Policy ("300;w=60" or "60;w=10") and RateLimit, like a quota 429 (whose policy is the daily "N;w=86400"). /v1/categories, /v1/changelog, /llms.txt, /llms-full.txt, /openapi.json and /health are not counted. The quota is soft: RateLimit-Remaining can lag your true total by up to about 5 seconds of your own traffic. Keyless callers are counted per IP address (per /64 for IPv6) per UTC day, except keyless MCP sessions (see MCP). 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", the city or ZIP label q was read as (query.resolved.label, e.g. "ZIP 92173"), or " (approximate)" for a location guessed from the caller's network. 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 (MCP tools default to it). 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, basis, measured_at, usable_until, source: {short}}, 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}]; travel_min adds next.leave_by. 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, by distance. 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, neighborhood, airport code or ZIP, or a plain ask: "urgent care near Queens, NY", "chest pain", "pediatric ER in Brooklyn", "San Ysidro SENTRI", "Ambassador Bridge vs Windsor tunnel". Category words, symptoms ("chest pain" means ERs, and the answer opens with the 911 line) and errands ("REAL ID" means DMV) become the category; who the visit is for ("pediatric ER", "for my dad"), lane types, directions, time words ("tomorrow": answered for now, use date= on one place) and "shortest" (sort=total with a location, else wait) are read as such; filler ("how long is the", "right now") goes; the rest must name a place, and nothing is guessed: a place Waity can't tell gets "Waity couldn't tell where ..." (with "did you mean"), a kind of place it doesn't track says so. query.match is exact or fuzzy, or location when q is read as a US city, neighborhood or ZIP: the list is the places nearest that point, in that order, query.resolved gives {label, lat, lng, source, approx_km} (source q), and each distance.from is that label (e.g. "ZIP 92173"); with near (or X-Waity-Location), distances stay from the caller. A ZIP that does not exist returns a warning. A bare name shared by several places is read as the one near the caller's location, else a tracked place of that name (a port's community: "San Ysidro" is San Ysidro, San Diego, CA), else the most populous city; 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 vs B" lists both. "pediatric ER" lists children's ERs within 40 km, else all-ages ERs with a line saying so. A DMV search in a state where Waity tracks no office lists none. A q of only filler words ("near me", "nearby", "closest") counts as no q. "near me" (alone or with a category: "ER near me", "chest pain") with no location: Waity guesses the approximate city of the caller's network only when every one of these holds: the call is REST GET /v1/places (never MCP, never GET /v1/places/{id}); the network is a known residential (home broadband) network, not a datacenter, cloud or hosting network, a VPN, a mobile network or an unknown one; the User-Agent is a browser or curl-like tool (curl, wget, HTTPie, Postman, Insomnia, a browser opening the URL itself) and not a bot, crawler, HTTP library or AI agent; there is no Origin header and no script fetch (a web page's script is an app); the request isn't relayed by a proxy or another Worker; and q has no words Waity can't read beside "near me" (they may be the place meant; a warning says so). Opt out: send a location (near, X-Waity-Location: lat,lng, or a city or ZIP in q), or send the header X-Waity-Location: none, which never guesses and is otherwise treated as no location. With a guess, query.resolved has source ip, the city and approx_km (about 25 km), match stays null, distance.from is " (approximate)", sort=total is refused, and the response is Cache-Control: no-store; the city is never stored or logged. For ERs and urgent cares, warnings[0] is the 911 line and warnings[1] is "Location was estimated from your network (about 25 km): . Send your real location before choosing an ER." The list stays in distance order, holds at most 3 places, and each summary says the distance is from an approximate location. Without a guess, the warning asks for a location and a diagnostic says to ask the user for a city or ZIP. A question about leaving the US ("southbound" at the Mexican border, "into Mexico", "Laredo to Nuevo Laredo") gets coverage none and the warning that Waity has border waits only for entering the US; a direction never changes an ER, urgent-care or DMV question. 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 today (GET /v1/categories lists what is covered). Comma-separate several; omit for all. A parent (health, travel, government, ...) stands for the covered categories under it; a category Waity doesn't cover yet is answered with coverage none and a warning saying so, never an error. 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. The answer: {query, coverage, places, notes, warnings, diagnostics, links, answer_id, attribution}; places is the list of items (the shape above), in the order given by query.sort. Unknown parameters are ignored with a diagnostic ("Ignored unknown parameter ..."); MCP tools instead reject unknown arguments with invalid_parameter, since their inputSchema lists every argument. A point outside the US gets coverage none and the warning that Waity covers US places only. 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 (Waity tracks only some places of a category there, so closer ones may exist: every list with ERs or urgent cares, or asking for them, and any list whose nearest tracked place of a category is far away, e.g. an ER over 40 km; a 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 (naming the nearest place it does track), a location is missing, or the question is about leaving the US; never say nothing exists). Every answer about ERs says to go to the nearest ER or call 911 in an emergency. ## 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 (in both views). 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. date, forecast, under and travel_min apply to one lane or department, so a group id ignores them with a diagnostic. ## 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. Both can be posted 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, updated 12 min ago." now.usable true only for a fresh live reading: now.read_ago_s at most now.stale_after_s. now.value is set only then. Past that the reading is status stale, reason out_of_date, with last_value for context. now.stale_after_s (full view) seconds a posted reading stays usable. The source's own window, except border lanes: border waits are posted hourly, so a border post is stale after 90 minutes (5400). Null when there is no reading. now.usable_until when now.value stops being usable; after it, ask again. measured_at plus stale_after_s, in the place's local time with offset; null when there is no usable value. Answers are Cache-Control: private, max-age=60 (no-store around a location guessed from the network), so asking again within a minute can return the same answer. now.tier (full view) the kind of source behind the number, most to least authoritative: official (an operator's own feed: a hospital system, airport authority or border agency), place (the place posts its own wait), agent (reported through an agent), inferred (estimated, not posted). Null without a source. now.value minutes, or null. Null is never zero. 0 appears only where the source posts a real zero ("no delay", a 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), 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.measured_at when the number was posted; null when no number was posted (closed, source_not_reporting). now.read_ago_s (full view) seconds since measured_at: the age of the posted reading (a source that posts hourly can read 3,600 here while Waity checks it every 15 minutes). With measured_at null (source_not_reporting) it is the seconds since Waity last read the source, not a reading's age: never quote it as one. Null when closed, paused or no_recent_data. now.status live, stale, paused, closed, unknown. now.bound exact, upper (say "under N min"), lower (say "over N min": a source may post any wait over an hour 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 sources that post less often than every 15 minutes, and when bound is lower or range. 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 comparable. now.measures what the number spans; null if unpublished. now.source {name, short, kind, attribution_required}; view=minimal has {short}. Sources are not named. name says who posted the number, by kind ("the hospital", "the airport", "the border agency"). short is an opaque id ("src_7qm2"): equal ids mean the same publisher and method; it is stable across REST, MCP and history and says nothing else. kind: operator (hospital system, airport authority, border agency), place (the place posts its own wait), visitors (reports from people who were there). Compare minutes only between waits with the same source.short and basis: when the usable waits of a list come from more than one source or basis, an ER or urgent-care list says so in the warning right after the 911 line (warnings[1], or after the location-guess line when there is one), other lists in a diagnostic. ER lists stay in distance order. now.people_waiting present only when the place posts a live count of people in line. 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. 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, or if you would arrive while it is closed (a warning says so). 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 sources (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 (airport terminals not connected after security, an exclave reachable only through another country). ER and urgent-care alternatives are offered as shorter only from the same source; 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-source places appear only when the place asked about is closed. 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 serious-symptoms line (911 is only in warnings[0]). For urgent care: minor illness or injury only. For a freestanding ER: what it lacks. For border lanes: entering the US only. Then each place's own rules (the gates a checkpoint serves, what a DMV number counts, a part-time lane's hours), prefixed with the place's name. warnings sentences to read to the user, in order. Every ER answer (a list with ERs or asking for them, one ER, a hospital campus) has the 911 line as warnings[0], and only there. diagnostics for you, not the user: unknown parameters, ids that don't exist, lanes from more than one airport, why instead is empty. attribution lines you must show ("Data by Waity"). answer_id cites this answer later (valid 30 minutes). Opaque: "ans_" and an encrypted token; it can't be decoded and says nothing about the caller. Treat it as a string. ## History GET https://api.waity.ai/v1/places/{id}/history (MCP: get_history) returns what one place's posted waits were. It is descriptive: past waits never predict a visit, and an ER history answer says so in notes once (in an emergency, go to the nearest ER or call 911). from start, inclusive: a date YYYY-MM-DD (local midnight at the place) or an ISO 8601 date-time (with an offset; without one, local to the place). Default: 7 days before to. to end, exclusive; a date means the end of that local day. Default and latest: now. granularity hour (default): one bucket per UTC hour with data. day: one per local day; from and to snap to local midnight. raw: every posted reading. tier official, place or agent. Default: the tier of the place's displayed source. cursor page.next_cursor from the previous page, as is; it carries from, to, granularity and tier, so pass limit again with every page (links.next repeats it when you passed it). from, to, granularity or tier sent with a cursor are ignored, with a diagnostic. limit rows per page: raw 500 (at most 1000), hour 168 (at most 336), day 92 (at most 366). The answer: {place, summary, range, observations (raw) or buckets (hour, day), page, sources, notes, warnings, links, attribution}. sources: [{short, name, tier, basis}], short the same opaque id as now.source.short. observations [{t, value, value_low?, unit, bound, status, basis, people_waiting?, source: {short}}]: t is when the number was posted; bound and value_low read as in now. buckets [{t, avg, median, p90, min, max, n, open_share, partial, unit}], day buckets add date and hours. avg is the time-weighted mean while open; median and p90 of an hour weigh each posted value by how long it held; a day's median and p90 weigh its hours' averages by open time; n counts posted updates; open_share is the share of the hour or day the place was open with a wait. Hours with no open time are left out. range {from, to, granularity, tier, timezone, clipped, clipped_reason, plan, window_days, earliest_allowed, data_from}. data_from is where Waity's history for the place starts. page {limit, returned, has_more, next_cursor}. links.self, links.next (the next page) and links.place are URLs; over MCP they are tool calls: self and next get_history calls, place a get_wait call {id, view: "full"}. How buckets are built: hold a reading holds until the next reading of the same place and tier, until it stops being usable by the live rule (the same rule as now.usable_until: the source's stale_after_s, capped for border lanes at 1.5 times how often the source posts when it posts every 30 minutes or less often, so an hourly post holds 90 minutes, 5,400 s; ER and urgent care hold exactly stale_after_s), or the bucket's end, whichever comes first. A reading from before the bucket that still holds counts. partial the hour in progress is included, computed from the raw readings so far, with partial: true; its n counts the readings so far and its open_share is over the elapsed part of the hour. Today's day bucket is partial: true the same way. Finished buckets have partial: false. timezone hour buckets are UTC hours, with t shown at the place's offset; day buckets are the place's local calendar days (23 or 25 hours when the clocks change). range.timezone names the zone. open_share the share of the bucket's time (its elapsed time when partial) covered by open readings with a wait. Access: keyless calls get 401 key_required, before any quota check, and it is never counted. How far back a plan reads: Free 30 days, Plus and Pro 365, Team without limit. A wider range is clipped, not refused: range.clipped true, clipped_reason plan_history_window, and a warning. Each request counts as one call. A group id (airport, border port, hospital campus) is refused with its lane or department ids. A range that ended over 6 hours ago is Cache-Control: private, max-age=86400. Only sources Waity shows are in history. ## Errors {"error":{"code","message","request_id","param"?,"retry_after_s"?}}. retry_after_s comes with 429 rate_limited and quota_exceeded and matches the Retry-After header (seconds). 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), key_required (401, history: send a key (a free one reads 30 days back)), place_not_found (404), not_found (404), invalid_promo (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). Both 429s carry Retry-After and the RateLimit headers: rate_limited for the burst window, quota_exceeded for the day. ## 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). time is UTC (ISO 8601 with Z). 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. ## Accuracy and reliability https://waity.ai/status/accuracy shows, and https://waity.ai/status/accuracy.json gives as JSON (not counted): the nightly check that hourly and daily history agrees with the raw readings (recomputed by a separate implementation, compared with stored aggregates and with GET /v1/places/{id}/history), the archive check, p50/p95/p99 response times and error rates per component over 24 h, 7 d and 30 d (a 429 counts as a success), forecast accuracy by week (MAE, share within 5 min, skill against persistence and the usual for the hour, shown only from 100 scored forecasts) and nightly data-honesty checks on live answers. Its methodology section says how each number is computed. It reads from the site, so it answers while the API is down. ## Changelog GET https://api.waity.ai/v1/changelog: what changed in Waity's API, MCP, data and accounts, newest first: {rev, updated_at, links, entries: [{id, version, date, title, areas, breaking, url, html, markdown, media}]}. areas are api, mcp, data and accounts; breaking: true marks a change that can break a caller. ?limit=1-100. Not counted, cached 60 s with an ETag (send If-None-Match for a 304). The same as Markdown: https://waity.ai/changelog.md; as an Atom feed: https://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":{...}}. get_history mirrors GET /v1/places/{id}/history (see History) and takes the same arguments; links.next is a ready-made get_history call. place.api_url stays a REST URL. Unknown arguments are rejected with invalid_parameter (REST ignores unknown parameters with a diagnostic). MCP never guesses a location. Tool results: structuredContent holds the answer (the REST body, links as tool calls) and matches the tool's outputSchema. The text block repeats the same answer as JSON, for clients that read only text. Errors are always JSON text {"error":{...}} with isError true. A rate-limited or quota-exceeded tools/call is HTTP 200 with such a result (code rate_limited or quota_exceeded, with retry_after_s) and the Retry-After and RateLimit headers. A rate-limited request other than tools/call (initialize, tools/list) gets HTTP 200 with a JSON-RPC error {"code":-32000,"message":...,"data":{"error":{...}}}. Every MCP error response carries the CORS headers for an allowed Origin. A custom client (no SDK): POST each JSON-RPC message to https://api.waity.ai/mcp with Content-Type: application/json and Accept: application/json, text/event-stream; answers are always application/json. Send initialize {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}: the result comes with an Mcp-Session-Id header. Then send the notification {"jsonrpc":"2.0","method":"notifications/initialized"} (202, no body), then tools/list and tools/call {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"find_waits","arguments":{"q":"San Ysidro","category":["border"]}}} with the Mcp-Session-Id header. Batches (a JSON array) are refused with -32600: send one message per request. GET and DELETE https://api.waity.ai/mcp answer 405: Waity opens no server streams. Using a key: send X-Api-Key (or Authorization: Bearer) on the HTTP request, or, for hosted connectors that take only a URL, put it in the URL: https://api.waity.ai/mcp?key=wk_... Waity never logs query strings. Treat that URL like the key itself. Setup per client: Claude Code: claude mcp add --transport http waity https://api.waity.ai/mcp --header "X-Api-Key: ". Cursor, VS Code, Windsurf and other clients that take a JSON config: {"mcpServers":{"waity":{"url":"https://api.waity.ai/mcp","headers":{"X-Api-Key":""}}}}. Claude.ai and ChatGPT custom connectors: https://api.waity.ai/mcp?key=. 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. The session id is used only for that quota: no conversation state is kept, so any request may go to any server, and a lost session id just counts the next calls per IP address. 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=.