Property Images API
Upload, manage, and reorder property listing images. Images are stored on Cloudflare R2 and served via Cloudflare Image Transformations.
All endpoints support the standard Idempotency-Key header on writes — see Authentication.
Upload Flow
Direct-to-R2 upload is a three-step process. The confirm step is required — without it the file lives orphaned in R2, never appears in GET /images, and does not count toward your image quota.
- Presign —
POST .../images/presignreturns a presigned URL and anr2Key. - Upload —
PUTthe file bytes directly to that URL. SendContent-Typematching what you presigned. Wait for a2xxresponse. - Confirm —
POST .../images/confirmwith ther2Keyto create the DB row and make the image visible.
For uploading multiple files at once, use the batch endpoints.
# 1. Presign
curl -X POST https://rentalot.ai/api/v1/properties/$PROP/images/presign \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"fileName":"living-room.jpg","contentType":"image/jpeg","sizeBytes":2048576}'
# 2. Upload to R2 (use uploadUrl from step 1)
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/jpeg" \
--data-binary @living-room.jpg
# 3. Confirm
curl -X POST https://rentalot.ai/api/v1/properties/$PROP/images/confirm \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"r2Key":"images/...","contentType":"image/jpeg","sizeBytes":2048576,"altText":"Living room"}'List Images
GET /api/v1/properties/:id/imagesReturns all images for a property, ordered by display order.
Response (200 OK):
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://images.rentalot.ai/images/user123/prop456/img789.jpg",
"altText": "Living room with hardwood floors",
"order": 0
}
]
}Get Presigned Upload URL
POST /api/v1/properties/:id/images/presignGenerates a single presigned PUT URL. Honors Idempotency-Key so a retry returns the same r2Key instead of burning through your image quota.
Request body:
{
"fileName": "living-room.jpg",
"contentType": "image/jpeg",
"sizeBytes": 2048576
}| Field | Type | Description |
|---|---|---|
fileName | string | Original filename, used to derive the file extension. Max 255 chars. |
contentType | string | One of image/jpeg, image/png, image/webp, image/heic. |
sizeBytes | integer | File size in bytes. Max 10 MB (10 * 1024 * 1024). |
Response (200 OK):
{
"data": {
"uploadUrl": "https://r2.example.com/presigned-put-url...",
"r2Key": "images/user123/prop456/abc-def.jpg",
"maxImages": 20
}
}After this, PUT the file bytes to uploadUrl with the same Content-Type you presigned, then call Confirm Upload below.
Errors:
422— property not found,contentTypenot allowed, or your plan’simagesPerPropertylimit is reached.
Confirm Upload
POST /api/v1/properties/:id/images/confirmCreates the database record after a successful PUT to R2.
Request body:
{
"r2Key": "images/user123/prop456/abc-def.jpg",
"contentType": "image/jpeg",
"sizeBytes": 2048576,
"altText": "Living room with hardwood floors"
}| Field | Type | Description |
|---|---|---|
r2Key | string | The r2Key returned from presign. Must start with images/{userId}/{propertyId}/. |
contentType | string | Must match an allowed image type. |
sizeBytes | integer | File size in bytes. |
altText | string? | Optional alt text. Max 500 chars. |
Response (201 Created): Includes a Location: /api/v1/properties/:id/images/:imageId header.
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://images.rentalot.ai/images/user123/prop456/abc-def.jpg",
"altText": "Living room with hardwood floors",
"order": 1
}
}Emits the property.updated webhook event.
Batch Upload
For uploading 2–20 files at once, use the batch endpoints. The flow is the same three steps, but each call accepts an array.
Presign Batch
POST /api/v1/properties/:id/images/presign-batchRequest body:
{
"images": [
{ "fileName": "kitchen.jpg", "contentType": "image/jpeg", "sizeBytes": 1500000 },
{ "fileName": "bath.png", "contentType": "image/png", "sizeBytes": 800000 }
]
}Up to 20 entries per call. Will reject with 422 if current + requested > maxImages.
Response (200 OK):
{
"data": [
{ "fileName": "kitchen.jpg", "uploadUrl": "https://r2.example.com/...", "r2Key": "images/user123/prop456/k.jpg" },
{ "fileName": "bath.png", "uploadUrl": "https://r2.example.com/...", "r2Key": "images/user123/prop456/b.png" }
]
}Confirm Batch
POST /api/v1/properties/:id/images/confirm-batchRequest body:
{
"images": [
{ "r2Key": "images/user123/prop456/k.jpg", "contentType": "image/jpeg", "sizeBytes": 1500000, "altText": "Kitchen" },
{ "r2Key": "images/user123/prop456/b.png", "contentType": "image/png", "sizeBytes": 800000 }
]
}Response (201 Created):
{
"data": [
{ "id": "...", "url": "https://images.rentalot.ai/...", "altText": "Kitchen", "order": 1 },
{ "id": "...", "url": "https://images.rentalot.ai/...", "altText": null, "order": 2 }
]
}Quota is re-checked on confirm (not just on presign) to guard against races. Emits one property.updated webhook event.
Import Images by URL
POST /api/v1/properties/:id/images/importImports images from public URLs. The server downloads, validates, and uploads each URL to R2, then creates DB records. Processing happens asynchronously — the endpoint returns immediately with a jobId for polling.
Request body:
{
"urls": [
"https://example.com/listing-photo-1.jpg",
"https://example.com/listing-photo-2.jpg"
]
}Up to 20 URLs per call, each up to 2048 chars.
Response (202 Accepted):
{
"data": {
"jobId": "9f1e1f2a-...",
"status": "pending",
"totalUrls": 2
}
}Errors:
422— property not found, monthly import budget exhausted, or per-property image limit reached.
Poll Import Job
GET /api/v1/properties/:id/images/import/:jobIdResponse (200 OK):
{
"data": {
"jobId": "9f1e1f2a-...",
"status": "completed",
"totalUrls": 2,
"imported": 2,
"failed": 0,
"totalBytes": 2300000,
"errors": [],
"createdAt": "2026-05-08T12:00:00.000Z",
"completedAt": "2026-05-08T12:00:04.123Z"
}
}status is one of pending, processing, completed, failed.
Delete Images
DELETE /api/v1/properties/:id/imagesDelete one or more images by ID. Removes from both the database and R2 storage.
Request body:
{
"imageIds": ["550e8400-e29b-41d4-a716-446655440000"]
}Up to 20 IDs per call.
Response (200 OK):
{
"data": { "deleted": 1 }
}Emits the property.updated webhook event. IDs that don’t belong to your account are silently skipped.
Reorder Images
PATCH /api/v1/properties/:id/images/reorderSets the display order. The first ID in the array gets order = 0, the second 1, etc.
Request body:
{
"imageIds": [
"img-id-for-order-0",
"img-id-for-order-1",
"img-id-for-order-2"
]
}Up to 20 IDs per call.
Response (200 OK):
{ "ok": true }Emits the property.updated webhook event.
Notes
- Tier-limited — images per property is a plan limit. Check
maxImagesin the presign response or your tier in Settings. - Allowed types:
image/jpeg,image/png,image/webp,image/heic. Max 10 MB per image. - Content-Type matching: the
Content-Typeyou send on the R2PUTmust match thecontentTypeyou sent to presign — R2 will reject mismatches. - CDN delivery: images are served via Cloudflare Image Transformations for automatic resizing and format optimization.