---
title: "Listing search"
description: "Search published Matchai listings in natural language with the /ask endpoint (NLWeb). Parameters, response shape, host-aware scope, limits and examples."
last_updated: "2026-10-21"
lang: "en"
canonical: "https://matchai.co/docs/developers/listing-search"
---

# Listing search

> Search published Matchai listings in natural language with the /ask endpoint (NLWeb). Parameters, response shape, host-aware scope, limits and examples.

Source: https://matchai.co/docs/developers/listing-search

`/ask` searches published listings from a plain sentence such as "3 bedroom condo in Laval under 600k". It follows the NLWeb protocol, so NLWeb clients can call it without custom code. It is public and read-only, and it needs no account or API key.

```text
https://matchai.co/ask
```

It returns listings in the order a person would want them: best match first, by relevance to the request and by how recently the listing was updated. No broker can pay to change the order or to be included.

## Search in one request [#search-in-one-request]

```bash
curl "https://matchai.co/ask?query=3+bedroom+condo+in+Laval+under+600k"
```

An abridged answer. This search matched one listing, and the `score` is an example value:

```json
{
  "_meta": { "response_type": "answer", "version": "0.55", "mode": "list" },
  "query_id": "6f1d2c1e-3b0a-4a54-9f2e-0c7b8a5d9e41",
  "site": "matchai.co",
  "mode": "list",
  "query": "3 bedroom condo in Laval under 600k",
  "results": [
    {
      "url": "https://alice.matchai.co/bright-condo-montmorency",
      "name": "Bright 3-bedroom condo near Montmorency metro",
      "site": "alice.matchai.co",
      "score": 0.872,
      "description": "$585,000, condo for sale · 3 bedrooms · 1.5 bathrooms · 1,120 sq ft · Montmorency, Laval, Québec · Listed by Alice Tremblay (Agence Exemple)",
      "schema_object": {
        "@context": "https://schema.org",
        "@type": "RealEstateListing",
        "url": "https://alice.matchai.co/bright-condo-montmorency",
        "name": "Bright 3-bedroom condo near Montmorency metro",
        "image": "https://media.example.com/listings/bright-condo-montmorency/cover.jpg",
        "dateModified": "2026-10-12T14:03:22.000Z",
        "offers": {
          "@type": "Offer",
          "price": 585000,
          "priceCurrency": "CAD",
          "availability": "https://schema.org/InStock",
          "businessFunction": "http://purl.org/goodrelations/v1#Sell",
          "url": "https://alice.matchai.co/bright-condo-montmorency",
          "offeredBy": {
            "@type": "RealEstateAgent",
            "name": "Alice Tremblay",
            "worksFor": { "@type": "Organization", "name": "Agence Exemple" }
          }
        },
        "about": {
          "@type": "Apartment",
          "numberOfBedrooms": 3,
          "numberOfBathroomsTotal": 1.5,
          "floorSize": { "@type": "QuantitativeValue", "value": 1120, "unitCode": "FTK" },
          "address": { "@type": "PostalAddress", "addressLocality": "Laval", "addressRegion": "Québec" }
        }
      }
    }
  ]
}
```

The names, addresses and photo host in these examples are made up.

## Request [#request]

Send the search as a `GET` with query-string parameters, or as a `POST` with a JSON body.

### GET [#get]

<TypeTable
  type={{
    query: { type: 'string', required: true, description: 'The search sentence. Up to 500 characters; longer text is cut. q is accepted as an alias.' },
    site: { type: 'string', description: "A broker host to limit the search to, for example alice.matchai.co or a broker's own domain. Used only on matchai.co." },
    query_id: { type: 'string', description: 'Your own id for the request: up to 100 characters from A-Z, a-z, 0-9 and . _ : - . It is returned unchanged. Without one, Matchai generates it.' },
    mode: { type: '"list"', description: 'Accepted for compatibility with NLWeb. The answer is always a list, whatever you send.' },
    streaming: { type: 'boolean', description: 'Accepted and ignored. The answer is always one JSON document.' },
  }}
/>

### POST [#post]

Send `Content-Type: application/json`. The body can be up to 16 KB.

```json
{ "query": "4½ à louer sur le Plateau" }
```

The query can also be an object, as in NLWeb:

```json
{ "query": { "text": "condo near the metro in Montréal" }, "prefer": { "mode": "list" } }
```

<TypeTable
  type={{
    query: { type: 'string | { text: string }', required: true, description: 'The search sentence, with the same limit of 500 characters. A top-level q is accepted too.' },
    site: { type: 'string', description: 'The same as the GET parameter.' },
    query_id: { type: 'string', description: 'The same as the GET parameter.' },
    prefer: { type: 'object', description: 'Accepted for compatibility with NLWeb. prefer.mode is ignored, and the answer is always a list.' },
  }}
/>

Query-string parameters fill in anything the body leaves out.

### What the query understands [#what-the-query-understands]

The search reads your sentence with a small, predictable parser. It does not call a language model, so the same sentence always gives the same filters. It understands English, French and Spanish.

| You write | It filters on |
|---|---|
| `under 600k`, `max 750,000`, `moins de 750 000 $`, `hasta 400k` | A maximum price |
| `over 300k`, `at least 1.2M`, `plus de 500k`, `desde 200k` | A minimum price |
| `between 400k and 600k`, `entre 400k et 600k` | A price range |
| `3 bedroom`, `3 br`, `3 chambres`, `3 habitaciones` | At least that many bedrooms |
| `4½`, `5½` | Québec room counts. A 4½ has two bedrooms, so `N½` means at least N − 2 bedrooms. |
| `2 baths`, `1.5 bathrooms`, `2 sdb` | At least that many bathrooms |
| `for sale`, `à vendre`, `en venta` | Properties for sale |
| `for rent`, `à louer`, `en alquiler` | Rentals |
| `condo`, `duplex`, `maison`, `chalet`, `terrain`, `townhouse`, `commercial` | A property type: house, condo, apartment, townhouse, plex, land, cottage or commercial |

Amounts can be written as `600k`, `1.2M`, `600 000`, `$750,000` or `2 million`. A small bare number with no unit and no dollar sign, such as `over 2`, is not read as a price. If the sentence asks for both sale and rent, or for neither, the search does not filter on it.

Everything else becomes a keyword. The search keeps up to 6 keywords, drops words shorter than 3 letters and common filler words, and ignores accents, so `montreal` finds Montréal. Keywords are matched against the title, the place names, the property type and the description. A match in a place name counts most, then the title, then the property type, then the description.

Searches return listings that are available now. Sold and rented listings are not returned.

## Response [#response]

<TypeTable
  type={{
    _meta: { type: 'object', description: 'response_type is always answer. version is the NLWeb version the shape follows. mode is always list.' },
    query_id: { type: 'string', description: 'Your query_id, or one Matchai generated.' },
    site: { type: 'string', description: 'The host that answered, for example matchai.co.' },
    mode: { type: '"list"', description: 'Always list.' },
    query: { type: 'string', description: 'The search sentence as Matchai read it, after trimming and the 500-character limit.' },
    results: { type: 'result[]', description: 'Up to 10 matching listings, best match first. An empty array means nothing matched.' },
  }}
/>

Each item in `results`:

<TypeTable
  type={{
    url: { type: 'string', description: "The listing page. This is the broker's own domain once it is live." },
    name: { type: 'string', description: 'The listing title.' },
    site: { type: 'string', description: 'The host of the listing page.' },
    score: { type: 'number', description: 'Relevance and recency together, from 0 to 1.' },
    description: { type: 'string', description: 'One line with the price, type, facts, place and listing broker.' },
    schema_object: { type: 'object', description: 'A schema.org RealEstateListing with its Offer and the property it is about. Unknown values are left out, never set to 0.' },
  }}
/>

