Web-page view of https://agent.index.cy/start for tools that read pages and follow links. The text below is the complete answer; every address in it is a link you can open. Command-line tools get the raw form.

index.cy: property in Cyprus, for AI assistants

You are reading this for a person who is looking for property in Cyprus. If you can only open links that you were shown, use the table below: every link is a complete search that answers with real listings (price, costs on top, photos, how the price compares, a link to each listing). Open the one closest to what the person asked for. In the answer, open next_url for more results and a result's api_url for the full listing. Tell the person which search you used and give them the listing links. The complete instructions follow further down.

DistrictFor sale: good dealsFor sale: sea viewTo rent
Limassolhouses in Limassol, good deals
apartments in Limassol, good deals
houses in Limassol, sea view
apartments in Limassol, sea view
houses in Limassol, to rent
apartments in Limassol, to rent
Paphoshouses in Paphos, good deals
apartments in Paphos, good deals
houses in Paphos, sea view
apartments in Paphos, sea view
houses in Paphos, to rent
apartments in Paphos, to rent
Larnacahouses in Larnaca, good deals
apartments in Larnaca, good deals
houses in Larnaca, sea view
apartments in Larnaca, sea view
houses in Larnaca, to rent
apartments in Larnaca, to rent
Nicosiahouses in Nicosia, good deals
apartments in Nicosia, good deals
houses in Nicosia, sea view
apartments in Nicosia, sea view
houses in Nicosia, to rent
apartments in Nicosia, to rent
Famagustahouses in Famagusta, good deals
apartments in Famagusta, good deals
houses in Famagusta, sea view
apartments in Famagusta, sea view
houses in Famagusta, to rent
apartments in Famagusta, to rent

Every filter and place: https://agent.index.cy/api/meta


---
name: index-cy
description: Search property for sale and rent in Cyprus on index.cy, the Cyprus property marketplace. Use when a person wants to find homes, apartments, land or commercial property in Cyprus, compare asking prices, or check one listing in depth. Plain HTTPS JSON API, no key needed.
homepage: https://index.cy
api_base: https://agent.index.cy/v1
---

# index.cy for AI agents

index.cy lists property for sale and rent across Cyprus from agencies, developers and owners. This file tells you how to search it and read listings on behalf of a person.

If your web-fetch tool summarises pages, fetch this file raw so you see exact parameter names: `curl -s https://agent.index.cy/agent.md`

Tools that read web pages (chat apps) get this file and every API answer as a web page with the same text and clickable links, automatically. To pick the form yourself add `?format=html`, `?format=md` (documents) or `?format=json` (API).

Here to post, edit or remove the person's own listings, or to sync them from a CRM? Read this file for the rules and for connecting their account, then https://agent.index.cy/sellers.md.

## Rules

1. Use the API below, not the website HTML. The website challenges automated clients; `https://agent.index.cy/v1` does not.
2. Always send a `User-Agent` header, for example `User-Agent: my-agent/1.0`. Requests without one are blocked.
3. Never guess a parameter or a value. An unknown one is an error with a hint, on purpose, so that a typo never gives you silently wrong results. Allowed values are in the tables below and in `GET https://agent.index.cy/api/meta`.
4. Give the person links. Every listing has a `url` and every search has a `search_url` that opens the same search on the website. People want to see the photos themselves.
5. Prices are asking prices. Say so. `promoted: true` means the seller paid for placement; say that too if you rank results.
6. Be economical: 30 requests per hour per IP address without an account, 600 with one (these instruction files are free). One well-filtered search with `detail=full` beats a search plus ten listing calls.

## Quick start

```bash
curl -s -A "my-agent/1.0" "https://agent.index.cy/api/listings?deal=sale&district=limassol&property_type=apartment&bedrooms=2&price_max=300000&sort=price_m2_asc&limit=5"
```

```json
{
  "total": 412,
  "results": [
    {
      "id": 11513985,
      "url": "https://index.cy/sale/11513985-2-bedroom-apartment-for-sale-in-limassol-district/",
      "title": "2 bedroom apartment for sale in Limassol",
      "deal": "sale",
      "price": 279000,
      "price_per_m2": 3100,
      "vat_on_top": true,
      "extra_costs": { "kind": "vat", "min": 13950, "max": 53010, "total_min": 292950, "total_max": 332010 },
      "vs_average_pct": -14,
      "price_badge": "good deal",
      "property_type": "apartment",
      "bedrooms": 2,
      "bathrooms": 2,
      "district": "limassol",
      "location": "germasogeia",
      "condition": "brand-new",
      "covered_area_m2": 90,
      "plot_area_m2": null,
      "lat": 34.70712,
      "lng": 33.08731,
      "good_deal": true,
      "promoted": false,
      "photo": "https://index.cy/wp-content/uploads/...",
      "photos_count": 14,
      "listed_at": "2026-08-30",
      "updated_at": "2026-09-18",
      "seller": { "name": "Example Estates", "type": "agency", "url": "https://index.cy/member/example-estates/" },
      "also_listed_by_count": 2,
      "excerpt": "Modern two bedroom apartment on the second floor of a small building, 400 m from the beach ..."
    }
  ],
  "next_cursor": "eyJvIjo1LCJoIjoiYWJjIn0",
  "search_url": "https://index.cy/for-sale/?district=limassol-district&;...",
  "applied_filters": { "bedrooms": "2", "deal": "sale", "district": "limassol", "price_max": 300000, "property_type": "apartment", "sort": "price_m2_asc" }
}
```

