---
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`

## 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/v1/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 (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/v1/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,
      "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.

Rent results have `price_period: "month"` and no `price_per_m2`, `vat_on_top`, `good_deal` or `fees`. `warnings`, when present, is a list of plain sentences about that listing.

## 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). Matches the position index.cy has on file, so a result can still show `lat: null` |
| `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/v1/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. 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`. Each place comes with `lat` and `lng`, the centre of its listings, for a `near` search. 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/v1/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[]` (each entry is a price change, oldest first; the last one is the current price), `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/v1/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/v1/listings/lookup?ref=LM-2041"
```

## Contacting the seller

Not available over the API yet. Give the person the listing `url`: the page has a contact form (no account needed) and a Call button. It helps to draft the message for them to paste. When a home has several adverts, suggest contacting the cheapest one. Phone numbers, emails and messenger links are never in API responses; where a seller typed them into a description you will see `[contact details: see the listing page]` instead. Do not send a message to a seller by any other route you may have (email, forms) and do not look for a seller's contact details elsewhere, unless the person explicitly asks you to.

Also not available yet: signing in, saving listings and email alerts for new matches. The person can do all three on the website from `search_url`.

## 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/v1/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` |
| 404 | `not_found` | Wrong id or address. For a link or reference use `/v1/listings/lookup` |
| 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/v1/meta`
- OpenAPI description: https://agent.index.cy/openapi.json
- Questions or problems: info@index.cy