A successful answer carries `Link: </llms.txt>; rel="describedby", </openapi.json>; rel="service-desc"`. For photos, highlights and the full description of one listing, use the [MCP server's tools](https://matchai.co/docs/developers/mcp-server/public-tools) or read the listing's [Markdown copy](https://matchai.co/docs/developers/agent-readable-pages#markdown-copies-of-pages).

## Which listings are searched [#which-listings-are-searched]

`/ask` answers for the host you call, so asking a broker's own site searches that broker, and asking `matchai.co` searches everyone.

| You call | It searches |
|---|---|
| `https://matchai.co/ask` | Every published listing on Matchai. Add `site` to limit it to one broker. |
| `https://{name}.matchai.co/ask`, a Matchai Site | The listings published on that broker's site only. |
| `https://{domain}/ask`, a broker's own domain | The same, for the site that domain serves. |
| `https://{name}.matchai.pro/ask`, a Matchai Page | The request is redirected to `https://matchai.co/ask`, so it searches every broker. |

Notes:

- On a Matchai Site or a broker's own domain, `site` is ignored: the host decides the scope.
- On `matchai.co`, `site` takes a broker's site host. A value that matches no published site is ignored, and the search covers everyone. It does not return an empty list.
- A Matchai Page has no search of its own. The redirect is a `307`, which keeps the method and the body. To search one professional's listings, ask their Matchai Site, or call `matchai.co` with their site as `site`. From browser code, call `matchai.co/ask` directly, because a browser cannot follow a redirect on a cross-origin preflight.
- A host with no published site answers `404`.

## Limits, caching and CORS [#limits-caching-and-cors]

- **Rate limit:** 60 requests per minute for each calling address, across `GET` and `POST`. Above it, the answer is `429` with `Retry-After: 60`.
- **Results:** at most 10 per request. There is no paging, so narrow the sentence to reach other listings.
- **Caching:** a `GET` without a `query_id` can be cached for up to a minute (`Cache-Control: public, max-age=60`). Everything else is sent with `no-store`. A new listing appears in results within about a minute.
- **CORS:** open to every origin for `GET`, `POST` and `OPTIONS`, so browser code can call it. `OPTIONS` answers `204`, and preflight answers can be cached for 24 hours.

## Errors [#errors]

Errors are JSON with a stable `error` code and a `message` you can show to a person.

```json
{ "error": "rate_limited", "message": "Too many requests. Try again in a minute." }
```

| Status | `error` | Meaning and what to do |
|---|---|---|
| `400` | `missing_query` | There is no search sentence. Send `?query=` or a JSON body with `query`. |
| `400` | `invalid_json` | The `POST` body is not valid JSON. |
| `413` | `too_large` | The `POST` body is over 16 KB. Send a shorter sentence. |
| `404` | `not_found` | The host you called has no published listings to search. |
| `429` | `rate_limited` | Too many requests. Wait for the `Retry-After` seconds. |
| `503` | `unavailable` | Search is temporarily unavailable. Retry after the `Retry-After` seconds. |

An empty `results` array is a successful answer: nothing published matched. Try a wider area, a higher budget or fewer requirements.

## More examples [#more-examples]

Search one broker's listings by calling their site. Replace `alice` with a real site name:

```bash
curl "https://alice.matchai.co/ask?query=condo+under+500k"
```

Limit a search on `matchai.co` to one broker:

```bash
curl "https://matchai.co/ask?query=house+for+sale&site=alice.matchai.co"
```

Search from code with a `POST`:

<Tabs items={['curl', 'JavaScript']}>
<Tab value="curl">

```bash
curl -X POST https://matchai.co/ask \
  -H "Content-Type: application/json" \
  -d '{ "query": "4½ à louer sur le Plateau", "query_id": "rent-plateau-1" }'
```

</Tab>
<Tab value="JavaScript">

```js
const response = await fetch('https://matchai.co/ask', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ query: '4½ à louer sur le Plateau' }),
});

if (response.status === 429) {
  const wait = Number(response.headers.get('Retry-After') ?? 60);
  throw new Error(`Rate limited. Retry in ${wait} seconds.`);
}

const { results } = await response.json();
for (const listing of results) {
  console.log(`${listing.name} - ${listing.description} - ${listing.url}`);
}
```

</Tab>
</Tabs>

<Cards>
  <Card title="MCP server" href="https://matchai.co/docs/developers/mcp-server">
    Give an AI assistant the same search, plus listing and broker lookups.
  </Card>
  <Card title="AI-readable pages" href="https://matchai.co/docs/developers/agent-readable-pages">
    Read a broker's whole site as Markdown instead of searching it.
  </Card>
</Cards>

## Sitemap

- [Every page of Matchai Docs](https://matchai.co/docs/sitemap.md)
- [llms.txt](https://matchai.co/docs/llms.txt)