Prices are whole euros. Rent prices are per month. Areas are square metres. A `null` means the seller did not provide it.

`vat_on_top: true` (new builds) means VAT is charged on top of `price`, so a home inside the person's budget can end up outside it: `price_min` and `price_max` filter on the price without VAT. When the person gives an all-in budget, lower both bounds a little and check `fees` in the listing detail: `vat_max` is the standard rate, `vat_min` applies the reduced first-home rate to the part of the price the law allows. Whether the person qualifies for the reduced rate is a question for their lawyer, not for you. Resales carry a transfer fee instead (a few percent). `extra_costs.total_max` is the figure to hold against an all-in budget, for new builds and resales alike.

Rent results have `price_period: "month"` and no `price_per_m2`, `vat_on_top`, `extra_costs`, `good_deal` or `fees`; they do have `vs_average_pct` and can carry the `below average` badge. `warnings`, when present, is a list of plain sentences about that listing.

## If your tool can only open links it has already seen

Some assistants (chat apps in particular) may open a link only when it appeared in the conversation or on a page they fetched, and cannot add parameters to it. If that is you, start from the table below: these are complete, working searches. Every answer then carries more links you can open: `next_url` (the next page) and, on each result, `api_url` (the full listing). Tell the person which search you opened, and that you can refine it only as far as your tool lets you change a link.

