Skip to content

Listings API

GET /listings

Browse listings with filters and pagination.

Query Parameters

ParamTypeDescription
pageintegerPage number (default: 1)
limitintegerItems per page (default: 20, max: 100)
searchstringText search on title and description
neighborhoodIduuidFilter by single neighborhood
neighborhoodIdsstringComma-separated neighborhood UUIDs (multi-select)
boroughstringFilter by borough name
propertyTypestringComma-separated property types (e.g., apartment,condo)
minPriceintegerMinimum price in cents
maxPriceintegerMaximum price in cents
bedroomsstringComma-separated bedroom counts (e.g., 0,1,2). Value 4 means 4+ beds.
isPetFriendlybooleanOnly pet-friendly
hasParkingbooleanOnly with parking
hasLaundrybooleanOnly with laundry
availableBystringShow listings available on or before this date (ISO format)
minAreaintegerMinimum area in sq ft
maxAreaintegerMaximum area in sq ft
postalCodestringPostal code prefix filter (e.g., H2T or H2T1S6)
sortstringprice_asc, price_desc, newest, oldest, bestDeal
boundsstringMap viewport: minLat,minLng,maxLat,maxLng
priceDropsbooleanOnly show listings with price drops

Response

json
{
  "listings": [
    {
      "id": "uuid",
      "title": "Bright 3.5 on Plateau",
      "description": "...",
      "propertyType": "apartment",
      "pricePerMonth": 145000,
      "bedrooms": 1,
      "bathrooms": 1,
      "areaSqFt": 650,
      "addressCity": "Montreal",
      "latitude": "45.5235000",
      "longitude": "-73.5720000",
      "isPetFriendly": true,
      "hasParking": false,
      "hasLaundry": true,
      "publishedAt": "2026-02-28T00:00:00.000Z",
      "neighborhoodName": "Le Plateau-Mont-Royal",
      "neighborhoodSlug": "plateau",
      "borough": "Le Plateau-Mont-Royal",
      "walkabilityScore": 94,
      "transitScore": 85,
      "primaryImage": {
        "listingId": "uuid",
        "url": "https://...",
        "altText": "Main photo"
      },
      "priceRating": "good_price",
      "pricePctile": 28,
      "benchmarkMedian": 160000,
      "priceChange": {
        "oldPrice": 155000,
        "newPrice": 145000,
        "changedAt": "2026-03-05T12:00:00.000Z"
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 20,
    "totalPages": 1
  }
}

GET /listings/:id/similar

Returns similar listings based on a weighted scoring formula.

Params: id — listing UUID

Query: limit (int, default 6, max 20)

Scoring (0–100):

  • Bedrooms (weight 40): same = 1.0, ±1 = 0.5, ±2+ = 0
  • Price (weight 35): 1 − |price_diff| / source_price, clamped 0–1
  • Distance (weight 25): 1 − km / 10, clamped 0–1 (haversine)

Minimum threshold: score > 20. Only active listings, excludes source listing.

Response: { "listings": [ ListingSummary... ] }


GET /listings/:id

Get full listing detail with all images.

Response

Returns the same fields as the list endpoint plus:

  • status, leaseType, addressStreet, addressUnit, addressProvince, addressPostalCode, neighborhoodId
  • amenities (string array), availableDate, contactEmail, contactPhone
  • isSmokingAllowed, landlordName
  • isScraperListing (boolean) — true if listing is owned by the scraper system user (claimable)
  • hasContactInfo (boolean) — true if listing has contactEmail or contactPhone
  • walkabilityScore (integer | null) — neighborhood walkability score (0–100, from neighborhood data)
  • transitScore (integer | null) — neighborhood transit accessibility score (0–100, from neighborhood data)
  • areaMedianIncome (integer | null) — median household income in dollars for the listing's FSA postal area (from census data). Used for affordability calculations.
  • priceRating (string | undefined) — price tier: great_deal, good_price, fair_price, above_average, high_price. Only present when >= 3 comparables exist.
  • pricePctile (integer | undefined) — estimated percentile rank 0-100 (lower = cheaper relative to market)
  • benchmarkMedian, benchmarkP20, benchmarkP80 (integer | undefined) — rent benchmark prices in cents
  • benchmarkSampleCount (integer | undefined) — number of comparables used
  • pricePerSqFt (number | undefined) — listing's price per sq ft (when areaSqFt available)
  • avgPricePerSqFt (number | undefined) — average price per sq ft for same bedroom/neighborhood
  • priceChange (object | undefined) — latest price drop: { oldPrice, newPrice, changedAt }
  • images array with id, url, altText, sortOrder, isPrimary, createdAt

Owner-only fields (returned when isOwner is true):

  • rentalPrice (integer) — actual rent agreed, in cents
  • tenantName (string) — who rented it
  • leaseStartDate (string) — lease start date
  • leaseEndDate (string) — lease end date
  • views (integer) — total detail page view count

GET /listings/my

Get current landlord's listings (all statuses). Requires landlord auth.

Each listing in the response includes noteCount (integer) — the number of private notes on the listing.

POST /listings

Create a new listing. Requires landlord auth. Creates with status: "draft".

Body

FieldTypeRequiredDescription
titlestringyesListing title
descriptionstringyesListing description
propertyTypestringyesapartment, condo, house, studio, loft, duplex, townhouse
pricePerMonthintegeryesPrice in cents
bedroomsintegeryesNumber of bedrooms (0 = studio)
addressStreetstringyesStreet address
addressPostalCodestringyesPostal code
leaseTypestringnofixed, month_to_month, sublet (default: fixed)
bathroomsintegernoNumber of bathrooms (default: 1)
areaSqFtintegernoArea in square feet
neighborhoodIduuidnoNeighborhood reference
amenitiesstring[]noList of amenity labels
availableDatestringnoISO date string
contactEmailstringnoContact email
contactPhonestringnoContact phone
isPetFriendlybooleannodefault: false
isSmokingAllowedbooleannodefault: false
hasParkingbooleannodefault: false
hasLaundrybooleannodefault: false

PUT /listings/:id

Update a listing. Requires auth + ownership. Accepts any field from POST body.

PATCH /listings/:id/status

Change listing status. Requires auth + ownership. Sets publishedAt when status becomes active.

Body

FieldTypeRequiredDescription
statusstringyesdraft, active, rented, archived
rentalPriceintegernoActual rent agreed, in cents (saved when status is rented)
tenantNamestringnoWho rented it, max 200 chars (saved when status is rented)
leaseStartDatestringnoLease start date, ISO date string (saved when status is rented)
leaseEndDatestringnoLease end date, ISO date string (saved when status is rented)

Behavior

  • When status is set to active, publishedAt is set to the current time.
  • When status is set to rented, the optional rental fields (rentalPrice, tenantName, leaseStartDate, leaseEndDate) are saved alongside the status change.
  • When status changes away from rented (e.g., back to active or archived), all rental fields are cleared (set to null).

Example

json
{
  "status": "rented",
  "rentalPrice": 155000,
  "tenantName": "Jean Dupont",
  "leaseStartDate": "2026-04-01",
  "leaseEndDate": "2027-03-31"
}

DELETE /listings/:id

Delete a listing and all associated images/favorites. Requires auth + ownership.

POST /listings/:listingId/images

Upload images (multipart/form-data). Requires auth + ownership. First image auto-sets as primary.

  • Field name: file (multiple allowed)
  • Max file size: 5MB
  • Allowed types: JPEG, PNG, WebP, GIF

Response

json
{
  "images": [
    { "id": "uuid", "url": "/uploads/uuid.png", "sortOrder": 0, "isPrimary": true }
  ]
}

DELETE /listings/:listingId/images/:imageId

Delete an image. Requires auth + ownership. If deleted image was primary, next image becomes primary.

PATCH /listings/:listingId/images/:imageId

Set an image as the primary image. Requires auth + ownership.

GET /listings/:id/nearby

Get nearby transit stops and schools for a listing.

Response

Returns POIs grouped by category, ordered by distance.

FieldTypeDescription
transit.metroNearbyPoi[]Nearest 3 metro stations within 2km
transit.busNearbyPoi[]Nearest 5 unique bus routes within 500m
transit.trainNearbyPoi[]Nearest 3 train stations within 2km
schoolsNearbyPoi[]Nearest 3 schools within 2km

NearbyPoi fields: name, nameFr, category, subcategory, distanceM, latitude, longitude

Example

POST /listings/:id/report

Report a listing as outdated, spam, duplicate, etc. No authentication required.

Body

FieldTypeRequiredDescription
reasonstringYesOne of: already_rented, wrong_info, spam, duplicate, other
commentstringNoAdditional details (max 2000 chars)

Rate Limiting

Max 5 reports per IP per hour. Returns 429 if exceeded.

Response (201)

json
{
  "report": { "id": "uuid" }
}

json
{
  "transit": {
    "metro": [{ "name": "Joliette", "nameFr": "Joliette", "category": "metro", "subcategory": "green_line", "distanceM": 466, "latitude": "45.5468190", "longitude": "-73.5514270" }],
    "bus": [{ "name": "185 Sherbrooke / De Chambly", "nameFr": "Sherbrooke / De Chambly", "category": "bus", "subcategory": null, "distanceM": 135, "latitude": "45.5490380", "longitude": "-73.5552340" }],
    "train": []
  },
  "schools": [{ "name": "Cégep de Maisonneuve", "nameFr": "Cégep de Maisonneuve", "category": "school", "subcategory": "cegep", "distanceM": 381, "latitude": "45.5510410", "longitude": "-73.5534260" }]
}