# Uploading photos (https://docs.propertypixel.app/uploading-photos)

Four ways to get listing photos into a project.



Every photo belongs to a **project**. Create one first (`create_project`, or `POST /v1/projects`).

All routes accept JPEG, PNG, WebP, HEIC and HEIF files up to **25 MB** each, and a project holds up to **100 photos**. HEIC and HEIF are converted to JPEG, and large photos are resized to a 2048px long edge, as in the web app.

| Method        | Use it when                                        | MCP | REST |
| ------------- | -------------------------------------------------- | --- | ---- |
| Image URLs    | The photos are online (CDN, listing site, Dropbox) | ✓   | ✓    |
| ChatGPT files | The user attached photos in ChatGPT                | ✓   |      |
| Upload link   | The photos are on someone's phone or computer      | ✓   | ✓    |
| Signed upload | Your script has the files locally                  |     | ✓    |

## Image URLs [#image-urls]

Pass up to 25 public `https` URLs to `add_photos` (`POST /v1/projects/{project_id}/photos`):

```json
{
  "image_urls": ["https://example.com/front.jpg", "https://www.dropbox.com/s/abc/kitchen.jpg?dl=0"]
}
```

Public Dropbox and Google Drive share links are converted to direct downloads. URLs must be reachable without signing in; private and internal addresses are refused. The response lists the new `photos` and a `failed` entry, with a reason, for each URL that couldn't be used.

## ChatGPT files [#chatgpt-files]

In ChatGPT, photos attached to a message are passed to `add_photos` as `files`, and PropertyPixel downloads them. If ChatGPT can't hand over a file, PropertyPixel shows its upload widget instead.

## Upload links [#upload-links]

`create_upload_link` (`POST /v1/projects/{project_id}/upload-links`) returns a private `url` and a QR code (`qr_svg`). Open the link on a phone or computer to add photos without signing in.

* A link works for **2 hours** and only for that project.
* Photos appear in the project as they upload.
* `get_project` shows the latest link's `upload_link.status` (`waiting`, `receiving`, `done` or `expired`) and `photos_received`.
* When the person finishes, PropertyPixel sends the `upload.completed` [webhook](/docs/webhooks).

Anyone with the link can add photos to that project until it expires, so share it only with the person taking the photos.

## Signed upload [#signed-upload]

For files on disk, use the three-step signed upload (REST only).

**1. Reserve uploads** with each file's name, type and size:

```bash
curl -s -X POST "$PP_API/uploads" \
  -H "Authorization: Bearer $PP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project_id": "'"$PROJECT_ID"'",
       "files": [{"filename": "kitchen.heic", "content_type": "image/heic", "size": 3481234}]}'
```

The response has an upload `id` and, per file, an `upload_url`, the `method`, the `headers` to send and a `storage_path`.

**2. Upload each file's bytes** to its `upload_url` with exactly those headers:

```bash
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: image/heic" --data-binary @kitchen.heic
```

**3. Complete the upload**, listing every file that uploaded:

```bash
curl -s -X POST "$PP_API/uploads/$UPLOAD_ID/complete" \
  -H "Authorization: Bearer $PP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"project_id": "'"$PROJECT_ID"'",
       "files": [{"storage_path": "…", "filename": "kitchen.heic", "size": 3481234, "content_type": "image/heic"}]}'
```

PropertyPixel checks each file's size, type and contents, then adds it to the project. Files you leave out are discarded. Rejected files are listed in `failed` with a reason.