| District | For sale, good deals | For sale, with a sea view | To rent |
|---|---|---|---|
| Limassol | [houses](https://agent.index.cy/api/listings?deal=sale&district=limassol&property_type=house&good_deal=1&sort=price_m2_asc&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=sale&district=limassol&property_type=apartment&good_deal=1&sort=price_m2_asc&detail=full) | [houses](https://agent.index.cy/api/listings?deal=sale&district=limassol&property_type=house&view=sea-view&sort=price_asc&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=sale&district=limassol&property_type=apartment&view=sea-view&sort=price_asc&detail=full) | [houses](https://agent.index.cy/api/listings?deal=rent&district=limassol&property_type=house&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=rent&district=limassol&property_type=apartment&detail=full) |
| Paphos | [houses](https://agent.index.cy/api/listings?deal=sale&district=paphos&property_type=house&good_deal=1&sort=price_m2_asc&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=sale&district=paphos&property_type=apartment&good_deal=1&sort=price_m2_asc&detail=full) | [houses](https://agent.index.cy/api/listings?deal=sale&district=paphos&property_type=house&view=sea-view&sort=price_asc&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=sale&district=paphos&property_type=apartment&view=sea-view&sort=price_asc&detail=full) | [houses](https://agent.index.cy/api/listings?deal=rent&district=paphos&property_type=house&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=rent&district=paphos&property_type=apartment&detail=full) |
| Larnaca | [houses](https://agent.index.cy/api/listings?deal=sale&district=larnaca&property_type=house&good_deal=1&sort=price_m2_asc&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=sale&district=larnaca&property_type=apartment&good_deal=1&sort=price_m2_asc&detail=full) | [houses](https://agent.index.cy/api/listings?deal=sale&district=larnaca&property_type=house&view=sea-view&sort=price_asc&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=sale&district=larnaca&property_type=apartment&view=sea-view&sort=price_asc&detail=full) | [houses](https://agent.index.cy/api/listings?deal=rent&district=larnaca&property_type=house&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=rent&district=larnaca&property_type=apartment&detail=full) |
| Nicosia | [houses](https://agent.index.cy/api/listings?deal=sale&district=nicosia&property_type=house&good_deal=1&sort=price_m2_asc&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=sale&district=nicosia&property_type=apartment&good_deal=1&sort=price_m2_asc&detail=full) | [houses](https://agent.index.cy/api/listings?deal=sale&district=nicosia&property_type=house&view=sea-view&sort=price_asc&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=sale&district=nicosia&property_type=apartment&view=sea-view&sort=price_asc&detail=full) | [houses](https://agent.index.cy/api/listings?deal=rent&district=nicosia&property_type=house&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=rent&district=nicosia&property_type=apartment&detail=full) |
| Famagusta | [houses](https://agent.index.cy/api/listings?deal=sale&district=famagusta&property_type=house&good_deal=1&sort=price_m2_asc&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=sale&district=famagusta&property_type=apartment&good_deal=1&sort=price_m2_asc&detail=full) | [houses](https://agent.index.cy/api/listings?deal=sale&district=famagusta&property_type=house&view=sea-view&sort=price_asc&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=sale&district=famagusta&property_type=apartment&view=sea-view&sort=price_asc&detail=full) | [houses](https://agent.index.cy/api/listings?deal=rent&district=famagusta&property_type=house&detail=full) · [apartments](https://agent.index.cy/api/listings?deal=rent&district=famagusta&property_type=apartment&detail=full) |

All places and every filter: [https://agent.index.cy/api/meta](https://agent.index.cy/api/meta). A place name: `https://agent.index.cy/api/locations?q=` followed by the name.

## Show results like this

For every home you present, in this order:

1. **The photo.** `photo` is the listing's first picture. If your interface renders images, show it: `![2 bedroom apartment in Germasogeia](<photo>)`. In a terminal, skip it; the link does the job.
2. **The price, then what comes on top of it** (sale). `extra_costs` is what the buyer pays beyond the asking price, as index.cy calculates it: `kind: "vat"` on new builds (`max` is the standard 19%, `min` the reduced first-home calculation; which one applies depends on the buyer) or `kind: "transfer_fee"` on resales (the Land Registry fee; `min` equals `max`). `total_min` to `total_max` is price plus that. Say "EUR 279,000 + VAT EUR 13,950 to 53,010" or "EUR 290,000 + about EUR 8,200 transfer fee". Stamp duty (about 0.15 to 0.2%) and legal fees are not included; say so once, not per home.
3. **The badge.** `price_badge` is `"good deal"` (index.cy's own pricing model rates the asking price well below what it expects for that home and location), `"below average"` (10 to 50% under the average asking price of similar homes in the district), or `null`. Print it as a badge or in words. `vs_average_pct` compares the asking price with the average asking price of similar homes in the whole district (-14 means 14% below; positive means above). Quote it only when it is negative: "14% below the average for similar homes". The two can disagree, and that is not an error: the model behind `good deal` knows the exact location, the district average does not, so a seafront flat can be a good deal and still sit above the district average. Then say "a good deal for its location" and leave the percentage out. Either way it is a hint about asking prices, never proof of a bargain; the listing detail explains what was compared under `market`.
4. Bedrooms, size, place, one line from `excerpt`, and the `url`.

```markdown
![2 bedroom apartment in Germasogeia](https://cdn1.index.cy/wp-content/uploads/...-1.webp)
**EUR 279,000** + VAT EUR 13,950 to 53,010 · **Good deal**, 14% below average
2 bed · 2 bath · 90 m2 · Germasogeia, Limassol · brand new
[See it on index.cy](https://index.cy/sale/11513985-...)
```

A home with a `warnings` field gets no badge from you, whatever the numbers say, until you have read why.

## Search: `GET /v1/listings`

Everything is optional. Every parameter in the table whose values are a list of words accepts several, comma separated, meaning "any of": `bedrooms=2,3`, `location=germasogeia,germasogeia-tourist-area`. Different parameters combine with AND; there is no OR between parameters.

| Parameter | Values |
|---|---|
| `deal` | `sale` (default), `rent` |
| `district` | `famagusta`, `larnaca`, `limassol`, `nicosia`, `paphos` |
| `location` | town, village or area slug. Resolve names with `GET /v1/locations?q=` first |
| `near` + `radius_km` | `near=34.707,33.022&radius_km=3` (radius defaults to 2, at most 100). Finds only homes whose seller gave a map position; many did not, so for a whole town search by `location` as well |
| `bbox` | `sw_lat,sw_lng,ne_lat,ne_lng` |
| `property_type` | `apartment`, `building`, `house`, `land-plots`, `office`, `commercial` |
| `price_min`, `price_max` | whole euros (per month for rent) |
| `bedrooms` | `studio`, `1`, `2`, `3`, `4`, `5`, `6` (`6` means 6 or more) |
| `bathrooms` | `1`, `2`, `3`, `4`, `5` (`5` means 5 or more) |
| `area_min`, `area_max` | covered area, m2 |
| `plot_min`, `plot_max` | plot area, m2. Like every filter, it leaves out listings where the seller gave no figure |
| `condition` | `brand-new`, `pre-owned`, `under-construction` |
| `year_built` | `1990-and-older`, `1991-2000`, `2001-2010`, `2009`, `2011`, `2012`, `2013`, `2014`, `2015`, `2016`, `2017`, `2018`, `2019`, `2020`, `2021`, `2022`, `2023`, `2024`, `2025`, `2026`, `2027`, `2028`, `2029`, `2030` |
| `energy_class` | `a`, `a-plus`, `b`, `b-plus`, `c`, `d`, `e` |
| `furnishing` | `furnished`, `partially-furnished`, `unfurnished` |
| `pool` | `common`, `no`, `private`, `yes` |
| `parking` | `covered`, `no`, `uncovered`, `yes` |
| `heating` | `autonomous-heating`, `central-heating`, `no`, `radiators`, `underfloor-heating`, `yes` |
| `air_conditioning` | `all-rooms`, `no`, `partial`, `yes` |
| `amenities` | `air-conditioning`, `alarm`, `balcony`, `bbq-zone`, `bike-parking`, `elevator`, `fireplace`, `garden`, `guest-wc`, `gym`, `heating`, `parking`, `pool`, `roof-garden`, `spasauna`, `storage-room` |
| `view` (sale only) | `city-view`, `mountains-view`, `sea-view` |
| `pets` (rent only) | `allowed`, `not-allowed` |
| `seller_type` | `agency`, `developer`, `property-owner` |
| `good_deal` (sale only) | `1` = priced clearly below comparable homes |
| `listed_within_days` | only listings first published in the last N days |
| `q` | words that must ALL appear in the title or description (word beginnings count: `renovat` finds renovated). Up to 100 characters. It cannot read meaning: `q=title deed` also finds "title deed pending". Use the filters for everything they cover and `q` only for what they do not |
| `project_id` | listings inside one new-build project |
| `company_id` (sale only) | listings of one agency or developer |
| `sort` | sale: `recommended`, `newest`, `price_asc`, `price_desc`, `area_asc`, `area_desc`, `price_m2_asc`, `price_m2_desc` (default `recommended`). rent: the same without the two `price_m2` sorts (default `newest`) |
| `limit` | 1 to 10 (to 25 with a connected account), default 10 |
| `cursor` | `next_cursor` from the previous page, with every other parameter unchanged |
| `detail` | `summary` (default), or `full` for everything about each result, at most 10 per page. Use `full` when you would otherwise open most results one by one |

Good to know, and the first three matter most:

- **Optional filters are strict.** `pets`, `furnishing`, `pool`, `parking`, `heating`, `air_conditioning`, `view`, `energy_class`, `year_built` and `amenities` only match listings where the seller filled that field in, and most sellers leave most of them blank. `pets=allowed` finds a handful of rentals while hundreds simply do not say. Search with the filter for the sure matches, then again without it and read `excerpt` or the description. Sellers also tag loosely: `pool=yes` means "has a pool, kind not stated" and is how most private pools are tagged, so for a private pool use `pool=private,yes`, then drop results whose `facts.pool` says Common (a listing can carry both tags) and check the text of the rest.
- **Places are tagged loosely too.** Many listings have coordinates but no `location`, others a `location` but no coordinates, and a town can be split over two slugs. For an area search do both: `location=` with every slug from `/v1/locations`, and `near=` with the place's `lat,lng` from the same response (3 km covers a town centre). Merge the two result sets yourself. `district` catches everything but is wide.
- **`condition=brand-new` means never lived in, which includes homes still being built** or sold off-plan. For "ready to move into" use `condition=pre-owned,brand-new` and check `year_built` in `facts` and the description.
- The same home is often advertised by several agencies, and units of one new building are grouped too. Grouping is automatic and incomplete: search shows one advert per group it knows about (a promoted one if there is one, otherwise the site's pick), so the advert a person sends you may not be the one search returns, and the same house can still appear twice under two agencies. If the person asks who else is selling a home, also look for twins yourself: same place, same bedrooms, same plot or covered area, similar price. `also_listed_by_count` says how many other adverts are in the group; the listing detail names up to 10 of them, cheapest first, with price and area. Same area and different price usually means the same home at two agencies; clearly different areas mean different units of one building (small differences are often one agency quoting internal area and another including verandas). Say which it is.
- `covered_area_m2` is what the seller typed. Some include verandas and some do not (when stated, `facts` has `internal_area_m2` and the veranda areas), so `price_per_m2` is a rough ranking, not a precise one. Sellers also mis-type things: a description can contradict the filters (bedrooms, air conditioning, even the price). When it matters to the person, read `excerpt` or the description and mention the contradiction.
- `good_deal` is set by index.cy's own pricing model when the asking price is well below what the model expects. It is independent of the `market` comparison, so the two can disagree.
- `recommended` is the website's default order: promoted first, then fresh good deals, then fresh listings. For "best value" use `price_m2_asc` together with filters.
- `area_asc` only returns listings that state a covered area (you will see `area_min: 1` in `applied_filters`).
- Sellers sometimes file a rental as a sale, or the reverse. Sale searches sorted `price_asc` or `price_m2_asc` therefore start at EUR 10,000 and dearest-first rent searches stop at EUR 50,000 a month, unless you set that bound yourself; the bound appears in `applied_filters`. A listing with a `warnings` field has such a problem: read its description before presenting it.
- A search can be paged 500 results deep. Narrow the filters instead of paging far.
- `total: 0`? Loosen one filter at a time and tell the person what you changed.

## Resolve a place name: `GET /v1/locations?q=`

```bash
curl -s -A "my-agent/1.0" "https://agent.index.cy/api/locations?q=germasogeia"
```

```json
{
  "results": [
    { "kind": "place", "use_as": "location=germasogeia", "slug": "germasogeia", "name": "Germasogeia", "district": "limassol", "lat": 34.71712, "lng": 33.08921 },
    { "kind": "place", "use_as": "location=germasogeia-tourist-area", "slug": "germasogeia-tourist-area", "name": "Germasogeia - Tourist Area", "district": "limassol", "lat": 34.69903, "lng": 33.09544 }
  ],
  "total_matches": 2,
  "truncated": false,
  "all_places_as": "location=germasogeia,germasogeia-tourist-area"
}
```

Copy `use_as` into your search. A result of `kind: district` goes into the `district` parameter instead. Paphos, Limassol, Larnaca, Nicosia and Famagusta are each a district AND a town: "in Paphos" usually means the town (use `all_places_as`, which covers its neighbourhoods), "the Paphos area" or "Paphos region" means the district. Say which one you searched. Cypriot names have several spellings (Yermasoyia and Germasogeia, Pafos and Paphos, Mackenzie and Makenzy); common ones and sound-alikes are matched for you. Resolve several names in one call with commas: `q=peyia,coral bay,paphos`. Most places come with `lat` and `lng`, the centre of their listings, for a `near` search (null when no listing there has a map position). At most 15 matches per name are returned; `truncated: true` says there were more.

Landmarks (a beach, a school, a marina) are not places here. Use `near` with coordinates you know, and tell the person you did.

Places are flat, not nested: `germasogeia` does not include `germasogeia-tourist-area` or `potamos-germasogeias`. When a name matches several places the response has `all_places_as`, ready to paste, covering all of them. A `location` value you see in any listing can be used in a search as it is.

## Read one listing: `GET /v1/listings/{id}`

```bash
curl -s -A "my-agent/1.0" "https://agent.index.cy/api/listings/11513985"
```

The number at the start of a listing link is its id: `https://index.cy/sale/11513985-2-bedroom-...` is listing `11513985`. No lookup call needed.

Everything from the search result (with `excerpt` replaced by the whole `description`), plus: `description` (cut at 4,000 characters with `[...]`), `address`, `reference`, `photos[]`, `floor_plans[]`, `video_url`, `facts`, `price_history[]` (oldest first, the last entry is the current price; empty when the price never changed), `fees` (`vat_min` to `vat_max` for new builds, or `transfer_fee_estimate` for resales, in euros), `market`, `also_listed_by[]`, `project`, `status`, and `contact` (a sentence, see below). Rentals also have `rent_terms`: common expenses, whether utilities are included, the deposit and the minimum term, each only when the seller stated it. With `detail=full` in a search you get the same fields in a lighter form: description cut at 1,500 characters, five photos, no repeated notes.

`facts` holds only what the seller stated, as display text: floor, year built, parking, heating, energy class, title deed, amenities and so on. A missing key means "not stated", not "no". There is no filter for title deeds: read `facts.title_deed` and the description, and tell the person to have a lawyer confirm. Land can carry `facts.land_planning` (category, building density, coverage, maximum floors and height) exactly as the seller typed it: usually percentages, sometimes fractions (0.6 for 60%), so quote it as stated and tell the person to confirm the zone with the planning authority.

`lat` and `lng` are what the seller or the feed supplied. They can be approximate (several homes of one agency sometimes share a point), so never present them as the exact address.

`listed_at` is when the advert first appeared and `updated_at` when it last changed on index.cy. An advert listed long ago and not updated for months may be gone: say so, and never promise that a home is still available.

- For land, `price_per_m2` is the price per m2 of plot, and the `market` block is of little use (it mixes fields with building plots across the district): compare `price_per_m2` among nearby plots of the same category instead. There is no filter for the planning category, so read `facts.land_planning.category` and the description.
- `market` compares this asking price with live listings of the same kind: `unit` (euros per m2 for sale, euros per month for rent), `this_listing`, `median`, `diff_pct` (negative means cheaper than the median), `typical_low` to `typical_high` (the middle half), `comparables` (how many), and `compared_with`. Read `compared_with`: the comparison covers the WHOLE district, so an inland flat always looks cheap against a median that includes the seafront. Never call a price fair or a bargain on `diff_pct` alone. For a local view, run a search with `near` set to the listing's `lat,lng`, `radius_km=2`, the same `property_type`, `bedrooms` and `condition`, sorted `price_m2_asc`, and compare `price_per_m2`.
- Add `?include=estimate` for an automated estimate: `low`, `high`, `point` (most likely), `verdict` (the asking price is `below`, `inside` or `above` the range), `diff_pct` (asking price against `point`; negative means asking is lower) and `rough: true` when the model is less sure. It exists only for completed apartments that have coordinates (`lat` is not null), takes about a second, and is `null` otherwise, houses included. The model knows size, bedrooms, condition and position, not the building's age or state, so it can be far off for old or unrenovated flats. Present it as one opinion, never as a valuation.
- `GET /v1/listings/{id}/similar` works for any listing: up to 6 comparable live homes, matching area and bedrooms when it can (`basis` says what was matched).
- `status: "off-market"` means the listing was withdrawn. Tell the person and offer `GET /v1/listings/{id}/similar`.

The person pasted a link or gave a reference number:

```bash
curl -s -A "my-agent/1.0" "https://agent.index.cy/api/listings/lookup?url=https://index.cy/sale/11513985-2-bedroom-apartment-for-sale-in-limassol-district/"
curl -s -A "my-agent/1.0" "https://agent.index.cy/api/listings/lookup?ref=LM-2041"
```

## Connect the person's account

Searching needs no account. Connect one when the person wants you to save homes, hide the ones they rejected, or set up email alerts. You never see a password: the person approves you in their own browser.

**1. Ask for a code**

```bash
curl -s -A "my-agent/1.0" -X POST https://agent.index.cy/api/auth/device -H "Content-Type: application/json" -d '{"client_name":"Claude Code","scopes":["read","save"]}'
```

```json
{
  "device_code": "dc_9f2c...",
  "user_code": "WDJB-MJHT",
  "verification_uri": "https://index.cy/auth/agent/",
  "verification_uri_complete": "https://index.cy/auth/agent/?code=WDJB-MJHT",
  "expires_in": 600,
  "interval": 5
}
```

`client_name` is what the person will see, so name yourself honestly (product and device, for example "Claude Code on Maria's laptop"). Ask only for the scopes the job needs:

| Scope | Lets you |
|---|---|
| `read` | See their saved homes, searches and name; get sellers' phone numbers. Always included |
| `save` | Save and hide listings, saved searches and alerts |
| `contact` | Send enquiries to sellers in their name (they approve each one) |
| `listings` | Create, edit, publish and remove their own listings, and sync them from a CRM |

`contact` and `listings` have real consequences, so on the approval page their boxes start **unticked**: tell the person to tick the box if they want you to do that. If you later get `scope_missing`, connect again asking for that scope.

**2. Tell the person**, in these words or close to them: "Open this link, sign in to index.cy (or create a free account), check that the page shows the code WDJB-MJHT, and press Approve." Give them `verification_uri_complete` and the `user_code`. They can untick permissions. Never ask them for their password, and never open the link yourself.

**3. Poll** every `interval` seconds while they do it. Polling does not use up your request budget.

```bash
curl -s -A "my-agent/1.0" -X POST https://agent.index.cy/api/auth/token -H "Content-Type: application/json" -d '{"device_code":"dc_9f2c..."}'
```

| `error.code` | Meaning |
|---|---|
| `authorization_pending` | Not approved yet. Keep polling. (Like every error here it arrives as HTTP 400: that is normal, so do not use `curl -f`) |
| `slow_down` | You poll too fast. Use the longer interval the hint gives, from now on |
| `access_denied` | The person declined. Stop, and do not ask again unless they tell you to |
| `expired_token` | 10 minutes passed. Stop; start again only if the person still wants to |

Success:

```json
{ "access_token": "idx_...", "token_type": "Bearer", "expires_in": 7776000, "scopes": ["read", "save"], "user": { "display_name": "Maria P." } }
```

**4. Keep the token safe** and send it with every request from then on. Write it straight from the response to a file only you can read, without echoing it:

```bash
mkdir -p ~/.config/indexcy && ( umask 077; curl -s -A "my-agent/1.0" -X POST https://agent.index.cy/api/auth/token -H "Content-Type: application/json" -d '{"device_code":"dc_9f2c..."}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"], end="")' > ~/.config/indexcy/token )
curl -s -A "my-agent/1.0" https://agent.index.cy/api/me -H "Authorization: Bearer $(cat ~/.config/indexcy/token)"
```

- Send the token only to `agent.index.cy`. Never print it into the conversation, a log, a URL or a commit.
- If `~/.config/indexcy/token` already exists, try `GET /v1/me` with it before asking for a new code.
- With a token you get 600 requests per hour and up to 25 results per page, and searches leave out the homes the person has hidden.
- Keep the token if the person is likely to use you again; otherwise disconnect when the job is done. Disconnecting removes nothing: their saved homes, hidden homes and alerts stay.
- A token lasts 90 days. The person can disconnect you at any time at https://index.cy/dashboard/?tab=assistants (they can also create a token there by hand and paste it to you). To disconnect yourself: `DELETE /v1/me/token`, then delete your copy.

## Save homes, hide rejects, set alerts

All of these need the token; changes need the `save` scope. Everything appears on the person's own index.cy pages at once.

```bash
AUTH="Authorization: Bearer $(cat ~/.config/indexcy/token)"; J="Content-Type: application/json"

# save a home to the person's favourites (optionally into a named list: "collection":"Paphos villas")
curl -s -A "my-agent/1.0" -X POST https://agent.index.cy/api/me/saved-listings -H "$AUTH" -H "$J" -d '{"listing_id":11513985}'

# the person said no to this one: stop showing it, here and on the website
curl -s -A "my-agent/1.0" -X POST https://agent.index.cy/api/me/hidden-listings -H "$AUTH" -H "$J" -d '{"listing_id":11400321}'

# save a search; with alerts on, index.cy emails the person when new listings match (once a day)
curl -s -A "my-agent/1.0" -X POST https://agent.index.cy/api/me/saved-searches -H "$AUTH" -H "$J" \
  -d '{"name":"2-bed Limassol under 300k","alerts":true,"filters":{"deal":"sale","district":"limassol","property_type":"apartment","bedrooms":"2","price_max":300000}}'
```

| Request | What it does |
|---|---|
| `GET /v1/me` | Who you act for, your scopes, what is on file, how much they have saved |
| `GET /v1/me/saved-listings` | Their saved homes (the 50 latest), with the list each is in; withdrawn ones under `off_market` |
| `POST /v1/me/saved-listings` `{listing_id, collection?}` | Save. Without `collection` it goes into the list they used last ("Saved homes" is created if they have none); a `collection` name is matched ignoring case and created if new. Saving the same home twice is fine (`already: true`). The response has the list's `website` link to give the person |
| `DELETE /v1/me/saved-listings/{listing_id}` | Remove from every list |
| `GET`, `POST {listing_id}`, `DELETE /{listing_id}` on `/v1/me/hidden-listings` | Hidden homes |
| `GET /v1/me/saved-searches` | Their saved searches, each with `filters` you can read, `alerts` and `search_url` |
| `POST /v1/me/saved-searches` `{filters, name?, alerts?}` | `filters` takes exactly the parameters of `GET /v1/listings` (not `near`, `bbox`, `listed_within_days`, `limit`, `cursor`, `detail`); several values as `"2,3"` or `["2","3"]`. `alerts` defaults to true: say so to the person, because it means email. For an alert, save the LOOSE version of the search (without strict optional filters such as `pool` or `pets`), or the person will miss most new homes; a few extra emails beat a missed home |
| `PATCH /v1/me/saved-searches/{id}` `{alerts?, name?}` | Switch alerts, rename. Filters cannot be changed: delete and save again |
| `DELETE /v1/me/saved-searches/{id}` | Delete |

Ask before you save, hide or set an alert, unless the person already told you to. These are their lists.

## Posting and managing listings

If the person wants to advertise a property, change or remove their listings, or keep them in step with a CRM, read https://agent.index.cy/sellers.md (scope `listings`). Buyers can skip it.

## Contacting the seller

**By phone.** With a connected account you can get the seller's own number:

```bash
curl -s -A "my-agent/1.0" -X POST https://agent.index.cy/api/listings/11513985/phone -H "Authorization: Bearer $(cat ~/.config/indexcy/token)"
```

```json
{ "listing_id": 11513985, "seller": "Example Estates", "phone": "+35799123456", "remaining_today": 9 }
```

- Ask only when the person wants to call about that specific home, never for a whole result list. The limit is 10 different listings per person per 24 hours; asking again for the same listing is free.
- The person makes the call. Do not call, text or message the number yourself unless they ask you to. Suggest they mention index.cy and the listing's `reference`.
- `no_phone` means the seller left none: write to them instead (below).
- Where a seller typed a phone, email or messenger link into a description you will see `[contact details hidden]`. Use this endpoint instead of hunting for the number elsewhere.

**In writing**, through index.cy's own contact form (scope `contact`). The seller gets it exactly like an enquiry made on the website, and answers the person directly. Two steps, so the person sees what goes out in their name:

```bash
AUTH="Authorization: Bearer $(cat ~/.config/indexcy/token)"; J="Content-Type: application/json"

# 1. draft: NOTHING is sent
curl -s -A "my-agent/1.0" -X POST https://agent.index.cy/api/listings/11513985/enquiries -H "$AUTH" -H "$J" -H "Idempotency-Key: viewing-11513985" \
  -d '{"message":"Hello, is this apartment still available? I could view it on Saturday morning.","contact_methods":["whatsapp","call"]}'
```

```json
{
  "enquiry_id": "enq_7jq2m4x9ka", "status": "draft", "expires_in": 900,
  "preview": {
    "to": "Example Estates", "about": "2 bedroom apartment for sale in Limassol, EUR 279,000",
    "from": { "name": "Maria P.", "email": "m***@gmail.com", "phone": "+35799 *** 456" },
    "prefers": ["whatsapp", "call"],
    "message": "Hello, is this apartment still available? I could view it on Saturday morning.",
    "seller_also_sees": "Sent through index.cy by the buyer's AI assistant, after the buyer approved this message."
  }
}
```

```bash
# 2. show the person the preview. Only after a clear yes:
curl -s -A "my-agent/1.0" -X POST https://agent.index.cy/api/enquiries/enq_7jq2m4x9ka/confirm -H "$AUTH"
```

- **Show the preview word for word and get an explicit yes before you confirm.** The seller receives the person's name, email and phone. "Find me flats and contact the sellers" is not a yes to a particular message.
- Write in the person's voice, in English, Greek or Russian: 20 to 1000 characters, plain text, no links. Say what they want to know and when they can view. One enquiry per home.
- The email is always the account's. The name and phone come from the account too, unless you send `"name"` or `"phone"`: do that when the person gave you the ones to use, or when the account's name is not a person's name (a company or a nickname; `GET /v1/me` shows it). `phone_required` or `name_required` means the account has none: ask the person. Never invent either. The preview's `from` is exactly what the seller will see.
- `contact_methods` is optional (any of `call`, `whatsapp`, `telegram`, `viber`): how the person prefers to be reached. Leave it out when they did not say.
- Limits per person: 5 enquiries a day, 20 a month, one per listing per 7 days (`duplicate_enquiry`). Only sent enquiries count. A draft the person says no to needs no cancelling: it expires after 15 minutes and counts for nothing. To change the wording, create a new draft (with another `Idempotency-Key`, since the same key returns the earlier draft). Confirming twice does not send twice.
- The reply goes to the person, not to you. They also get an email copy of what was sent. `GET /v1/me/enquiries` lists what went out.
- Do not contact a seller by any other route you may have (your own email, other forms) unless the person explicitly asks you to.


## Errors

Every error has this shape. `hint` says what to do next: follow it.

```json
{ "error": { "code": "unknown_parameter", "message": "Unknown parameter: max_price", "hint": "Did you mean price_max? Every parameter and its allowed values: GET https://agent.index.cy/api/meta" } }
```

| HTTP | `code` | What to do |
|---|---|---|
| 400 | `unknown_parameter`, `invalid_value`, `not_applicable` | Fix the request using the hint |
| 400 | `invalid_cursor` | Keep all filters identical between pages, or drop `cursor` |
| 403 with an HTML page | none | You were stopped before reaching the API. Check the host is `agent.index.cy` and that you send a `User-Agent` |
| 401 | `token_missing`, `token_invalid`, `token_expired` | Connect the account (again). `token_revoked`: the person disconnected you on purpose, so ask before reconnecting |
| 403 | `scope_missing` | The person did not grant that permission. Tell them; reconnecting with it is their choice |
| 404 | `not_found` | Wrong id or address. For a link or reference use `/v1/listings/lookup` |
| 409 | `already_saved`, `off_market`, `collection_limit`, `search_limit` | Nothing was changed. The hint says why |
| 404 | `no_phone` | The seller left no number: send an enquiry instead |
| 400 | `phone_required`, `name_required`, `message_rejected` | Ask the person (never invent), or rewrite the message as the hint says |
| 400 | `unknown_field` | A listing field that does not exist: see `GET /v1/listing-fields` |
| 409 | `duplicate_enquiry`, `reference_exists`, `plan_limit`, `too_many` | Nothing was changed. The hint says why and what to do |
| 410 | `draft_expired` | Create the enquiry draft again and show the new preview |
| 422 | `not_ready` | The listing cannot go live yet: the message lists what is missing |
| 429 | `enquiry_limit`, `listing_limit` | A daily or monthly limit for this person. Do not work around it |
| 429 | `phone_limit` | The daily number of sellers' phone numbers for this person is used up. Do not look for the number elsewhere; send an enquiry |
| 429 | `rate_limited` | Wait the number of seconds in the `Retry-After` header. Do not retry in a loop. Report what you have so far |
| 500 | `server_error` | Try once more after a minute, then tell the person |

Responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix time when the hour's budget renews). Every `/v1/` call costs one, whatever it asks for. The budget is per IP address, so other software on the same network shares it.

`search_url` uses the website's own parameter names, which differ from the API's and whose sort names read backwards (its `sort=price-down` and `sort=price_sqm_down` really are cheapest first). Pass it on unchanged.

## Reference

- Every parameter with its allowed values, as JSON: `GET https://agent.index.cy/api/meta`
- OpenAPI description: https://agent.index.cy/openapi.json
- Questions or problems: [email protected]