Listings Worker
The listings worker handles property listing CRUD operations and image uploads to R2.
API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
POST | /api/listings/upload-image | ✅ | Upload a listing image to R2 |
POST | /api/listings | ✅ | Create a new listing |
GET | /api/listings | — | List public listings (paginated) |
GET | /api/listings/me | ✅ | List my listings |
GET | /api/listings/user/:id | — | List user’s public listings |
GET | /api/listings/:id | — | Get a single listing |
PUT | /api/listings/:id | ✅ | Update my listing |
DELETE | /api/listings/:id | ✅ | Delete my listing |
Full Flow: Create a Listing
1. POST /api/listings/upload-image (multipart/form-data, auth required)2. POST /api/listings (JSON with image URLs, auth required)POST /api/listings/upload-image
Upload a property image to R2. Requires auth.
Request
multipart/form-data with field image.
Validation
| Rule | Value |
|---|---|
| Formats | JPEG, PNG, WebP, GIF |
| Max file size | 5 MB |
| Min dimensions | 200×200 px |
| Max dimensions | 8192×8192 px |
| Validation | Magic-byte (extension not trusted) |
Response 200 OK
{ "url": "https://{r2}/listings/temp/{userId}/{uuid}.jpg", "width": 1200, "height": 800, "size": 123456}- Images are stored temporarily under
listings/temp/{userId}/ - On listing creation, they are moved to
listings/images/{listingId}/ - Rate limit: 10 uploads / 60 sec per user
POST /api/listings
Create a new property listing. Requires auth.
Request body
{ "title": "Casa com 3 quartos em Florianópolis", "description": "Linda casa com piscina e vista para o mar...", "price": 850000, "property_type": "casa", "city": "Florianópolis", "state_code": "SC", "bedrooms": 3, "bathrooms": 2, "area_m2": 180, "phone": "48999999999", "whatsapp": "48999999999", "images": [ "https://{r2}/listings/temp/{userId}/{uuid1}.jpg" ]}| Field | Type | Required | Rules |
|---|---|---|---|
title | string | ✅ | 5–200 characters |
description | string | ✅ | 10–5000 characters |
price | number | ✅ | > 0 (in BRL, whole number) |
property_type | string | ✅ | casa, apartamento, terreno, comercial, chacara |
city | string | ❌ | Free text |
state_code | string | ❌ | 2-letter Brazilian UF |
bedrooms | number | ❌ | Integer |
bathrooms | number | ❌ | Integer |
area_m2 | number | ❌ | Positive number |
phone | string | ❌ | 10–13 digits |
whatsapp | string | ❌ | 10–13 digits |
images | string[] | ✅ | At least 1 R2 URL required |
Business Rules
- 1 free listing per user. Additional listings require an active subscription.
- Returns
402 Payment Requiredwith codeSUBSCRIPTION_REQUIREDif limit exceeded. - Images are moved from
temp/tolistings/images/{listingId}/on creation.
Response 201 Created
{ "listing": { "id": "uuid", "user_id": "uuid", "title": "Casa com 3 quartos em Florianópolis", "description": "Linda casa com piscina...", "price": 850000, "property_type": "casa", "city": "Florianópolis", "state_code": "SC", "bedrooms": 3, "bathrooms": 2, "area_m2": 180, "phone": "48999999999", "whatsapp": "48999999999", "images": ["https://{r2}/listings/images/{listingId}/{uuid}.jpg"], "status": "active", "created_at": "2026-01-01T00:00:00.000Z", "updated_at": "2026-01-01T00:00:00.000Z" }}GET /api/listings
List public listings. No auth required.
Query params
| Param | Default | Max |
|---|---|---|
page | 1 | — |
limit | 20 | 50 |
Response 200 OK
{ "listings": [ ... ], "total": 100, "page": 1, "limit": 20, "totalPages": 5}- Only returns listings with
status = 'active' - Rate limit: 30 req / 60 sec per IP
GET /api/listings/me
List all listings for the authenticated user. Requires auth.
Response 200 OK
{ "listings": [ ...full Listing objects... ]}- Returns ALL statuses (
active,sold,pending,paused) - Ordered by
created_at DESC
GET /api/listings/user/:userId
List public listings for a specific user. No auth required.
Query params
Same as GET /api/listings (page, limit).
Response 200 OK
Paginated listing list, only status = 'active'.
GET /api/listings/:id
Get a single listing.
Response 200 OK
{ "listing": { ... }}- If
status !== 'active', requires auth and ownership to view - Returns
404for hidden listings
PUT /api/listings/:id
Update a listing. Requires auth + ownership.
Request body
Same schema as create, but all fields optional. You may also include:
| Field | Values |
|---|---|
status | active, sold, pending, paused |
Response 200 OK
Updated listing object.
- Replaces images in
listing_imagestable - Rate limit: 10 updates / 60 sec per user
DELETE /api/listings/:id
Delete a listing. Requires auth + ownership.
Response 200 OK
{ "message": "Anúncio excluído com sucesso"}- Also cleans up R2 objects under
listings/images/{id}/
Data Model
Listings are stored in the listings table in D1:
CREATE TABLE listings ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, title TEXT NOT NULL, description TEXT, price INTEGER, city_id INTEGER, state_code TEXT, property_type_id INTEGER, status TEXT DEFAULT 'active', images TEXT, -- JSON array of image URLs created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP);Development
cd listings-workernpm run devDeployment
cd listings-worker && npm run deploy