Appearance
Listings API
GET /listings
Browse listings with filters and pagination.
Query Parameters
| Param | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| limit | integer | Items per page (default: 20, max: 100) |
| search | string | Text search on title and description |
| neighborhoodId | uuid | Filter by single neighborhood |
| neighborhoodIds | string | Comma-separated neighborhood UUIDs (multi-select) |
| borough | string | Filter by borough name |
| propertyType | string | Comma-separated property types (e.g., apartment,condo) |
| minPrice | integer | Minimum price in cents |
| maxPrice | integer | Maximum price in cents |
| bedrooms | string | Comma-separated bedroom counts (e.g., 0,1,2). Value 4 means 4+ beds. |
| isPetFriendly | boolean | Only pet-friendly |
| hasParking | boolean | Only with parking |
| hasLaundry | boolean | Only with laundry |
| availableBy | string | Show listings available on or before this date (ISO format) |
| minArea | integer | Minimum area in sq ft |
| maxArea | integer | Maximum area in sq ft |
| postalCode | string | Postal code prefix filter (e.g., H2T or H2T1S6) |
| sort | string | price_asc, price_desc, newest, oldest, bestDeal |
| bounds | string | Map viewport: minLat,minLng,maxLat,maxLng |
| priceDrops | boolean | Only 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,neighborhoodIdamenities(string array),availableDate,contactEmail,contactPhoneisSmokingAllowed,landlordNameisScraperListing(boolean) — true if listing is owned by the scraper system user (claimable)hasContactInfo(boolean) — true if listing has contactEmail or contactPhonewalkabilityScore(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 centsbenchmarkSampleCount(integer | undefined) — number of comparables usedpricePerSqFt(number | undefined) — listing's price per sq ft (when areaSqFt available)avgPricePerSqFt(number | undefined) — average price per sq ft for same bedroom/neighborhoodpriceChange(object | undefined) — latest price drop:{ oldPrice, newPrice, changedAt }imagesarray withid,url,altText,sortOrder,isPrimary,createdAt
Owner-only fields (returned when isOwner is true):
rentalPrice(integer) — actual rent agreed, in centstenantName(string) — who rented itleaseStartDate(string) — lease start dateleaseEndDate(string) — lease end dateviews(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
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | yes | Listing title |
| description | string | yes | Listing description |
| propertyType | string | yes | apartment, condo, house, studio, loft, duplex, townhouse |
| pricePerMonth | integer | yes | Price in cents |
| bedrooms | integer | yes | Number of bedrooms (0 = studio) |
| addressStreet | string | yes | Street address |
| addressPostalCode | string | yes | Postal code |
| leaseType | string | no | fixed, month_to_month, sublet (default: fixed) |
| bathrooms | integer | no | Number of bathrooms (default: 1) |
| areaSqFt | integer | no | Area in square feet |
| neighborhoodId | uuid | no | Neighborhood reference |
| amenities | string[] | no | List of amenity labels |
| availableDate | string | no | ISO date string |
| contactEmail | string | no | Contact email |
| contactPhone | string | no | Contact phone |
| isPetFriendly | boolean | no | default: false |
| isSmokingAllowed | boolean | no | default: false |
| hasParking | boolean | no | default: false |
| hasLaundry | boolean | no | default: 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
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | yes | draft, active, rented, archived |
| rentalPrice | integer | no | Actual rent agreed, in cents (saved when status is rented) |
| tenantName | string | no | Who rented it, max 200 chars (saved when status is rented) |
| leaseStartDate | string | no | Lease start date, ISO date string (saved when status is rented) |
| leaseEndDate | string | no | Lease end date, ISO date string (saved when status is rented) |
Behavior
- When status is set to
active,publishedAtis 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 toactiveorarchived), 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.
| Field | Type | Description |
|---|---|---|
| transit.metro | NearbyPoi[] | Nearest 3 metro stations within 2km |
| transit.bus | NearbyPoi[] | Nearest 5 unique bus routes within 500m |
| transit.train | NearbyPoi[] | Nearest 3 train stations within 2km |
| schools | NearbyPoi[] | 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
| Field | Type | Required | Description |
|---|---|---|---|
| reason | string | Yes | One of: already_rented, wrong_info, spam, duplicate, other |
| comment | string | No | Additional 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" }]
}