{
  "openapi": "3.1.0",
  "info": {
    "title": "Wu and Kin API",
    "version": "1.0.0",
    "summary": "Read the published homes of a long-stay rental operator in Vietnam, and ask about one.",
    "description": "Wu and Kin operates furnished homes in Vietnam on residential leases of six months and longer, each inspected in person against 47 points before it is listed. This API is the machine surface over the same published facts as the website.\n\n**Read** (`/api/v1/listings`, `/api/v1/listings/{slug}`, `/api/v1/cities`) needs no key and no account: the data is the same file the site publishes. **Write** (`/api/inquiry`, `/api/waitlist`, `/api/employer`, `/api/viewing`) is a form post that a person reads and answers; nothing is booked and no payment is taken.\n\n**Authentication.** There is none. Nothing here is private, and no key exists to leak.\n\n**Rate limits.** 120 requests per 10 minutes per address on the read endpoints, 10 per 10 minutes on each write endpoint. Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`; a `429` also carries `Retry-After`.\n\n**Errors.** Every failure is `application/problem+json` per RFC 9457, with a stable `code` to branch on. A browser form post is the one exception: it receives a `303` back to the form so a visitor without JavaScript is never shown a raw JSON dead end. Send `Accept: application/json` to always get the problem document.\n\n**Versioning.** The major version is in the path (`/api/v1`). A version is announced at least 90 days before it is retired and stays available for at least 12 months after its successor ships. Every response carries `X-Wuandkin-API-Version`. When a version is retired, its responses carry `Deprecation: true` and `Sunset: <HTTP-date>` for at least 90 days first. Adding a field is not a breaking change; removing or retyping one is, and ships as a new major version.\n\n**Deprecation.** There is nothing deprecated today: v1 is the first and current version. The full policy — the notice period, the `Deprecation` and `Sunset` headers, and what counts as a breaking change — is published at https://wuandkin.com/developers/deprecation and summarized here. A deprecated version answers with `Deprecation: true` and `Sunset: <HTTP-date>` on every response for the whole notice period.",
    "contact": {
      "name": "Wu and Kin",
      "email": "han@wuandkin.com",
      "url": "https://wuandkin.com/contact"
    },
    "license": {
      "name": "Proprietary",
      "identifier": "LicenseRef-Proprietary"
    }
  },
  "servers": [
    {
      "url": "/",
      "description": "This host. The API is served from the same origin as the documentation; production is https://wuandkin.com."
    }
  ],
  "tags": [
    {
      "name": "Homes",
      "description": "The published homes. Read-only, no key, no account."
    },
    {
      "name": "Forms",
      "description": "The public lead forms. Each submission is read and answered by a named person; none of them book, confirm or charge anything."
    }
  ],
  "paths": {
    "/api": {
      "get": {
        "operationId": "getApiIndex",
        "tags": [
          "Homes"
        ],
        "summary": "Discover this API",
        "description": "The entry point: the current version, where the documentation and the OpenAPI document live, and every read and write endpoint with a one-line description. This is the cheapest first call for an agent that has only a domain name.",
        "responses": {
          "200": {
            "description": "The endpoint index.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Wuandkin-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          },
          "405": {
            "description": "This path does not accept that method.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client address exceeded the endpoint's rate limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "default": {
            "description": "Any other failure, in the same problem document shape.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1": {
      "get": {
        "operationId": "getApiV1Index",
        "tags": [
          "Homes"
        ],
        "summary": "Discover this API, at the versioned path",
        "description": "Identical to `GET /api`, so a client that hard-codes the versioned root never has to know about the unversioned alias.",
        "responses": {
          "200": {
            "description": "The endpoint index.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Wuandkin-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          },
          "405": {
            "description": "This path does not accept that method.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client address exceeded the endpoint's rate limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "default": {
            "description": "Any other failure, in the same problem document shape.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/listings": {
      "get": {
        "operationId": "listListings",
        "tags": [
          "Homes"
        ],
        "summary": "Every published home",
        "description": "The full catalogue: rent, deposit, minimum stay, availability and inspection status for every home the site publishes, as one document. A home that has not been walked yet publishes no measurements and says so in its `inspection` object, rather than reporting zeroes.",
        "responses": {
          "200": {
            "description": "Every home.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Wuandkin-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListingsDocument"
                }
              }
            }
          },
          "405": {
            "description": "This path does not accept that method.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client address exceeded the endpoint's rate limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The published data behind this endpoint could not be read, so no answer was produced. Retry after the seconds in Retry-After.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "default": {
            "description": "Any other failure, in the same problem document shape.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/listings/{slug}": {
      "get": {
        "operationId": "getListing",
        "tags": [
          "Homes"
        ],
        "summary": "One home",
        "description": "A single home by the `slug` used in `/api/v1/listings` and in `/listings.json`. An unknown slug is a `404` problem document with `code: \"not_found\"`, never an empty object and never a redirect.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The home's slug, for example `le-hong-phong-dalat-house`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The home.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Wuandkin-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListingDocument"
                }
              }
            }
          },
          "404": {
            "description": "No resource exists at this path.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "This path does not accept that method.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client address exceeded the endpoint's rate limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The published data behind this endpoint could not be read, so no answer was produced. Retry after the seconds in Retry-After.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "default": {
            "description": "Any other failure, in the same problem document shape.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/cities": {
      "get": {
        "operationId": "listCities",
        "tags": [
          "Homes"
        ],
        "summary": "Cities with a home",
        "description": "Every city that currently has at least one published home, and how many. A city on the roadmap with nothing live is deliberately absent: this endpoint reports what exists, not what is planned.",
        "responses": {
          "200": {
            "description": "The cities.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "X-Wuandkin-API-Version": {
                "$ref": "#/components/headers/ApiVersion"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CitiesDocument"
                }
              }
            }
          },
          "405": {
            "description": "This path does not accept that method.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client address exceeded the endpoint's rate limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "503": {
            "description": "The published data behind this endpoint could not be read, so no answer was produced. Retry after the seconds in Retry-After.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "default": {
            "description": "Any other failure, in the same problem document shape.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/inquiry": {
      "post": {
        "operationId": "createInquiry",
        "tags": [
          "Forms"
        ],
        "summary": "Ask about a specific home",
        "description": "Sends a tenancy inquiry for one home. The record is appended to the operator's lead sheet and Han or Trang replies. `moveInDate` is required unless `moveInOptional=true`, which the form sends for a home with no published move-in date yet. Send `Accept: application/json` to receive the JSON document below instead of a redirect; a browser form post gets the `303`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/InquiryForm"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/InquiryForm"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted, returned to a client that sent `Accept: application/json`.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormAccepted"
                }
              }
            }
          },
          "303": {
            "description": "Accepted by a browser form post. The visitor is redirected to the confirmation page, and the form shows an inline error banner when a failure code is present in the query string. A client that sends `Accept: application/json` receives the `200` document instead.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                },
                "description": "The confirmation page for this form."
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "413": {
            "description": "The body exceeded the endpoint's limit, or Content-Length was absent or unparseable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "One or more fields were missing, malformed, or not an allowed option.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client address exceeded the endpoint's rate limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "default": {
            "description": "Any other failure, in the same problem document shape.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/waitlist": {
      "post": {
        "operationId": "joinWaitlist",
        "tags": [
          "Forms"
        ],
        "summary": "Join the list for a city that is not open yet",
        "description": "The two-field ask for a city or a home that cannot be offered on a date yet. The list is contacted by hand before the site changes. `landing` and `flex` carry an optional intended landing date and how flexible it is.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/WaitlistForm"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/WaitlistForm"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted, returned to a client that sent `Accept: application/json`.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormAccepted"
                }
              }
            }
          },
          "303": {
            "description": "Accepted by a browser form post. The visitor is redirected to the confirmation page, and the form shows an inline error banner when a failure code is present in the query string. A client that sends `Accept: application/json` receives the `200` document instead.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                },
                "description": "The confirmation page for this form."
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "413": {
            "description": "The body exceeded the endpoint's limit, or Content-Length was absent or unparseable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "One or more fields were missing, malformed, or not an allowed option.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client address exceeded the endpoint's rate limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "default": {
            "description": "Any other failure, in the same problem document shape.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/employer": {
      "post": {
        "operationId": "submitEmployerRequest",
        "tags": [
          "Forms"
        ],
        "summary": "Corporate or relocation enquiry",
        "description": "For a company moving people to Vietnam. Only a work email, a headcount band and a first-arrival band are required; everything else is optional. The reply comes from Han.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/EmployerForm"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/EmployerForm"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted, returned to a client that sent `Accept: application/json`.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormAccepted"
                }
              }
            }
          },
          "303": {
            "description": "Accepted by a browser form post. The visitor is redirected to the confirmation page, and the form shows an inline error banner when a failure code is present in the query string. A client that sends `Accept: application/json` receives the `200` document instead.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                },
                "description": "The confirmation page for this form."
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "413": {
            "description": "The body exceeded the endpoint's limit, or Content-Length was absent or unparseable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "One or more fields were missing, malformed, or not an allowed option.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client address exceeded the endpoint's rate limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "default": {
            "description": "Any other failure, in the same problem document shape.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/viewing": {
      "post": {
        "operationId": "requestViewing",
        "tags": [
          "Forms"
        ],
        "summary": "Ask for a live video walk-through before committing",
        "description": "Requests a live camera walk of a home with Trang. Nothing is booked and no slot is confirmed by this call; Trang replies on the named platform to agree a time.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/ViewingForm"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/ViewingForm"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted, returned to a client that sent `Accept: application/json`.",
            "headers": {
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FormAccepted"
                }
              }
            }
          },
          "303": {
            "description": "Accepted by a browser form post. The visitor is redirected to the confirmation page, and the form shows an inline error banner when a failure code is present in the query string. A client that sends `Accept: application/json` receives the `200` document instead.",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string"
                },
                "description": "The confirmation page for this form."
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            }
          },
          "413": {
            "description": "The body exceeded the endpoint's limit, or Content-Length was absent or unparseable.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "One or more fields were missing, malformed, or not an allowed option.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client address exceeded the endpoint's rate limit.",
            "headers": {
              "Retry-After": {
                "$ref": "#/components/headers/RetryAfter"
              },
              "RateLimit-Limit": {
                "$ref": "#/components/headers/RateLimitLimit"
              },
              "RateLimit-Remaining": {
                "$ref": "#/components/headers/RateLimitRemaining"
              },
              "RateLimit-Reset": {
                "$ref": "#/components/headers/RateLimitReset"
              }
            },
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "default": {
            "description": "Any other failure, in the same problem document shape.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "RateLimitLimit": {
        "description": "Requests allowed per window for this endpoint.",
        "schema": {
          "type": "integer",
          "example": 120
        }
      },
      "RateLimitRemaining": {
        "description": "Requests remaining in the current window for this client address.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "example": 119
        }
      },
      "RateLimitReset": {
        "description": "Seconds until the current window resets.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "example": 600
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before sending the request again. Present on `429` and `503`.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "ApiVersion": {
        "description": "The major version of the API that answered.",
        "schema": {
          "type": "integer",
          "const": 1
        }
      },
      "Deprecation": {
        "description": "Present and true only while this version is deprecated, for the whole notice period. Absent means the version is current.",
        "schema": {
          "type": "boolean"
        }
      },
      "Sunset": {
        "description": "The HTTP date this version stops answering. Published alongside Deprecation for the whole notice period; absent on a current version.",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem document. Returned as `application/problem+json` on every API failure. Branch on `code`, never on `title`; `code` is stable and `title` is prose.",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "A URL identifying the problem class.",
            "example": "https://wuandkin.com/problems/not-found"
          },
          "title": {
            "type": "string",
            "description": "A short human-readable summary."
          },
          "status": {
            "type": "integer",
            "description": "The HTTP status code, repeated in the document."
          },
          "code": {
            "type": "string",
            "description": "The stable machine code.",
            "enum": [
              "invalid",
              "toolarge",
              "busy",
              "method_not_allowed",
              "not_found",
              "unavailable"
            ]
          },
          "detail": {
            "type": "string",
            "description": "What was wrong with this specific request."
          },
          "resolution": {
            "type": "string",
            "description": "What to change before retrying."
          }
        },
        "required": [
          "type",
          "title",
          "status",
          "code",
          "detail",
          "resolution"
        ]
      },
      "ApiIndex": {
        "type": "object",
        "description": "The discovery document served at `/api` and `/api/v1`.",
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "integer",
            "const": 1
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "openapi": {
            "type": "string",
            "format": "uri"
          },
          "data": {
            "type": "string",
            "format": "uri",
            "description": "The plain data file behind the read endpoints."
          },
          "read": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Endpoint"
            }
          },
          "write": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Endpoint"
            }
          },
          "deprecation": {
            "type": "string",
            "format": "uri",
            "description": "The published deprecation policy: notice period, headers, and what counts as breaking."
          }
        },
        "required": [
          "name",
          "version",
          "documentation",
          "openapi",
          "deprecation",
          "data",
          "read",
          "write"
        ]
      },
      "Endpoint": {
        "type": "object",
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "GET",
              "POST"
            ]
          },
          "path": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        },
        "required": [
          "method",
          "path",
          "description"
        ]
      },
      "ListingsDocument": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "const": 1
          },
          "updated": {
            "type": "string",
            "format": "date-time",
            "description": "When the published data was built."
          },
          "source": {
            "type": "string",
            "format": "uri"
          },
          "count": {
            "type": "integer",
            "minimum": 0
          },
          "listings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Listing"
            }
          }
        },
        "required": [
          "version",
          "updated",
          "source",
          "count",
          "listings"
        ]
      },
      "ListingDocument": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "const": 1
          },
          "listing": {
            "$ref": "#/components/schemas/Listing"
          }
        },
        "required": [
          "version",
          "listing"
        ]
      },
      "CitiesDocument": {
        "type": "object",
        "properties": {
          "version": {
            "type": "integer",
            "const": 1
          },
          "count": {
            "type": "integer",
            "minimum": 0
          },
          "cities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/City"
            }
          }
        },
        "required": [
          "version",
          "count",
          "cities"
        ]
      },
      "City": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The city name as published, with Vietnamese diacritics."
          },
          "listings": {
            "type": "integer",
            "minimum": 0,
            "description": "How many homes are published in this city."
          }
        },
        "required": [
          "name",
          "listings"
        ]
      },
      "Listing": {
        "type": "object",
        "description": "One published home. Money is in Vietnamese dong, as published; no markup is added by the API.",
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9-]+$"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The human page for this home."
          },
          "title": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "area": {
            "type": "string",
            "description": "Ward and city, as published."
          },
          "bedrooms": {
            "type": "integer",
            "minimum": 0
          },
          "bathrooms": {
            "type": "integer",
            "minimum": 0
          },
          "monthlyVnd": {
            "type": "integer",
            "description": "Monthly rent in VND, all-in."
          },
          "depositVnd": {
            "type": "integer",
            "description": "Deposit in VND. Per home, not a fixed number of months."
          },
          "minimumStayMonths": {
            "type": "integer",
            "minimum": 6
          },
          "status": {
            "type": "string",
            "enum": [
              "available",
              "reserved",
              "leased",
              "renovation"
            ]
          },
          "availableFrom": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "The next date the home is free, or null when the owner has not set one."
          },
          "inspection": {
            "$ref": "#/components/schemas/Inspection"
          },
          "rooms": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Room"
            }
          }
        },
        "required": [
          "slug",
          "url",
          "title",
          "city",
          "area",
          "bedrooms",
          "bathrooms",
          "monthlyVnd",
          "depositVnd",
          "minimumStayMonths",
          "status",
          "availableFrom",
          "inspection",
          "rooms"
        ]
      },
      "Inspection": {
        "type": "object",
        "description": "The 47-point walk. `signed` is false until Trang has walked the home, and a home that is not walked yet publishes no measurements rather than zeroes.",
        "properties": {
          "signed": {
            "type": "boolean"
          },
          "inspector": {
            "type": "string"
          },
          "date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "checksPassed": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 0,
            "maximum": 47
          },
          "checksTotal": {
            "type": [
              "integer",
              "null"
            ]
          },
          "failedChecks": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "integer",
              "minimum": 1,
              "maximum": 47
            }
          },
          "internetMbps": {
            "type": [
              "number",
              "null"
            ]
          },
          "noiseDbAt9pm": {
            "type": [
              "number",
              "null"
            ]
          }
        },
        "required": [
          "signed",
          "inspector"
        ]
      },
      "Room": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "outside": {
            "type": "boolean",
            "description": "True for a balcony, terrace or garden."
          },
          "sqm": {
            "type": "number",
            "exclusiveMinimum": 0
          },
          "measured": {
            "type": "boolean",
            "description": "False until the room has been measured on a walk."
          }
        },
        "required": [
          "name",
          "measured"
        ]
      },
      "FormAccepted": {
        "type": "object",
        "description": "Returned to a writer that asked for JSON. The record is stored; a person answers it.",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "endpoint": {
            "type": "string"
          },
          "next": {
            "type": "string",
            "description": "What happens after this call, in plain words."
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          }
        },
        "required": [
          "ok",
          "endpoint",
          "next",
          "documentation"
        ]
      },
      "InquiryForm": {
        "type": "object",
        "properties": {
          "moveInDate": {
            "type": "string",
            "format": "date",
            "description": "When the tenancy would start, ISO 8601. Required unless moveInOptional is true."
          },
          "moveInOptional": {
            "type": "string",
            "enum": [
              "true",
              "false"
            ],
            "description": "Set to `true` only for a home with no published move-in date; it relaxes the moveInDate requirement."
          },
          "stayLength": {
            "type": "string",
            "description": "Intended length of stay in months.",
            "enum": [
              "6",
              "9",
              "12",
              "18",
              "24",
              "other"
            ]
          },
          "tenants": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4
          },
          "fullName": {
            "type": "string",
            "minLength": 2,
            "maxLength": 100
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "phone": {
            "type": "string",
            "description": "WhatsApp or phone number, with or without a country code.",
            "pattern": "^\\+?[0-9\\s-]{7,15}$",
            "example": "+84 90 123 4567"
          },
          "currentlyLocated": {
            "type": "string",
            "maxLength": 200
          },
          "aboutYou": {
            "type": "string",
            "maxLength": 500
          },
          "listingSlug": {
            "type": "string",
            "maxLength": 120,
            "description": "The home being asked about, as it appears in /api/v1/listings."
          },
          "company": {
            "type": "string",
            "description": "Honeypot. Leave empty; a filled value discards the submission."
          }
        },
        "required": [
          "stayLength",
          "tenants",
          "fullName",
          "email",
          "phone"
        ]
      },
      "WaitlistForm": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "city": {
            "type": "string",
            "enum": [
              "Da Lat",
              "Da Nang",
              "Saigon",
              "Hanoi",
              "Hoi An"
            ]
          },
          "interestedIn": {
            "type": "string",
            "maxLength": 120
          },
          "landing": {
            "type": "string",
            "format": "date",
            "description": "Intended landing date, ISO 8601. Optional."
          },
          "flex": {
            "type": "string",
            "description": "How flexible the landing date is."
          },
          "company": {
            "type": "string",
            "description": "Honeypot. Leave empty; a filled value discards the submission."
          }
        },
        "required": [
          "email",
          "city"
        ]
      },
      "EmployerForm": {
        "type": "object",
        "properties": {
          "firstName": {
            "type": "string",
            "maxLength": 100
          },
          "lastName": {
            "type": "string",
            "maxLength": 100
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254,
            "description": "A work email address."
          },
          "organization": {
            "type": "string",
            "maxLength": 200
          },
          "people": {
            "type": "string",
            "enum": [
              "1",
              "2 to 5",
              "6 to 20",
              "More than 20"
            ]
          },
          "city": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "Da Nang",
                "Saigon",
                "Hanoi",
                "Da Lat"
              ]
            },
            "description": "Repeat the field once per city."
          },
          "arrival": {
            "type": "string",
            "enum": [
              "Q4 2026",
              "Q1 2027",
              "Q2 2027",
              "Later in 2027",
              "Not sure yet"
            ]
          },
          "budget": {
            "type": "string",
            "maxLength": 100
          },
          "note": {
            "type": "string",
            "maxLength": 500
          },
          "company": {
            "type": "string",
            "description": "Honeypot. Leave empty; a filled value discards the submission."
          }
        },
        "required": [
          "email",
          "people",
          "arrival"
        ]
      },
      "ViewingForm": {
        "type": "object",
        "properties": {
          "fullName": {
            "type": "string",
            "minLength": 2,
            "maxLength": 100
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 254
          },
          "phone": {
            "type": "string",
            "pattern": "^\\+?[0-9\\s-]{7,15}$",
            "example": "+84 90 123 4567"
          },
          "platform": {
            "type": "string",
            "enum": [
              "WhatsApp",
              "Zalo"
            ]
          },
          "preferredTimes": {
            "type": "string",
            "maxLength": 300
          },
          "listingSlug": {
            "type": "string",
            "maxLength": 120
          },
          "company": {
            "type": "string",
            "description": "Honeypot. Leave empty; a filled value discards the submission."
          }
        },
        "required": [
          "fullName",
          "email",
          "phone",
          "platform"
        ]
      }
    }
  }
}
