{"openapi":"3.1.0","info":{"title":"Manhattin Public API","version":"1.5.0","summary":"Read rental listings and submit leads for properties managed in Manhattin.","description":"Manhattin is property management software for Canada. This API is the integration surface\nfor a property or portfolio website: read the units you can advertise, read one unit in\ndetail, and post a contact-form submission back as a lead.\n\n## When to use this API\n\n- You are building or maintaining a rental website (WordPress, Next.js, static) and need a\n  live vacancy list for buildings managed in Manhattin.\n- You are wiring a contact or \"book a viewing\" form on such a site to the property manager.\n- You are an agency or listing portal syncing availability for partner buildings.\n\nIt is NOT a general property-management API: there is no endpoint for leases, rent\npayments, maintenance requests, accounting or tenant records, and no endpoint writes unit\nstate. Staff own that data inside Manhattin.\n\n## Three key families, three surfaces\n\n| Key prefix | Scope | Endpoints |\n| --- | --- | --- |\n| `pk_live_` | one property | `/api/public/availability`, `/api/public/units`, `/api/public/unit/{unit_id}`, `/api/public/inquiries` |\n| `ok_live_` | one organization, many properties | `/api/public/org/*` |\n| `agency_live_` | properties under a partnership agreement | `/api/v1/agency/availability` |\n\nSending a key to the wrong family returns `403 INVALID_KEY`. This is the single most\ncommon integration mistake.\n\n## Getting a key\n\nKeys are issued by Manhattin. Email developer@manhattin.com, or claim a key you were sent an\ninvitation for at https://manhattin.com/api-key/claim. The plain-text key is shown once and stored\nonly as a SHA-256 hash. Keep it server-side: `POST` on the inquiry endpoints creates lead\nrecords, so a leaked key lets anyone file inquiries against your property.\n\n## Conventions\n\n- No pagination, no cursors: list endpoints return the whole result set.\n- No webhooks. Poll on a schedule and cache on your side.\n- Errors are always JSON: `{ \"error\": \"<code>\", \"message\": \"<human text>\" }`.\n- Every response carries `RateLimit`, `RateLimit-Policy`, `RateLimit-Limit`,\n  `RateLimit-Remaining` and `RateLimit-Reset`, plus `Retry-After` on a 429. The\n  publicly cacheable documents (`/openapi.json`, `/api/docs/*`) carry the policy\n  without live counters, because a shared cache would serve one client's numbers\n  to everyone.\n- To watch the live counters without a key, call `GET https://manhattin.com/api/public/pricing`:\n  it is unauthenticated and uncached, so it answers 200 with the full set. Browser\n  callers can read them cross-origin — every RateLimit header is named in\n  `Access-Control-Expose-Headers`.\n\n## MCP server\n\nThe same endpoints are exposed as sixteen Model Context Protocol tools at\n`https://manhattin.com/api/mcp`, over the Streamable HTTP transport (JSON-RPC 2.0, protocol\nversions `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`).\nDiscovery documents:\n\n- `https://manhattin.com/.well-known/mcp` — manifest (SEP-1960)\n- `https://manhattin.com/.well-known/mcp/server-card.json` — server card (SEP-1649)\n\nIt authenticates with the same keys, calls these same endpoints, and is subject to\nthe same rate limits — it is a client of this API, not a privileged path around it.\nThe tools `list_pricing_plans` and `get_openapi_specification` need no key.\n\nFive further tools serve the developer building an integration rather than the site\nrunning it — they read the API-key console: which property a key is bound to, what\nthe site actually sent, which posted fields the endpoint ignored. Those take a\nconsole token in `X-Manhattin-Console-Token`, not an API key, and contact details\nin the submissions view are redacted unless explicitly requested.\n\n## Function calling\n\nEvery operation here is also published as a ready-made tool at `https://manhattin.com/tools.json`,\nin both the OpenAI `function` shape and the Anthropic `input_schema` shape. Use it\nrather than converting this document yourself: the flattening of path, query, header\nand body parameters into one JSON Schema is the one we intend, and the five\noperations that take no parameters carry an explicit empty object schema there\nrather than no schema at all.\n\n## Versioning and deprecation\n\nThe API is versioned in the URL path: `/api/public/*` and `/api/v1/*` are the current,\nstable surfaces, and a breaking change gets a new path rather than altering one already\nin use. Adding a field to a response is not a breaking change — ignore fields you do not\nrecognise.\n\nA retiring endpoint announces it in its own responses, so no changelog needs watching:\n\n- `Deprecation` (RFC 9745) — an `@`-prefixed epoch second, when it became deprecated.\n- `Sunset` (RFC 8594) — an HTTP-date, when it stops working.\n- `Link: <https://manhattin.com/developers>; rel=\"deprecation\"` — what to do instead.\n- `\"deprecated\": true` on the operation in this document.\n\nAfter the sunset date it answers `410 ENDPOINT_WITHDRAWN` and keeps sending both\nheaders, so a client arriving late still learns when the ground moved. A withdrawn\nendpoint never degrades to a `404`, and its path is never reused.\n\nAt least 90 days separate the `Deprecation` date from the `Sunset` date. The only\nexception is a retirement forced by a security or legal obligation, where the window\nmay be shorter; the headers carry the real dates either way, so read them rather than\ncounting on the minimum.\n\n## Withdrawn\n\n`POST /api/public/units/update-status` was withdrawn on 2026-08-04 and answers\n`410 ENDPOINT_WITHDRAWN`. It is deliberately absent from `paths`: an operation\nlisted there becomes a callable tool in a function-calling bridge, and a tool\nthat can only fail is worse than no tool. Unit availability is owned by staff in\nManhattin.","contact":{"name":"Manhattin developer support","email":"developer@manhattin.com","url":"https://manhattin.com/developers"},"license":{"name":"Proprietary","url":"https://manhattin.com/terms"},"termsOfService":"https://manhattin.com/terms"},"servers":[{"url":"https://manhattin.com","description":"Production"}],"externalDocs":{"description":"Prose documentation, including the agent brief","url":"https://manhattin.com/developers"},"tags":[{"name":"Property API","description":"Property-scoped endpoints. The property is resolved from the `pk_live_` key; there is no property parameter.","externalDocs":{"description":"API reference","url":"https://manhattin.com/api/docs/api/API_REFERENCE.md?raw=1"}},{"name":"Organization API","description":"Portfolio-wide endpoints for an `ok_live_` key. Response shapes differ from the property API.","externalDocs":{"description":"Organization guide","url":"https://manhattin.com/api/docs/api/ORG_API_GUIDE.md?raw=1"}},{"name":"Agency API","description":"Availability sync for agency partners holding an `agency_live_` key."},{"name":"Discovery","description":"Unauthenticated endpoints an agent can call to understand the product."}],"security":[{"PropertyApiKey":[]}],"paths":{"/openapi.json":{"get":{"operationId":"getOpenApiSpecification","tags":["Discovery"],"summary":"Fetch this OpenAPI document","description":"Returns the machine-readable description of every endpoint below. Requires no authentication and no key, so an agent can discover the API surface before anyone has issued it credentials. The YAML form is at /api/openapi.yaml.","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OpenApiDocument"}}}}}}},"/api/docs/{path}":{"get":{"operationId":"getDocumentationFile","tags":["Discovery"],"summary":"Fetch an integration guide as markdown or HTML","description":"Serves the prose documentation under `docs/api/` and `docs/wordpress-plugin/`. Markdown files render as styled HTML for a browser and as raw markdown for an agent — set `raw=1`, `format=md`, or send `Accept: text/markdown`. No key required. Known files: `api/FOR_AGENTS.md`, `api/QUICK_START.md`, `api/API_REFERENCE.md`, `api/ORG_API_GUIDE.md`, `api/CLIENT_OPTIONS.md`, `api/TESTING.md`.","security":[],"parameters":[{"name":"path","in":"path","required":true,"description":"Folder and file, slash-separated, e.g. `api/FOR_AGENTS.md`. Only the `api` and `wordpress-plugin` folders are served; anything else answers 404.","schema":{"type":"string","pattern":"^(api|wordpress-plugin)/.+$"},"example":"api/FOR_AGENTS.md"},{"name":"raw","in":"query","required":false,"description":"Set to `1` for plain markdown with no HTML wrapper.","schema":{"type":"string","enum":["1"]},"example":"1"},{"name":"format","in":"query","required":false,"description":"Set to `md` for plain markdown. Equivalent to `raw=1`.","schema":{"type":"string","enum":["md"]}},{"name":"Accept","in":"header","required":false,"description":"Send `text/markdown` to receive markdown instead of the HTML rendering, without a query parameter.","schema":{"type":"string"},"example":"text/markdown"}],"responses":{"200":{"description":"The document, as markdown or as rendered HTML.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"description":"Always `public, max-age=3600`.","schema":{"type":"string"}}},"content":{"text/markdown":{"schema":{"type":"string"}},"text/html":{"schema":{"type":"string"}}}},"400":{"description":"The path tried to escape the documentation folder.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid path","message":"Invalid path"}}}},"404":{"description":"No such document, or a folder that is not published.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"File not found","message":"File not found"}}}},"429":{"description":"Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded"}}}}}}},"/api/public/pricing":{"get":{"operationId":"listPublicPricingPlans","tags":["Discovery"],"summary":"List published subscription plans","description":"Returns the Manhattin subscription plans shown on the public pricing section, including the free tier. Requires no authentication — this is the endpoint to call to confirm unauthenticated access works before requesting a key.","security":[],"responses":{"200":{"description":"Published plans, in display order.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PricingResponse"}}}},"500":{"description":"Unhandled failure. Retry with backoff.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error","message":"An unexpected error occurred"}}}}}}},"/api/public/availability":{"get":{"operationId":"listPropertyAvailability","tags":["Property API"],"summary":"List units that can be advertised on this property","description":"Marketable units for the property behind the key: vacant units with no lease starting\nlater than today, plus occupied units whose active lease ends within three calendar\nmonths. Units at status `leased`, `reserved` or `maintenance` are never returned.\n\nCommercial units are NOT filtered out — check `classification` if your site is\nresidential-only. This endpoint sends no `Cache-Control`; cache it yourself.\n\nNot interchangeable with `listPropertyUnits`: the rent field is `rent` here and\n`monthly_rent` there, and the two disagree about which vacant units count as available.","parameters":[{"name":"unit_type","in":"query","required":false,"description":"Exact match on the unit type string, e.g. `4 1/2`. Free text, not a fixed vocabulary.","schema":{"type":"string"},"example":"4 1/2"},{"name":"bedrooms","in":"query","required":false,"description":"Exact match on bedroom count.","schema":{"type":"integer","minimum":0},"example":2},{"name":"min_rent","in":"query","required":false,"description":"Keep units whose market rent is greater than or equal to this value.","schema":{"type":"number","minimum":0},"example":1000},{"name":"max_rent","in":"query","required":false,"description":"Keep units whose market rent is less than or equal to this value.","schema":{"type":"number","minimum":0},"example":2000}],"responses":{"200":{"description":"Matching units.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyAvailabilityResponse"}}}},"401":{"description":"No API key was supplied.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing API key","message":"Please provide an API key in the X-API-Key header or Authorization header"}}}},"403":{"description":"The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"INVALID_KEY","message":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded"}}}},"500":{"description":"Unhandled failure. Retry with backoff.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error","message":"An unexpected error occurred"}}}}}}},"/api/public/units":{"get":{"operationId":"listPropertyUnits","tags":["Property API"],"summary":"List every unit on this property, available and not","description":"The whole building roster plus active promotions. Every active unit appears in exactly\none of `units` (advertisable) and `unavailable_units` (with a reason).\n\nAccepts no query parameters — filter client-side. There is no top-level `success`\nfield, so `if (data.success)` silently skips every unit here; check `response.ok` or\nthe presence of `units`. Responses carry `Cache-Control: public, max-age=300`, so a\nstatus change made by staff can take up to five minutes to appear.","responses":{"200":{"description":"The full roster.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Cache-Control":{"description":"Always `public, max-age=300`.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyUnitsResponse"}}}},"401":{"description":"No API key was supplied.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing API key","message":"Please provide an API key in the X-API-Key header or Authorization header"}}}},"403":{"description":"The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"INVALID_KEY","message":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded"}}}},"500":{"description":"Unhandled failure. Retry with backoff.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error","message":"An unexpected error occurred"}}}}}}},"/api/public/unit/{unit_id}":{"get":{"operationId":"getPropertyUnit","tags":["Property API"],"summary":"Fetch one residential unit in detail","description":"Detail view for a unit on the property behind the key. Commercial units are never returned here — they answer 404, as do unknown, inactive, and other properties’ units. The response does not distinguish between those cases.","parameters":[{"name":"unit_id","in":"path","required":true,"description":"The `id` of a unit as returned by `listPropertyAvailability` or `listPropertyUnits`.","schema":{"type":"string","format":"uuid"},"example":"3f0b1c8e-6a2d-4b91-8f77-1d2c3e4a5b60"}],"responses":{"200":{"description":"The unit.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyUnitDetailResponse"}}}},"401":{"description":"No API key was supplied.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing API key","message":"Please provide an API key in the X-API-Key header or Authorization header"}}}},"403":{"description":"The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"INVALID_KEY","message":"Invalid API key"}}}},"404":{"description":"No residential unit with that id on this property.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unit not found","message":"The requested unit does not exist or is not available"}}}},"429":{"description":"Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded"}}}},"500":{"description":"Unhandled failure. Retry with backoff.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error","message":"An unexpected error occurred"}}}}}}},"/api/public/inquiries":{"post":{"operationId":"createPropertyInquiry","tags":["Property API"],"summary":"Submit a contact-form inquiry for this property","description":"Creates a marketing lead on the property behind the key. `email` is the only required\nfield. Fields not listed in the request schema are discarded silently — including\n`budget_range`; put that information in `message`.\n\nCloudflare Turnstile verification is in soft launch: send the token as\n`cf-turnstile-response` in the body or the `X-Turnstile-Token` header. A missing token\nis accepted today and will be rejected with `TURNSTILE_REQUIRED` once enforcement is on.\n\nResponds 201, not 200.","parameters":[{"name":"X-Turnstile-Token","in":"header","required":false,"description":"Cloudflare Turnstile token. Read before the body field of the same purpose. Soft-launch: a missing token is accepted today and will be rejected with `TURNSTILE_REQUIRED` once enforcement is on.","schema":{"type":"string"}},{"name":"CF-Turnstile-Response","in":"header","required":false,"description":"The same token under the name the Cloudflare widget uses.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyInquiryRequest"}}}},"responses":{"201":{"description":"Lead created.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InquiryCreatedResponse"}}}},"400":{"description":"Missing or malformed `email`, or a `unit_id` belonging to another property (`UNIT_MISMATCH`).","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid email","message":"Please provide a valid email address"}}}},"401":{"description":"No API key was supplied.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing API key","message":"Please provide an API key in the X-API-Key header or Authorization header"}}}},"403":{"description":"The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"INVALID_KEY","message":"Invalid API key"}}}},"404":{"description":"The supplied `unit_id` does not exist.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"UNIT_NOT_FOUND","message":"The specified unit could not be found."}}}},"429":{"description":"Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded"}}}},"500":{"description":"Unhandled failure. Retry with backoff.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error","message":"An unexpected error occurred"}}}}}}},"/api/public/org/properties":{"get":{"operationId":"listOrganizationProperties","tags":["Organization API"],"summary":"List every active property in the organization","description":"Returns each active property with its address, amenities, photos and unit counts, ordered by display name. Accepts no query parameters. `available_units` counts units sitting at raw status `vacant`; it is not the same figure as the `count` from `listOrganizationAvailability`, which applies the marketing rules.","security":[{"OrgApiKey":[]},{"OrgBearerKey":[]}],"responses":{"200":{"description":"The organization’s properties.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationPropertiesResponse"}}}},"401":{"description":"No API key was supplied.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing API key","message":"Please provide an API key in the X-API-Key header or Authorization header"}}}},"403":{"description":"The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"INVALID_KEY","message":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded"}}}},"500":{"description":"Unhandled failure. Retry with backoff.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error","message":"An unexpected error occurred"}}}}}}},"/api/public/org/availability":{"get":{"operationId":"listOrganizationAvailability","tags":["Organization API"],"summary":"List advertisable units across the whole portfolio","description":"Marketable units from every active property: vacant with no future lease signed, or occupied with an active lease ending within three calendar months and no successor. Includes both residential and commercial units. Sorted by property name, then unit number. An unknown `property_id` returns an empty successful result, not an error.","security":[{"OrgApiKey":[]},{"OrgBearerKey":[]}],"parameters":[{"name":"property_id","in":"query","required":false,"description":"Restrict to one property, as returned by `listOrganizationProperties`.","schema":{"type":"string","format":"uuid"}},{"name":"unit_type","in":"query","required":false,"description":"Exact match on the unit type string.","schema":{"type":"string"}},{"name":"bedrooms","in":"query","required":false,"description":"Exact match on bedroom count.","schema":{"type":"integer","minimum":0}},{"name":"min_rent","in":"query","required":false,"description":"Market rent greater than or equal to this value. Units with no market rent are excluded whenever either bound is supplied.","schema":{"type":"number","minimum":0}},{"name":"max_rent","in":"query","required":false,"description":"Market rent less than or equal to this value.","schema":{"type":"number","minimum":0}}],"responses":{"200":{"description":"Matching units, each carrying its property.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationAvailabilityResponse"}}}},"401":{"description":"No API key was supplied.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing API key","message":"Please provide an API key in the X-API-Key header or Authorization header"}}}},"403":{"description":"The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"INVALID_KEY","message":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded"}}}},"500":{"description":"Unhandled failure. Retry with backoff.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error","message":"An unexpected error occurred"}}}}}}},"/api/public/org/units":{"get":{"operationId":"listOrganizationUnits","tags":["Organization API"],"summary":"List every active unit in the portfolio with a computed availability status","description":"Every active, non-archived unit with an `availability_status`, the current lease end date, any successor lease start date, and a transfer intent date. Use this when you want to run your own display rules rather than the marketing rules baked into `listOrganizationAvailability`. Only `property_id` is supported as a filter.","security":[{"OrgApiKey":[]},{"OrgBearerKey":[]}],"parameters":[{"name":"property_id","in":"query","required":false,"description":"Restrict to one property. An unknown id returns an empty successful result.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Every active unit.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationUnitsResponse"}}}},"401":{"description":"No API key was supplied.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing API key","message":"Please provide an API key in the X-API-Key header or Authorization header"}}}},"403":{"description":"The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"INVALID_KEY","message":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded"}}}},"500":{"description":"Unhandled failure. Retry with backoff.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error","message":"An unexpected error occurred"}}}}}}},"/api/public/org/inquiries":{"post":{"operationId":"createOrganizationInquiry","tags":["Organization API"],"summary":"Submit a contact-form inquiry for the organization","description":"Creates a lead against the organization behind the key. `property_id` is optional — omit it for a portfolio-level lead. Supplying only `unit_id` infers the property. Responds 201. Note the asymmetric validation codes: a deactivated property in your own organization answers `PROPERTY_NOT_FOUND` (404), while a property belonging to another organization answers `PROPERTY_MISMATCH` (400).","security":[{"OrgApiKey":[]},{"OrgBearerKey":[]}],"parameters":[{"name":"X-Turnstile-Token","in":"header","required":false,"description":"Cloudflare Turnstile token. Read before the body field of the same purpose. Soft-launch: a missing token is accepted today and will be rejected with `TURNSTILE_REQUIRED` once enforcement is on.","schema":{"type":"string"}},{"name":"CF-Turnstile-Response","in":"header","required":false,"description":"The same token under the name the Cloudflare widget uses.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationInquiryRequest"}}}},"responses":{"201":{"description":"Lead created.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InquiryCreatedResponse"}}}},"400":{"description":"Missing or malformed `email`, or a `PROPERTY_MISMATCH` / `UNIT_MISMATCH` / `UNIT_PROPERTY_MISMATCH`.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"UNIT_PROPERTY_MISMATCH","message":"The specified unit does not belong to the specified property."}}}},"401":{"description":"No API key was supplied.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing API key","message":"Please provide an API key in the X-API-Key header or Authorization header"}}}},"403":{"description":"The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"INVALID_KEY","message":"Invalid API key"}}}},"404":{"description":"`PROPERTY_NOT_FOUND` or `UNIT_NOT_FOUND`.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"PROPERTY_NOT_FOUND","message":"The specified property could not be found."}}}},"429":{"description":"Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded"}}}},"500":{"description":"Unhandled failure. Retry with backoff.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error","message":"An unexpected error occurred"}}}}}}},"/api/v1/agency/availability":{"get":{"operationId":"listAgencyAvailability","tags":["Agency API"],"summary":"Sync unit availability for partner properties","description":"Every unit on the properties covered by your partnership agreements, split into `available` and `unavailable`, with a `meta` block giving counts and a pull timestamp. Rate limited to 5 requests per day per key, resetting at midnight UTC — sync on a schedule, never per page view. Keys expire when the partnership agreement ends.","security":[{"AgencyApiKey":[]}],"responses":{"200":{"description":"Availability snapshot.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgencyAvailabilityResponse"}}}},"401":{"description":"No API key was supplied.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing API key","message":"Please provide an API key in the X-API-Key header or Authorization header"}}}},"403":{"description":"The key is unknown, revoked, deactivated, of the wrong kind for this endpoint, or the browser Origin is not on the key allowlist.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"INVALID_KEY","message":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded. Per-key hourly budget (default 100/hour) or the per-IP burst limit (30/minute). Wait `Retry-After` seconds; the same headers appear on every response, so a client can pace itself before ever seeing this.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"RATE_LIMIT_EXCEEDED","message":"Rate limit exceeded"}}}},"500":{"description":"Unhandled failure. Retry with backoff.","headers":{"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimitPolicy"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Internal server error","message":"An unexpected error occurred"}}}}}}}},"components":{"headers":{"RateLimit":{"description":"Current state of the applicable policy, as an RFC 8941 structured field: `\"<policy>\";r=<remaining>;t=<seconds until reset>`.","schema":{"type":"string"},"example":"\"publicApi\";r=29;t=41"},"RateLimitPolicy":{"description":"The policy itself: `\"<policy>\";q=<requests>;w=<window in seconds>`. Constant for a given endpoint tier.","schema":{"type":"string"},"example":"\"publicApi\";q=30;w=60"},"RateLimitLimit":{"description":"Requests permitted in the current window.","schema":{"type":"integer","minimum":0},"example":30},"RateLimitRemaining":{"description":"Requests still permitted in the current window.","schema":{"type":"integer","minimum":0},"example":29},"RateLimitReset":{"description":"Seconds until the window resets. Note the legacy `X-RateLimit-Reset`, also sent, is an epoch timestamp in milliseconds instead.","schema":{"type":"integer","minimum":0},"example":41},"RetryAfter":{"description":"Seconds to wait before retrying. Sent on 429 only, and never zero.","schema":{"type":"integer","minimum":1},"example":41}},"securitySchemes":{"PropertyApiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"A property key: `pk_live_` followed by 64 lowercase hex characters (72 characters total). Bound to exactly one property. Checked before the Authorization header. Keep it server-side."},"PropertyBearerKey":{"type":"http","scheme":"bearer","bearerFormat":"pk_live_<64 hex>","description":"The same property key sent as `Authorization: Bearer pk_live_...`."},"OrgApiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"An organization key: `ok_live_...`, scoped to every active property in one organization. Rejected with `INVALID_KEY` on the property endpoints."},"OrgBearerKey":{"type":"http","scheme":"bearer","bearerFormat":"ok_live_...","description":"The same organization key sent as `Authorization: Bearer ok_live_...`."},"AgencyApiKey":{"type":"apiKey","in":"header","name":"X-API-Key","description":"An agency partner key, `agency_live_XXXX_YYYY...`, scoped to the properties in your partnership agreements."}},"schemas":{"OpenApiDocument":{"type":"object","title":"OpenApiDocument","description":"An OpenAPI 3.1 description document. Only the fields every reader needs are constrained here; the rest is the OpenAPI specification itself.","required":["openapi","info","paths"],"properties":{"openapi":{"type":"string","description":"Specification version, e.g. `3.1.0`."},"info":{"type":"object","description":"Title, version and contact details for this API.","required":["title","version"],"properties":{"title":{"type":"string"},"version":{"type":"string"},"description":{"type":"string"}}},"servers":{"type":"array","description":"Origins this document describes.","items":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"description":{"type":"string"}}}},"paths":{"type":"object","description":"Path item objects keyed by URL template.","additionalProperties":true},"components":{"type":"object","description":"Reusable schemas, headers and security schemes.","additionalProperties":true}}},"Error":{"type":"object","title":"Error","description":"Every error response, on every endpoint. `error` is a stable machine code for the documented codes (`INVALID_KEY`, `RATE_LIMIT_EXCEEDED`, `ORIGIN_NOT_ALLOWED`, `TURNSTILE_REQUIRED`, `UNIT_NOT_FOUND`, `UNIT_MISMATCH`, `PROPERTY_NOT_FOUND`, `PROPERTY_MISMATCH`, `UNIT_PROPERTY_MISMATCH`, `CONFIGURATION_ERROR`, `ENDPOINT_WITHDRAWN`, `NOT_FOUND`) and a short human label otherwise. Match on the HTTP status plus `error`. Retry 5xx with backoff; do not retry a 4xx other than 429.","required":["error","message"],"properties":{"error":{"type":"string","description":"Machine code or short label."},"message":{"type":"string","description":"Human-readable explanation."},"retryAfter":{"type":"integer","description":"Seconds to wait. Present on the per-IP burst 429 only."},"hint":{"type":"string","description":"What to do next, when the API can suggest one."},"documentation_url":{"type":"string","format":"uri","description":"Where the resolution is documented."}}},"UnitPhoto":{"type":"object","title":"UnitPhoto","description":"One photo. Extra keys may be present; store the object as received.","properties":{"url":{"type":"string","format":"uri","description":"Public URL of the image."},"storage_path":{"type":"string","description":"Internal storage key. Opaque."},"caption":{"type":["string","null"]},"is_primary":{"type":"boolean","description":"True for the unit’s lead image."},"uploaded_at":{"type":"string","format":"date-time"}}},"ParkingSpot":{"type":"object","title":"ParkingSpot","description":"A parking spot rentable with the unit.","properties":{"spot_number":{"type":"string"},"spot_type":{"type":"string","enum":["indoor","outdoor","covered","handicap"],"description":"Physical kind of spot."},"additional_cost":{"type":["number","null"]}}},"StorageUnit":{"type":"object","title":"StorageUnit","description":"A storage locker rentable with the unit.","properties":{"unit_number":{"type":"string"},"size":{"type":["string","null"]},"location":{"type":["string","null"]},"additional_cost":{"type":["number","null"]}}},"Promotion":{"type":"object","title":"Promotion","description":"An active, publishable promotion. `valid_from` / `valid_until` may be null, meaning no bound on that side; the API does not filter on them, so check the dates before displaying an offer.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description_en":{"type":["string","null"]},"description_fr":{"type":["string","null"]},"description_es":{"type":["string","null"]},"discount_type":{"type":"string","enum":["flat_off_market","percentage_off_market","free_month","rent_credit","custom","free_addon"],"description":"Decides which of `discount_value_cents` / `discount_percentage` is set."},"discount_value_cents":{"type":["integer","null"],"description":"Amount in cents. For a unit-targeted promotion this is the per-unit override."},"discount_percentage":{"type":["number","null"],"description":"Percentage, e.g. 10.00."},"duration_months":{"type":["integer","null"]},"valid_from":{"type":["string","null"]},"valid_until":{"type":["string","null"]},"discount_timing":{"type":"string","enum":["from_start","from_end"],"description":"Whether the discount applies at the beginning or end of the lease."}}},"AvailableUnit":{"type":"object","title":"AvailableUnit","description":"A unit returned by `listPropertyAvailability`. Rent is in `rent`.","required":["id","unit_number","availability_reason"],"properties":{"id":{"type":"string","format":"uuid"},"unit_number":{"type":"string"},"floor":{"type":["integer","null"]},"unit_type":{"type":["string","null"],"description":"Free text, e.g. `4 1/2`, `Retail`."},"classification":{"type":"string","enum":["residential","commercial"],"description":"Defaults to `residential` when unset. Filter on it if your site is residential-only."},"bedrooms":{"type":["integer","null"]},"bathrooms":{"type":["number","null"],"description":"One decimal place, e.g. 1.5."},"square_feet":{"type":["integer","null"]},"rent":{"type":["number","null"],"description":"The unit’s market rent."},"features":{"type":["array","null"],"items":{"type":"string"}},"parking_spots":{"type":"array","items":{"$ref":"#/components/schemas/ParkingSpot"}},"storage_units":{"type":"array","items":{"$ref":"#/components/schemas/StorageUnit"}},"primary_photo_url":{"type":["string","null"]},"photos":{"type":"array","items":{"$ref":"#/components/schemas/UnitPhoto"}},"floor_plan_url":{"type":["string","null"],"description":"Opaque URL carrying a `?v=` cache-busting token. Always use the latest value returned."},"floor_area_plan_url":{"type":["string","null"]},"unit_fact_sheet_url":{"type":["string","null"]},"is_affordable_housing":{"type":"boolean"},"availability_reason":{"type":"string","enum":["vacant","lease_ending_soon"],"description":"Why the unit is advertisable."},"lease_end_date":{"type":"string","format":"date","description":"Present ONLY when `availability_reason` is `lease_ending_soon`. Test for the key, not for null."}}},"RosterUnit":{"type":"object","title":"RosterUnit","description":"A unit returned by `listPropertyUnits`. Rent is in `monthly_rent`, not `rent`.","required":["id","unit_number","status"],"properties":{"id":{"type":"string","format":"uuid"},"unit_number":{"type":"string"},"status":{"type":"string","enum":["vacant","occupied","leased","maintenance","reserved"],"description":"Raw unit status. These five are the complete set."},"classification":{"type":"string","enum":["residential","commercial"]},"monthly_rent":{"type":["number","null"],"description":"Vacant: market rent, then current rent. Occupied: the lease rent, then market, then current."},"square_feet":{"type":["integer","null"]},"bedrooms":{"type":["integer","null"]},"bathrooms":{"type":["number","null"]},"floor":{"type":["integer","null"]},"unit_type":{"type":["string","null"]},"features":{"type":"array","items":{"type":"string"}},"is_affordable_housing":{"type":"boolean"},"availability_reason":{"type":"string","enum":["vacant","lease_ending_soon"]},"lease_end_date":{"type":"string","format":"date","description":"Only on `lease_ending_soon`."},"primary_photo_url":{"type":["string","null"]},"photos":{"type":"array","items":{"$ref":"#/components/schemas/UnitPhoto"}},"floor_plan_url":{"type":["string","null"]},"floor_area_plan_url":{"type":["string","null"]},"unit_fact_sheet_url":{"type":["string","null"]},"promotions":{"type":"array","description":"Property-wide promotions PLUS ones targeted at this unit. Property-wide entries repeat inside every unit — de-duplicate before rendering both lists.","items":{"$ref":"#/components/schemas/Promotion"}}}},"UnavailableUnit":{"type":"object","title":"UnavailableUnit","description":"A unit that cannot be advertised, and why.","required":["id","unit_number","unavailability_reason"],"properties":{"id":{"type":"string","format":"uuid"},"unit_number":{"type":"string"},"status":{"type":"string","enum":["vacant","occupied","leased","maintenance","reserved"]},"classification":{"type":"string","enum":["residential","commercial"]},"unavailability_reason":{"type":"string","enum":["reserved","maintenance","lease_active","future_lease"],"description":"`lease_active` means spoken for now; `future_lease` means vacant with a lease starting later than today."}}},"PropertyAvailabilityResponse":{"type":"object","title":"PropertyAvailabilityResponse","description":"The `listPropertyAvailability` envelope: a success flag, the building amenities, a count and the units.","required":["success","count","units"],"properties":{"success":{"type":"boolean","description":"Always true on a 200 from this endpoint."},"property_amenities":{"type":"array","description":"Building-level amenity labels. Free text.","items":{"type":"string"}},"count":{"type":"integer","description":"Length of `units`."},"units":{"type":"array","items":{"$ref":"#/components/schemas/AvailableUnit"}}}},"PropertyUnitsResponse":{"type":"object","title":"PropertyUnitsResponse","description":"Note the absence of a `success` field — check `response.ok` instead.","required":["units","count","unavailable_units","unavailable_count"],"properties":{"property_amenities":{"type":"array","items":{"type":"string"}},"promotions":{"type":"array","description":"Property-wide promotions only.","items":{"$ref":"#/components/schemas/Promotion"}},"units":{"type":"array","items":{"$ref":"#/components/schemas/RosterUnit"}},"count":{"type":"integer"},"unavailable_units":{"type":"array","items":{"$ref":"#/components/schemas/UnavailableUnit"}},"unavailable_count":{"type":"integer"}}},"PropertyUnitDetailResponse":{"type":"object","title":"PropertyUnitDetailResponse","description":"The `getPropertyUnit` envelope: a success flag and one residential unit.","required":["success","unit"],"properties":{"success":{"type":"boolean"},"unit":{"type":"object","description":"Always residential — commercial units answer 404 on this endpoint.","properties":{"id":{"type":"string","format":"uuid"},"unit_number":{"type":"string"},"floor":{"type":["integer","null"]},"unit_type":{"type":["string","null"]},"bedrooms":{"type":["integer","null"]},"bathrooms":{"type":["number","null"]},"square_feet":{"type":["integer","null"]},"rent":{"type":["number","null"],"description":"Market rent when `status` is `vacant`, otherwise the current rent."},"status":{"type":"string","enum":["vacant","occupied","leased","maintenance","reserved"]},"is_affordable_housing":{"type":"boolean"},"features":{"type":["array","null"],"items":{"type":"string"}},"restrictions":{"type":["object","null"],"description":"Free-form. Keys seen in practice: min_age, max_age, min_income, no_pets, no_smoking, accessibility_required, max_occupants, student_housing, senior_housing.","additionalProperties":true},"parking_spots":{"type":"array","items":{"$ref":"#/components/schemas/ParkingSpot"}},"storage_units":{"type":"array","items":{"$ref":"#/components/schemas/StorageUnit"}},"available":{"type":"boolean","description":"True only when `status` is `vacant`. Lease-blind: a vacant unit with a signed future lease still reports true. Use a list endpoint for the lease-aware answer."},"primary_photo_url":{"type":["string","null"]},"photos":{"type":"array","items":{"$ref":"#/components/schemas/UnitPhoto"}},"floor_plan_url":{"type":["string","null"]},"floor_area_plan_url":{"type":["string","null"]},"unit_fact_sheet_url":{"type":["string","null"]}}}}},"PropertyInquiryRequest":{"type":"object","title":"PropertyInquiryRequest","description":"The complete set of fields the endpoint reads. Anything else is ignored silently.","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"The only required field. Checked against `^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$`."},"first_name":{"type":"string"},"last_name":{"type":"string"},"phone":{"type":"string","description":"Stored as sent. No format validation."},"message":{"type":"string","description":"Lands in the lead notes, with `Submitted from: <origin>` appended when an Origin header is present."},"unit_id":{"type":"string","format":"uuid","description":"Must exist and belong to the property behind the key."},"preferred_move_in":{"type":"string","format":"date"},"requested_visit_date":{"type":"string","format":"date"},"newsletter_signup":{"type":"boolean","default":false},"apartment_type":{"type":"string","description":"Normalized server-side to Quebec room counts: `2 Bedroom` and `2 chambres` both become `4 1/2`. Unrecognised values are stored verbatim."},"language":{"type":"string","enum":["EN","FR","ES"],"description":"Website language. Stored as sent; not rejected if it differs."},"cf-turnstile-response":{"type":"string","description":"Cloudflare Turnstile token, as posted by the standard widget."}}},"OrganizationInquiryRequest":{"type":"object","title":"OrganizationInquiryRequest","description":"As `PropertyInquiryRequest`, plus an optional `property_id`.","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"The only required field."},"first_name":{"type":"string"},"last_name":{"type":"string"},"phone":{"type":"string"},"message":{"type":"string","description":"Lands in the lead notes."},"property_id":{"type":"string","format":"uuid","description":"Omit for a portfolio-level lead not tied to a building."},"unit_id":{"type":"string","format":"uuid","description":"When supplied without `property_id`, the property is inferred from the unit."},"preferred_move_in":{"type":"string","format":"date"},"requested_visit_date":{"type":"string","format":"date"},"newsletter_signup":{"type":"boolean","default":false},"apartment_type":{"type":"string","description":"Normalized to Manhattin’s vocabulary where possible."},"language":{"type":"string","enum":["EN","FR","ES"]},"cf-turnstile-response":{"type":"string","description":"Cloudflare Turnstile token."}}},"InquiryCreatedResponse":{"type":"object","title":"InquiryCreatedResponse","description":"Returned with HTTP 201 by both inquiry endpoints. Error bodies carry no `success` field at all, so check the status code.","required":["success","lead_id"],"properties":{"success":{"type":"boolean"},"lead_id":{"type":"string","format":"uuid","description":"Identifier of the created lead."},"message":{"type":"string","description":"Copy suitable for showing the visitor."}}},"OrganizationProperty":{"type":"object","title":"OrganizationProperty","description":"Address parts are empty strings, not null, when unset — treat an empty string as \"not available\".","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid","description":"Pass as `property_id` elsewhere."},"name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"province":{"type":"string"},"postal_code":{"type":"string"},"phone":{"type":["string","null"]},"email":{"type":["string","null"]},"website":{"type":["string","null"]},"property_type":{"type":["string","null"],"enum":["residential","commercial","mixed",null]},"year_built":{"type":["integer","null"]},"total_floors":{"type":["integer","null"]},"amenities":{"type":"array","items":{"type":"string"}},"photos":{"type":"array","items":{"$ref":"#/components/schemas/UnitPhoto"}},"logo_url":{"type":["string","null"]},"total_units":{"type":"integer","description":"Count of active units, computed per request."},"available_units":{"type":"integer","description":"Count of active units at raw status `vacant`. A headline figure — not the marketable count."}}},"OrganizationPropertiesResponse":{"type":"object","title":"OrganizationPropertiesResponse","description":"The `listOrganizationProperties` envelope.","required":["success","count","properties"],"properties":{"success":{"type":"boolean"},"count":{"type":"integer"},"properties":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationProperty"}}}},"OrganizationAvailableUnit":{"allOf":[{"$ref":"#/components/schemas/AvailableUnit"},{"type":"object","description":"The portfolio variant adds the owning property.","properties":{"property":{"type":"object","description":"Identity of the building. No postal code or coordinates here.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"province":{"type":"string"}}}}}],"title":"OrganizationAvailableUnit","description":"A marketable unit anywhere in the portfolio."},"OrganizationAvailabilityResponse":{"type":"object","title":"OrganizationAvailabilityResponse","description":"The `listOrganizationAvailability` envelope.","required":["success","count","units"],"properties":{"success":{"type":"boolean"},"count":{"type":"integer"},"units":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationAvailableUnit"}}}},"OrganizationRosterUnit":{"type":"object","title":"OrganizationRosterUnit","description":"Every active unit, with a computed availability status and lease dates.","required":["id","unit_number","status","availability_status"],"properties":{"id":{"type":"string","format":"uuid"},"unit_number":{"type":"string"},"status":{"type":"string","enum":["vacant","occupied","maintenance","reserved","leased"],"description":"Raw status. Reading this alongside `availability_status` is the robust approach."},"classification":{"type":"string","enum":["residential","commercial"]},"monthly_rent":{"type":["number","null"],"description":"Market rent, else current rent, else null."},"square_feet":{"type":["integer","null"]},"bedrooms":{"type":["integer","null"]},"bathrooms":{"type":["number","null"]},"floor":{"type":["integer","null"]},"unit_type":{"type":["string","null"]},"features":{"type":"array","items":{"type":"string"}},"is_affordable_housing":{"type":"boolean"},"availability_status":{"type":"string","enum":["vacant_available","vacant_leased","occupied_available","occupied_leased","unavailable"],"description":"`unavailable` covers raw status `maintenance` or `reserved` and was added 2026-08-04; add a case for it if you switch on this field."},"lease_end_date":{"type":["string","null"],"format":"date","description":"End of the current active lease. Always present as a key, unlike on the availability endpoint."},"future_lease_start_date":{"type":["string","null"],"format":"date","description":"When the next tenant takes possession, if a successor lease exists."},"transfer_intent_date":{"type":["string","null"],"format":"date","description":"Set when the current lease is flagged for transfer and no successor lease exists yet. Such units are usually absent from the availability endpoint."},"primary_photo_url":{"type":["string","null"]},"photos":{"type":"array","items":{"$ref":"#/components/schemas/UnitPhoto"}},"floor_plan_url":{"type":["string","null"]},"floor_area_plan_url":{"type":["string","null"]},"unit_fact_sheet_url":{"type":["string","null"]},"property":{"type":"object","description":"Richer than the availability variant: includes coordinates and amenities.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"address":{"type":"string"},"city":{"type":"string"},"province":{"type":"string"},"postal_code":{"type":"string"},"latitude":{"type":["number","null"]},"longitude":{"type":["number","null"]},"amenities":{"type":"array","items":{"type":"string"}}}}}},"OrganizationUnitsResponse":{"type":"object","title":"OrganizationUnitsResponse","description":"The `listOrganizationUnits` envelope. No `success` field — check the status code.","required":["units"],"properties":{"units":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationRosterUnit"}},"count":{"type":"integer"}}},"AgencyAvailabilityResponse":{"type":"object","title":"AgencyAvailabilityResponse","description":"The `listAgencyAvailability` snapshot: available units, unavailable ids with reasons, and counts.","required":["available","unavailable","meta"],"properties":{"available":{"type":"array","description":"Units the agency may list.","items":{"$ref":"#/components/schemas/AgencyUnit"}},"unavailable":{"type":"array","description":"Units that exist but cannot be listed, with a reason.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"reason":{"type":"string","description":"Why the unit cannot be listed, e.g. `leased`."}}}},"meta":{"type":"object","description":"Counts and the time the snapshot was taken.","properties":{"available_count":{"type":"integer"},"unavailable_count":{"type":"integer"},"properties_count":{"type":"integer"},"pulled_at":{"type":"string","format":"date-time"}}}}},"AgencyUnit":{"type":"object","title":"AgencyUnit","description":"The agency variant flattens the property onto the unit and names rent `market_rent`.","required":["id","unit_number","property_id"],"properties":{"id":{"type":"string","format":"uuid"},"unit_number":{"type":"string"},"floor":{"type":["integer","null"]},"unit_type":{"type":["string","null"]},"classification":{"type":"string","enum":["residential","commercial"]},"bedrooms":{"type":["integer","null"]},"bathrooms":{"type":["number","null"]},"square_feet":{"type":["integer","null"]},"market_rent":{"type":["number","null"]},"features":{"type":["object","null"],"description":"Feature flags keyed by name, e.g. `{ \"dishwasher\": true }`.","additionalProperties":true},"parking_spots":{"type":["array","object","null"]},"storage_units":{"type":["array","object","null"]},"primary_photo_url":{"type":["string","null"]},"photos":{"type":"array","items":{"type":"string","format":"uri"}},"floor_plan_url":{"type":["string","null"]},"floor_area_plan_url":{"type":["string","null"]},"unit_fact_sheet_url":{"type":["string","null"]},"is_affordable_housing":{"type":"boolean"},"availability_status":{"type":"string"},"availability_reason":{"type":"string"},"available_date":{"type":["string","null"],"format":"date"},"property_id":{"type":"string","format":"uuid"},"property_name":{"type":"string"},"property_address":{"type":"string"},"property_latitude":{"type":["number","null"]},"property_longitude":{"type":["number","null"]},"promotions":{"type":"array","items":{"$ref":"#/components/schemas/Promotion"}}}},"PricingPlan":{"type":"object","title":"PricingPlan","description":"One published subscription plan as shown on the pricing section.","properties":{"id":{"type":"string"},"key":{"type":"string","description":"Stable plan identifier, e.g. `starter`, `enterprise`."},"name":{"type":"string","description":"Display name."},"price":{"type":"string","description":"Formatted price, or `Custom` for a quoted plan."},"period":{"type":"string","description":"`/month`, or empty for a quoted plan."},"desc":{"type":"string"},"features":{"type":"array","items":{"type":"string"}},"cta":{"type":"string","description":"Call to action shown on the pricing card."},"badge":{"type":["object","null"],"properties":{"text":{"type":"string"},"type":{"type":"string"}}},"tier":{"type":["string","null"]},"perUnitPrice":{"type":["integer","null"],"description":"Per-unit surcharge in cents."},"includesUnits":{"type":["integer","null"],"description":"Units included before the surcharge."}}},"PricingResponse":{"type":"object","title":"PricingResponse","description":"The `listPublicPricingPlans` envelope: published plans in display order.","required":["plans"],"properties":{"plans":{"type":"array","items":{"$ref":"#/components/schemas/PricingPlan"}},"cached_at":{"type":"string","format":"date-time"}}}}}}