List my matches
Matches
List my matches
The authenticated user’s mutual-match feed (Search results).
GET
List my matches
Overview
Returns the authenticated user’s mutual-match feed — everymatches row where the caller is a participant and has not dismissed the match. Each entry surfaces the other user’s listing (the property the caller could swap into) plus the caller’s own saved state.
A row exists only when both users pass each other’s hard filters (the matching engine persists mutual handshakes only), so every result here is a genuine two-way match. There is no public listing-browse endpoint by design — properties are only ever visible through match context, which prevents scraping.
Results are sorted by score descending, then computedAt descending, and paginated with an opaque cursor.
Authentication
Bearer <accessToken> required. Scope: user. The caller must have completed onboarding and have an approved tenancy (requireOnboarded) — otherwise 403.
Path parameters
None.Query parameters
| Param | Type | Default | Max | Notes |
|---|---|---|---|---|
cursor | string | — | — | The nextCursor from a prior page. Omit on the first page. Opaque (base64url); do not parse. |
limit | integer | 20 | 50 | Items per page. |
q | string | — | 200 chars | Case-insensitive filter on the other property’s postcode or address. Free text only — see the note below on area searches. |
lat | number | — | -90..90 | Centre of an area search. Only applied when lat, lng and radiusMiles are all present. Also the point results are sorted nearest-first from, in every scoping mode. |
lng | number | — | -180..180 | See lat. |
radiusMiles | number | — | 0.1..100 | The distance window — used only when neither the map window nor a borough applies (see Area scoping). |
swLat, swLng, neLat, neLng | number | — | — | The map window to search, as the box’s south-west and north-east corners. All four must be sent together, with swLat <= neLat. swLng > neLng is legal and means the box crosses the antimeridian. |
scope | enum | auto | — | auto | bounds. bounds forces the map window to win however small it is; auto lets the server choose (see below). |
saved | boolean | — | — | When true, restricts the feed to matches the caller has saved (savedAt != null). Powers the Saved tab. |
Area scoping
The filters are chosen from the size of the box the caller sends:| Box sent | Box filter | Borough filter | Meaning |
|---|---|---|---|
scope=bounds (any size) | yes | no | Map re-search — the visible rectangle is the question. |
| >= 15 miles | yes | no | A region (“London, UK”). No single borough represents it. |
| >= 3 miles | yes | yes | A borough pick (“Tower Hamlets”). |
| < 3 miles | yes | no | A neighbourhood (“Kilburn”, “Camden Town”). |
| no box at all | no | yes | Legacy clients — borough first, then radiusMiles. |
radiusMiles applies only on that last row when the point resolves to no borough.
Why a neighbourhood search must ignore the borough. Areas routinely straddle council boundaries. Kilburn is mostly Brent, not Camden — so scoping a “Kilburn” search to the borough its centre lands in filtered to a council with no listings and returned nothing, while searching “Camden” returned the whole borough, Kilburn included. Users search by area, not by council.Checked against a 75-area sample across Camden, Islington, Hackney and Tower Hamlets, 7 areas resolve to a different borough than the one they are commonly listed under — Highgate (Haringey), Kilburn (Brent), Tufnell Park (Islington), De Beauvoir Town (Hackney), Nag’s Head (Westminster), Newington Green (Hackney) and Millwall (Lewisham). Every one of them returned zero results under borough scoping.Why borough scoping is still kept for borough-scale picks. A borough’s bounding box is not the borough. Camden is a wedge, so its box clips Islington and Westminster at the corners — the bleed that made “Camden” show Islington results. Keeping the borough equality for boxes >= 3 miles preserves that fix.Why 3 miles separates them. Google returns neighbourhoods as
sublocality with viewports of ~0.2–2.1mi, and boroughs as administrative_area_level_3 at ~4mi+ (Tower Hamlets is 4.06mi). Of the 75 areas sampled, all 75 fell below 3 miles.Request body
None.Response — 200 OK
| Field | Type | Notes |
|---|---|---|
matches | object[] | Array of Match summary. |
nextCursor | string | null | Pass as cursor to fetch the next page. null on the last page. |
Match summary object
| Field | Type | Notes |
|---|---|---|
matchId | string | 24-char ObjectId of the match row. Use in the detail / save endpoints. |
score | integer | 0..100 compatibility score. Drives the feed ordering; the app no longer renders it. |
perfectMatch | boolean | true only when score === 100. |
matchTier | enum | PERFECT | GREAT | GOOD — the label the app shows in place of the percentage. See Match tiers. |
savedAt | string | null | ISO 8601 UTC when the caller saved this match, or null. |
computedAt | string | ISO 8601 UTC when the match was last (re)computed. Rendered as “Active <relative> ago” on the match card — there is no separate user-presence signal, so the recompute time is the freshness proxy. |
matchedAt | string | ISO 8601 UTC when the match first appeared (the row’s creation time). Never rewritten by a recompute, so this — not computedAt — is what the app’s newest/oldest sort orders on: a peer editing their listing would otherwise float a months-old match to the top of “Newest”. |
theyWantSummary | string | null | One-line summary of the peer’s desired home, e.g. "2-3 bedroom flat or terraced in Camden". null when the peer has no preferences row. The full structured version is on the detail endpoint’s theyWant. |
peer | object | { id, firstName } — the other user. firstName may be null. |
property | object | The other user’s listing. See Property object. |
Match tiers
matchTier buckets score into the label the app renders on the match card. The raw percentage read badly — a genuine mutual match with no overlapping optional features scores 0 and showed as “0% match”, which looks like a rejection even though every mandatory requirement was met on both sides.
| Tier | Score | Meaning | App copy |
|---|---|---|---|
PERFECT | 100 | Mandatory filters + every preferred feature | ”Perfect Match” (star) |
GREAT | 1–99 | Mandatory filters + some preferred features | ”Great Match” (filled tick) |
GOOD | 0 | Mandatory filters only — no preferred feature overlap | ”Good Match” (outlined tick) |
matches row only exists when every hard filter passed on both sides, so GOOD is the floor. A browse/search listing that is not a match carries match: null (see the nearby endpoint) and the app labels that “No Match” itself.
Property object
| Field | Type | Notes |
|---|---|---|
id | string | 24-char ObjectId of the listing. |
propertyType | enum | DETACHED, SEMI_DETACHED, TERRACED, FLAT, MAISONETTE, BUNGALOW. |
bedrooms | integer | 0..10. |
bathrooms | integer | 0..10. |
address | string | Free-text address. |
addressDetails | string | Owner-typed house/flat detail (e.g. Flat 4); "" when not set. |
postcode | string | UK postcode, uppercased. |
location | object | { lng, lat }. |
rent | object | { amountMinor: integer, frequency: "WEEKLY" | "MONTHLY" }. |
rentMonthlyMinor | integer | Server-derived monthly pence. |
features | string[] | Subset of: GARDEN, PARKING, BALCONY, LIFT, GROUND_FLOOR, PETS_ALLOWED, WHEELCHAIR_ACCESS. |
photos | object[] | { id, url, thumbnailUrl, orderIndex, isCover, blurhash, width, height }, ordered by orderIndex. blurhash is a placeholder string ("" when absent); width/height are the cropped pixel dims (0 when absent). |
Example response
Error responses
| Status | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_FAILED | limit out of range, or other malformed query param. |
| 401 | UNAUTHENTICATED | Missing, malformed, or expired access token. |
| 403 | ONBOARDING_INCOMPLETE / TENANCY_PENDING / TENANCY_REJECTED | Caller has not completed onboarding / tenancy is not approved. |