Skip to contentSkip to Content
API ReferenceProperty Images

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.

  1. PresignPOST .../images/presign returns a presigned URL and an r2Key.
  2. UploadPUT the file bytes directly to that URL. Send Content-Type matching what you presigned. Wait for a 2xx response.
  3. ConfirmPOST .../images/confirm with the r2Key to 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/images

Returns 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/presign

Generates 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 }
FieldTypeDescription
fileNamestringOriginal filename, used to derive the file extension. Max 255 chars.
contentTypestringOne of image/jpeg, image/png, image/webp, image/heic.
sizeBytesintegerFile 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, contentType not allowed, or your plan’s imagesPerProperty limit is reached.

Confirm Upload

POST /api/v1/properties/:id/images/confirm

Creates 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" }
FieldTypeDescription
r2KeystringThe r2Key returned from presign. Must start with images/{userId}/{propertyId}/.
contentTypestringMust match an allowed image type.
sizeBytesintegerFile size in bytes.
altTextstring?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-batch

Request 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-batch

Request 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/import

Imports 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/:jobId

Response (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/images

Delete 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/reorder

Sets 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 maxImages in 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-Type you send on the R2 PUT must match the contentType you sent to presign — R2 will reject mismatches.
  • CDN delivery: images are served via Cloudflare Image Transformations for automatic resizing and format optimization.