{"openapi":"3.0.3","info":{"title":"Harbor by Signalry Catalog API","version":"1.0.0","description":"Read-only JSON for hotel jobs-to-be-done, providers, peer cases, and the buy/sell graph. Harbor by Signalry — not CNCF Harbor (goharbor.io). Resolve by slug. Never invent metrics or confidential deal prices. Hotels only. No PII. Estimates are not appraisals. Hotel graph start: GET /api/graph (discover), then GET /api/graph/stats (coverage), GET /api/graph/hotels (q, country, chainId, amenity, hasCoords, hasSameAs, hasBrand), GET /api/graph/search (hotels + node url hops), GET /api/graph/chains/{id} (mapped + bySource + types + amenities on the folio walk), GET /api/graph/amenities?amenity= (selected.byCountry + mapped/sameAs/brand/bySource on the exported 21 types). Contract: /docs/GRAPH-API.md. Wikidata CC0 + OSM ODbL + Harbor seed. Amenities and brands are source tags only — not a complete inventory. /api/graph/export is a sample (≤250 hotels).","contact":{"name":"Harbor by Signalry","url":"https://harbor.signalry.ai","email":"agents@harbor.signalry.ai"}},"servers":[{"url":"https://harbor.signalry.ai","description":"Public catalog"}],"paths":{"/api/v1":{"get":{"summary":"Harbor by Signalry API v1 index","responses":{"200":{"description":"Endpoint index"}}}},"/api/v1/jobs":{"get":{"summary":"List hotel jobs","parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"persona","in":"query","schema":{"type":"string","enum":["ops","owner","tech"]}},{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated jobs with stable slug ids"}}}},"/api/v1/jobs/{slug}":{"get":{"summary":"Job brief by stable slug","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job detail"},"404":{"description":"Unknown id"}}}},"/api/v1/providers":{"get":{"summary":"List providers","parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"type","in":"query","schema":{"type":"string","enum":["people","software","agent"]}},{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated providers"}}}},"/api/v1/providers/{slug}":{"get":{"summary":"Provider by stable slug","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Provider detail plus a certification URL. The catalog record is not a verification."},"404":{"description":"Unknown id"}}}},"/api/v1/providers/{slug}/certification":{"get":{"summary":"Public certification badge. No email.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Not certified unless evidence is on the queue"}}}},"/api/v1/cases":{"get":{"summary":"List peer cases","parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"job","in":"query","schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated cases"}}}},"/api/v1/cases/{slug}":{"get":{"summary":"Case by stable slug","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Case detail"},"404":{"description":"Unknown id"}}}},"/api/v1/hotels":{"get":{"summary":"Search the open hotel graph","parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"country","in":"query","schema":{"type":"string"}},{"name":"city","in":"query","schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated hotels. Tags are not a complete inventory."}}}},"/api/v1/transactions":{"get":{"summary":"Buy/sell tapes","parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated tapes. Missing prices stay missing."}}}},"/api/v1/outcomes":{"get":{"summary":"Reported outcomes. No contact email.","responses":{"200":{"description":"Reported until verified. insurance false."}}},"post":{"summary":"Queue a hotel outcome report. Rate-limited. Status reported.","responses":{"202":{"description":"Reported. verified false. insurance false."},"400":{"description":"Invalid packet"},"429":{"description":"Rate limit"}}}},"/api/v1/listings/claim":{"post":{"summary":"Queue a supplier claim. Rate-limited. Status unverified pending.","responses":{"202":{"description":"Unverified pending."},"400":{"description":"Invalid packet"},"429":{"description":"Rate limit"}}}},"/mcp":{"get":{"summary":"MCP discovery for Harbor by Signalry","responses":{"200":{"description":"Tool names and protocol"}}},"post":{"summary":"MCP Streamable HTTP JSON-RPC","responses":{"200":{"description":"JSON-RPC result"}}}},"/api/jobs":{"get":{"summary":"List jobs","parameters":[{"name":"persona","in":"query","schema":{"type":"string","enum":["ops","owner","tech"]}}],"responses":{"200":{"description":"Job summaries"}}}},"/api/jobs/{slug}":{"get":{"summary":"Job how-and-who brief","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Job detail"},"404":{"description":"Unknown slug"}}}},"/api/jobs/{slug}/providers":{"get":{"summary":"Providers ranked for a job","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Providers for this JTBD"},"404":{"description":"Unknown slug"}}}},"/api/jobs/{slug}/cases":{"get":{"summary":"Peer cases for a job","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Cases for this JTBD"},"404":{"description":"Unknown slug"}}}},"/api/pilots":{"get":{"summary":"Pilot request schema (nothing stored server-side)","responses":{"200":{"description":"Rules and empty items"}}},"post":{"summary":"Echo a pilot / waitlist request (stub)","responses":{"202":{"description":"Echo only — persist in the browser"},"400":{"description":"Missing fields or suspected PII"}}}},"/api/health":{"get":{"summary":"Live ops health — catalog and graph counts, no PII","responses":{"200":{"description":"status ok plus catalog and graph tallies"}}}},"/api/catalog":{"get":{"summary":"Compact catalog index — slugs, titles, counts","responses":{"200":{"description":"Jobs, providers, and cases as an id index"}}}},"/api/providers":{"get":{"summary":"List providers","parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["people","software","agent","human"]}}],"responses":{"200":{"description":"Provider summaries"}}}},"/api/providers/{slug}":{"get":{"summary":"Provider detail","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Provider detail"},"404":{"description":"Unknown slug"}}}},"/api/cases":{"get":{"summary":"List case studies","parameters":[{"name":"job","in":"query","schema":{"type":"string"}},{"name":"persona","in":"query","schema":{"type":"string","enum":["ops","owner","tech"]}}],"responses":{"200":{"description":"Case summaries"}}}},"/api/cases/{slug}":{"get":{"summary":"Case study detail","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Case detail"},"404":{"description":"Unknown slug"}}}},"/api/search":{"get":{"summary":"Search jobs, providers, and cases by JTBD language","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"}},{"name":"persona","in":"query","schema":{"type":"string","enum":["ops","owner","tech","all"]}},{"name":"lite","in":"query","schema":{"type":"string","enum":["1","true"]},"description":"Job hints only — skips providers, cases, and the hotel graph. Used by the homepage combobox."}],"responses":{"200":{"description":"Ranked jobs plus matching providers and cases","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}}}},"post":{"summary":"Search (JSON body { q, persona })","responses":{"200":{"description":"Ranked jobs plus matching providers and cases"}}}},"/api/packs":{"get":{"summary":"Weekly ops, owner, buy/sell, and sale-ready pack job lists","responses":{"200":{"description":"Curated job packs"}}}},"/api/outcomes":{"get":{"summary":"Verified Outcomes v1 schema — design partner, not insurance (no PII stored)","responses":{"200":{"description":"Program flags, schema, rules, and a synthetic example"}}},"post":{"summary":"Echo a verified-outcome underwriting packet (stub)","description":"Returns 202 with a demand-signal packet. licensed is always false. Harbor is not licensed to underwrite. Do not send guest names.","responses":{"202":{"description":"Echoed underwriting packet — not a quote or binder"},"400":{"description":"Missing fields or PII language"}}}},"/api/graph":{"get":{"summary":"Hotel graph start — discover endpoints","description":"Read-only index. Does not load the snapshot. units names byCountry (France 6,736 country hotels), byCountryGeo (France /hotels/geo?country=France), byCity (Paris 2,629 city tags), byCityBubble (Paris 2,625 map-bubble houses), byCityGeo (Paris /hotels/geo?country=France&city=Paris), byCityExtract (Paris 2,056 OSM extract houses), byCityExtractUrl (Paris /hotels/geo?metros=1&q=Paris), and byOsmMarket (Paris 2,056 OSM extract houses) so those counts and hops are not interchangeable. byCityExtract is the country→city index of the same extract as byOsmMarket — not the same key. osmMarkets stays a key→count map of the same extract houses. SAME_AS is merge edges. Amenity types stay 21. Export is a sample. No invented prices. No OTA scrape.","responses":{"200":{"description":"Start URLs, browse hops, units, and honesty rules"}}}},"/api/graph/hotels":{"get":{"summary":"Open hotel graph list (Wikidata / OSM)","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Substring tokens (AND). Quoted phrases stay intact. Unmatched quotes are stripped. Amenity labels and country aliases (USA, UK, China) match. Tokens of 3 characters or fewer need a word boundary. Name ranks first. Not fuzzy."},{"name":"country","in":"query","schema":{"type":"string"},"description":"Canonical name, ISO code, or alias (USA, UK, China)."},{"name":"city","in":"query","schema":{"type":"string"},"description":"Exact city tag, not a substring."},{"name":"brand","in":"query","schema":{"type":"string"},"description":"Exact brand tag. Hilton ≠ Hilton Garden Inn."},{"name":"source","in":"query","schema":{"type":"string"}},{"name":"lodgingType","in":"query","schema":{"type":"string"},"description":"Exact lodging-type tag. Aliases: guest house, guesthouse, B&B, campsite. boutique ≠ historic boutique."},{"name":"amenity","in":"query","schema":{"type":"string"},"description":"Amenity id (amenity-wifi) or label (Wi-Fi, EV charging). OSM/Wikidata tags only — missing tag ≠ hotel lacks the amenity. Snapshot amenity-type count is the exported seed."},{"name":"chainId","in":"query","schema":{"type":"string"},"description":"Exact chain node id (MEMBER_OF). Brand-label fallback only when the hotel has no chainIds."},{"name":"hasCoords","in":"query","schema":{"type":"string"}},{"name":"hasSameAs","in":"query","schema":{"type":"string"},"description":"1 — houses with an OSM↔Wikidata SAME_AS merge. Not a second listing."},{"name":"hasBrand","in":"query","schema":{"type":"string"},"description":"1 — houses with an OSM / Wikidata brand tag. Not a franchise census."},{"name":"harbor","in":"query","schema":{"type":"string"}},{"name":"minLat","in":"query","schema":{"type":"number"},"description":"WGS84 south. All four bbox params required."},{"name":"minLng","in":"query","schema":{"type":"number"},"description":"WGS84 west."},{"name":"maxLat","in":"query","schema":{"type":"number"},"description":"WGS84 north."},{"name":"maxLng","in":"query","schema":{"type":"number"},"description":"WGS84 east."},{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Paginated hotels. List items include amenityIds and amenities [{id,label}] (OSM/Wikidata tags only). Never treat a missing tag as 'the hotel lacks the amenity'. A country lock with no other slice adds bySource (France OSM 3,313 · Wikidata 4,225 — not a partition of total 6,736) plus lodgingType (France hotel 6,626 · OSM 3,203 · Wikidata 4,225 — Wikidata 4,225 is the same number as France country Wikidata, not the same unit; OSM 3,203 ≠ France OSM 3,313) plus amenity (France Wi-Fi 759 · OSM 759 · Wikidata 209 — not country hotels 6,736, not worldwide Wi-Fi 12,531) plus countryHotels + geoUrl (France 6,736 — the same number as total, not a second tally, not city tags, not the map bubble, not France hotel 6,626, not France Wi-Fi 759). A country alias resolves the same way a city lock does (?country=usa is United States 11,806 country hotels plus hotel 11,263 plus Wi-Fi 1,342). An ISO country code on a lock resolves from houses (?country=FR is France 6,736 plus hotel 6,626 plus Wi-Fi 759; ?country=FR&city=Paris is Paris 2,629 city tags — not zero source houses). A city lock adds in-city bySource (Paris OSM 2,478 · Wikidata 637) plus lodgingType (Paris hotel 2,554 · OSM 2,403 · Wikidata 637 — not city tags 2,629, not France hotel 6,626) plus amenity (Paris wheelchair 576 · OSM 576 · Wikidata 142 — not France Wi-Fi 759, not Paris Wi-Fi 505) plus bubbleHouses + geoUrl (Paris 2,625 — not city tags 2,629, not Paris OSM 2,478, not Paris hotel 2,554, not Paris OSM extract 2,056) plus cityTags (Paris 2,629 — the same number as total, not a second tally, not the map bubble 2,625, not Paris OSM 2,478, not Paris hotel 2,554, not Paris OSM extract 2,056) plus extractHouses + extractUrl when the city is a map-bubble metro (Paris 2,056 — not city tags 2,629, not the map bubble 2,625). A city without a map pin omits bubbleHouses, geoUrl, extractHouses, and extractUrl. An amenity, source, or other slice omits those envelope fields."}}}},"/api/graph/hotels/{id}":{"get":{"summary":"Hotel graph detail","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Hotel + Harbor links"},"404":{"description":"Unknown id"}}}},"/api/graph/hotels/{id}/estimate":{"get":{"summary":"Labeled hotel value estimate (not an appraisal)","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"occ","in":"query","schema":{"type":"number"}},{"name":"adr","in":"query","schema":{"type":"number"}},{"name":"cap","in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"Direct cap, comps, sensitivity"}}}},"/api/graph/hotels/{id}/comps":{"get":{"summary":"Scored transaction comps for a hotel","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Comps with scores and reasons"}}}},"/api/graph/search":{"get":{"summary":"Search hotels and graph nodes","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Token AND. Quoted phrases stay intact. Empty q returns empty lists. Nodes include url hops. Country aliases and amenity labels match. Short tokens need a word boundary."}],"responses":{"200":{"description":"Hotels plus Chain / City / Country / Amenity / Source nodes with url. City nodes print cityTags (ledger) plus bySource in-city source houses (Paris OSM 2,478 ≠ 2,629 city tags) plus amenity the top amenity-tag mix (Paris wheelchair 576 · OSM 576 · Wikidata 142 — not France Wi-Fi 759, not Paris Wi-Fi 505) plus lodgingType the top lodging-type mix (Paris hotel 2,554 · OSM 2,403 · Wikidata 637 — not city tags 2,629, not France hotel 6,626). geoUrl + bubbleHouses only on a map-bubble city — do not fold those counts. extractHouses + extractUrl only when the city label is an Overpass metro (Paris 2,056 — not city tags 2,629, not the map bubble 2,625). Country nodes print countryHotels + geoUrl (same number as hotels — ledger country hotels, not city tags, not map bubbles, not OSM extracts) plus bySource in-country source houses (France OSM 3,313 ≠ 6,736) plus amenity the top amenity-tag mix (France Wi-Fi 759 · OSM 759 · Wikidata 209 — not country hotels 6,736, not worldwide Wi-Fi 12,531) plus lodgingType the top lodging-type mix (France hotel 6,626 · OSM 3,203 · Wikidata 4,225 — Wikidata 4,225 is the same number as France country Wikidata, not the same unit; OSM 3,203 ≠ France OSM 3,313). Chain nodes print taggedHouses + mapped/mappedPct + bySource + types + amenities + countries + cities (MEMBER_OF tagged houses, MEMBER_OF source houses, MEMBER_OF lodging-type / amenity tags, and MEMBER_OF houses in that country / city — Ibis hotel 162 · OSM 161 · Wikidata 1 is not folio hotel 213; wheelchair 89 · OSM 89 is not folio wheelchair 116; France 60 MEMBER_OF houses · OSM 60 is not folio France 63; Paris 43 MEMBER_OF houses · OSM 43 is not folio Paris 46; not a franchise census, not the folio brand-fallback walk — Ibis OSM 161 ≠ 162 tagged ≠ folio OSM 212). GET /api/graph/chains list items stay MEMBER_OF tagged houses only — no types / amenities / countries / cities. Amenity nodes on the 21 seed types print taggedHousesWorldwide (same number as taggedHouses — worldwide seed totals, not selected.byCountry amenity tags) + mapped/mappedPct + sameAs + brand + bySource + types + countries + cities (Wi-Fi OSM 12,531 is the same number as tagged houses, not the same unit, not worldwide OSM 47,981; hotel 10,545 lodging-type tags · OSM 10,545 · Wikidata 3,823 is the same mix as amenity folio selected.types, not tagged houses 12,531, not France hotel 6,626; United States 1,342 amenity tags · OSM 1,342 · Wikidata 402 is the same mix as amenity folio selected.byCountry, not tagged houses 12,531, not France Wi-Fi 759, not Paris Wi-Fi 505; Paris 505 amenity tags · OSM 505 · Wikidata 98 is the same mix as amenity folio selected.byCity, not France Wi-Fi 759, not Paris city tags 2,629; SAME_AS is houses with a partner, not coverage merge edges; brand is a tag, not a franchise census). GET /api/graph/amenities list items stay tagged houses only — no types / countries / cities. Source nodes with a hop print sourceHouses (Wikidata / OSM / Harbor seed — not Wikipedia extracts). Wikipedia Source url is null."}}}},"/api/graph/chains":{"get":{"summary":"Top tagged hotel chains","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Token AND on chain label / id."},{"name":"limit","in":"query","schema":{"type":"integer"},"description":"Default 80, max 200."}],"responses":{"200":{"description":"Chain nodes ranked by MEMBER_OF tagged house count. Items print taggedHouses + mapped/mappedPct + bySource among those MEMBER_OF houses (Ibis OSM 161 ≠ folio OSM 212). Tags only — not a franchise census."}}}},"/api/graph/chains/{id}":{"get":{"summary":"Chain detail plus paginated hotels","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"page","in":"query","schema":{"type":"integer"}},{"name":"limit","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Chain + mapped + bySource + bySourceMemberOf + byCountry + byCountryMemberOf + byCountrySources + byCountryMemberOfSources + types + amenities + cities + hotels. count / folioHouses / byCountry / byCountrySources / bySource / types / amenities / cities is the brand-fallback folio walk; taggedHouses / byCountryMemberOf / byCountryMemberOfSources / bySourceMemberOf is MEMBER_OF only (Ibis 213 ≠ 162; Ibis OSM 212 folio ≠ 161 MEMBER_OF; France Ibis 63 folio · OSM 63 · Wikidata 3 · 60 MEMBER_OF · OSM 60 MEMBER_OF source houses). Ibis types hotel 213 · OSM 212 · Wikidata 52 · 162 MEMBER_OF · OSM 161 MEMBER_OF source houses · Wikidata 1 MEMBER_OF source houses — same number as folio houses, not MEMBER_OF tagged 162 as a unit. Type rows add memberOf + bySourceMemberOf. Ibis wheelchair 116 amenity tags · OSM 116 · Wikidata 27 · 89 MEMBER_OF · OSM 89 MEMBER_OF source houses — not worldwide Wi-Fi 12,531, not France Wi-Fi 759. Amenity rows add memberOf + bySourceMemberOf. Ibis Paris 46 folio houses · OSM 46 · Wikidata 3 · 43 MEMBER_OF · OSM 43 MEMBER_OF source houses — not France Ibis 63, not Ibis folio 213, not Paris city tags 2,629. City rows add bySourceMemberOf. byCountry is the full folio country map (Ibis 24 known countries — leftover 8 after the top 16 on the human folio hop ?all=1, not country hotels 6,736, not worldwide MEMBER_OF 162). List items omit types / amenities / cities / byCountrySources / byCountryMemberOfSources. Not a franchise census."},"404":{"description":"Unknown chain"}}}},"/api/graph/amenities":{"get":{"summary":"Amenity tag types on this snapshot","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Token AND on amenity label / id / aliases."},{"name":"amenity","in":"query","schema":{"type":"string"},"description":"Seed type id or label. Adds selected.byCountry (amenity tags in that country — not countryHotels, not taggedHousesWorldwide), selected.byCountrySources (OSM / Wikidata among those tags — France Wi-Fi OSM 759 ≠ worldwide Wi-Fi OSM 12,531 ≠ France OSM 3,313), selected.byCity (amenity tags in that city — Paris Wi-Fi 505 ≠ France Wi-Fi 759 ≠ Paris city tags 2,629), selected.byCitySources (OSM / Wikidata among those city amenity tags — Paris Wi-Fi OSM 505 ≠ Paris OSM 2,478), selected.types (lodging-type tags among amenity-tagged houses — Wi-Fi hotel 10,545 · OSM 10,545 · Wikidata 3,823 ≠ tagged houses 12,531 ≠ France hotel 6,626 ≠ amenity types 21), mapped, sameAs (houses), brand, and bySource when the type is on this export. EV / shuttle / business centre stay labels until a re-export."}],"responses":{"200":{"description":"Amenity types with tagged-house counts. Items name mapped/sameAs/brand/bySource. List items omit types (index cards print the top lodging-type mix — Wi-Fi hotel 10,545 · OSM 10,545 · Wikidata 3,823). selected.byCountry is amenity tags in that country — not countryHotels, not taggedHousesWorldwide. selected.byCountrySources is OSM / Wikidata source houses among those amenity tags (France Wi-Fi OSM 759 ≠ 12,531). selected.byCity is amenity tags in that city (Paris Wi-Fi 505 ≠ France Wi-Fi 759 ≠ Paris city tags 2,629). selected.byCitySources is OSM / Wikidata among those city amenity tags. selected.types is lodging-type tags among amenity-tagged houses (Wi-Fi hotel 10,545 ≠ 12,531). OSM/Wikidata tags only. Type count is the exported seed. Missing tag ≠ the hotel lacks the amenity."}}}},"/api/graph/stats":{"get":{"summary":"Coverage toward 164,808 Wikidata lodging items (P31/P279* Q27686)","responses":{"200":{"description":"Counts only: hotels, nodes, edges, amenities, chains, sameAs, osmMarkets (OSM extract houses — not city tags, not map-bubble size, not country hotels), bySource, field fill %. byCountry stays country hotels (France 6,736). byCountryGeo is the geo country card (France /hotels/geo?country=France — not city tags, not the map bubble, not an OSM extract). byCountrySources is OSM / Wikidata among those country hotels (France OSM 3,313 · Wikidata 4,225 — not a partition of 6,736, not worldwide OSM 47,981, not France Wi-Fi OSM 759). byCountryAmenity is the top amenity-tag mix (France Wi-Fi 759 · OSM 759 · Wikidata 209 — not country hotels 6,736, not worldwide Wi-Fi 12,531). byCityAmenity is the top amenity-tag mix in a map-bubble city (Paris wheelchair 576 · OSM 576 · Wikidata 142 — not France Wi-Fi 759, not Paris Wi-Fi 505, not city tags 2,629, not the map bubble 2,625, not Paris OSM extract 2,056). byCityLodgingType is the top lodging-type mix in a map-bubble city (Paris hotel 2,554 · OSM 2,403 · Wikidata 637 — not city tags 2,629, not the map bubble 2,625, not France hotel 6,626, not Paris wheelchair 576; Wikidata 637 is the same number as Paris city Wikidata, not the same unit; OSM 2,403 ≠ Paris OSM 2,478). byCitySources is OSM / Wikidata among those map-bubble city tags (Paris OSM 2,478 · Wikidata 637 — not a partition of city tags 2,629, not the map bubble 2,625, not France OSM 3,313, not Paris hotel OSM 2,403). byCity is ledger city tags in a map-bubble city (Paris 2,629 — not the map bubble 2,625, not Paris OSM 2,478, not Paris hotel 2,554, not Paris OSM extract 2,056). byCityBubble is map-bubble houses in that city (Paris 2,625 — not city tags 2,629, not Paris OSM 2,478, not Paris hotel 2,554, not Paris OSM extract 2,056). byCityGeo is the map-bubble geo hop (Paris /hotels/geo?country=France&city=Paris — not city tags 2,629, not the map bubble 2,625, not Paris OSM extract 2,056). byCityExtract is OSM extract houses on a map-bubble metro (Paris 2,056 — not city tags 2,629, not the map bubble 2,625, not Paris OSM 2,478). byCityExtractUrl is the same extract hop as a hotels city lock (Paris /hotels/geo?metros=1&q=Paris). byCity and byCityBubble stay numbers. byOsmMarket is OSM extract houses on an Overpass metro bbox plus extractUrl (Paris 2,056 — not city tags 2,629, not the map bubble 2,625, not Paris OSM 2,478). osmMarkets stays a key→count map. Not every ledger city. byCountryLodgingType is the top lodging-type mix (France hotel 6,626 · OSM 3,203 · Wikidata 4,225 — Wikidata 4,225 is the same number as France country Wikidata, not the same unit; OSM 3,203 ≠ France OSM 3,313). GET /api/health stays coverage.json. Licenses: Wikidata CC0, OSM ODbL. Progress toward 164,808 is a count, not completeness."}}}},"/api/graph/subgraph":{"get":{"summary":"1–3 hop neighborhood (place/source/NEAR edges reconstructed)","parameters":[{"name":"hotelId","in":"query","required":true,"schema":{"type":"string"}},{"name":"depth","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"Nodes and edges"}}}},"/api/graph/export":{"get":{"summary":"JSON-LD or GraphML sample dump (≤250 hotels — not the full graph)","parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["jsonld","graphml"]}}],"responses":{"200":{"description":"Sample dump. Paginate /api/graph/hotels or use snapshot.json.gz for the full list."}}}},"/api/graph/transactions":{"get":{"summary":"Public hotel transactions (cited or unknown — never invented)","parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"market","in":"query","schema":{"type":"string"}},{"name":"segment","in":"query","schema":{"type":"string"}},{"name":"priced","in":"query","schema":{"type":"string"}},{"name":"hotelId","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Transaction nodes"}}}},"/api/graph/transactions/search":{"get":{"summary":"Search public transactions","parameters":[{"name":"q","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Matching transactions"}}}},"/api/graph/brokers":{"get":{"summary":"Broker and practice-desk nodes","parameters":[{"name":"q","in":"query","schema":{"type":"string"}},{"name":"market","in":"query","schema":{"type":"string"},"description":"Market id"},{"name":"firm","in":"query","schema":{"type":"string"},"description":"Firm id"}],"responses":{"200":{"description":"Brokers"}}}},"/api/graph/firms":{"get":{"summary":"Brokerage firms","responses":{"200":{"description":"Firms"}}}},"/api/graph/markets":{"get":{"summary":"Hotel markets","responses":{"200":{"description":"Markets"}}}},"/api/deal-frame":{"get":{"summary":"Deal Frame schema (browser-local IC artifact, not an appraisal)","responses":{"200":{"description":"Rules, watermark, empty example"}}},"post":{"summary":"Echo a Deal Frame (stub — persist in the browser)","responses":{"202":{"description":"Echo only — Harbor does not host the file"},"400":{"description":"Bad JSON or suspected PII"}}}},"/api/graph/cap-rates":{"get":{"summary":"Harbor research cap-rate defaults (not a proprietary survey)","responses":{"200":{"description":"Bands + methodology"}}}},"/api/graph/comps":{"get":{"summary":"COMP_TO neighbors for a hotel","parameters":[{"name":"hotelId","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Comps"}}}}},"components":{"schemas":{"TrustLevel":{"type":"string","enum":["advice","claimed","verified"]},"Persona":{"type":"string","enum":["ops","owner","tech"]},"Job":{"type":"object","required":["id","slug","title","persona"],"properties":{"id":{"type":"string","description":"Equals slug. Resolve by this."},"slug":{"type":"string"},"url":{"type":"string"},"title":{"type":"string"},"persona":{"$ref":"#/components/schemas/Persona"},"jtbd":{"type":"string"},"outcome":{"type":"string"},"whenNotToBuy":{"type":"string","nullable":true},"claims":{"type":"array","items":{"type":"object","properties":{"level":{"$ref":"#/components/schemas/TrustLevel"},"text":{"type":"string"}}}}}},"Provider":{"type":"object","required":["id","slug","name","type","kind"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"type":{"type":"string","enum":["people","software","agent"]},"kind":{"type":"string","enum":["human","software","agent"]},"demo":{"type":"boolean"},"score":{"type":"object"},"claims":{"type":"array"}}},"Case":{"type":"object","required":["id","slug","jobId"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"jobId":{"type":"string"},"claim":{"type":"string","nullable":true},"evidence":{"type":"string","nullable":true},"caveat":{"type":"string","nullable":true},"metrics":{"type":"array"}}},"SearchResponse":{"type":"object","properties":{"q":{"type":"string"},"count":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/Job"}},"providers":{"type":"array","items":{"$ref":"#/components/schemas/Provider"}},"cases":{"type":"array","items":{"$ref":"#/components/schemas/Case"}}}}}}}