> ## Documentation Index
> Fetch the complete documentation index at: https://api.housera.ro/llms.txt
> Use this file to discover all available pages before exploring further.

# Photos

> The photos of a listing: from public URLs or uploaded directly, with their order and labels.

You set the photos of a listing with `PUT /listings/{code}/photos`, which receives the **full list**, in order. Each photo comes either from a public URL or from a file you uploaded directly to Housera. You need a key with the right `media:write`.

## The list is the truth

```json theme={null}
{
  "photos": [
    { "url": "https://cdn.exemplu.ro/anunturi/1029/living.jpg", "rooms": ["living"] },
    { "key": "tmp/uploads/4812/3f6d2a90-5b1e-4c8f-a7d2-91e0b6c4f3a8.jpg", "rooms": ["kitchen"] },
    { "url": "https://cdn.exemplu.ro/anunturi/1029/fatada.jpg", "rooms": ["facade"], "floor": "ground" }
  ]
}
```

* **The order in the list is the order on the listing.** The first photo is the cover.
* **A photo missing from the list is deleted** from the listing.
* **A photo already on the listing remains**, if you send it again with the same URL: it is not downloaded a second time, it just takes its new position and new labels.
* **An empty list** (`"photos": []`) deletes all photos from the listing.
* A listing can have **a maximum of 100 photos**.

The response comes immediately:

```http theme={null}
HTTP/1.1 202 Accepted

{ "queued": 3 }
```

The photos are downloaded and processed in the background. As long as the work is not finished, the listing has `"media": { "processing": true }`. When it becomes `false`, the photos are on the listing, in `images`.

<Tip>
  Do you want to replace the content of a photo? Use a new URL (for example with a version parameter, `living.jpg?v=2`). The same URL is considered the same photo and is not downloaded again.
</Tip>

## Option 1: Public URLs

The simplest option, if your photos are already on a server accessible from the internet. Housera downloads them on its own.

Conditions for a URL:

* **HTTPS**, maximum 2,048 characters;
* on a public address (not an internal network);
* responds within a maximum of 20 seconds, with no more than two redirects;
* the file is no more than 20 MB and is a JPEG, PNG, or WebP image.

A URL that cannot be downloaded is skipped, and the other photos are applied normally. If **no** URL in a non-empty list can be downloaded (for example, your photo server is down), Housera does not touch the existing photos: a temporary problem on your side never empties a listing. Resend the list after the server is back online.

## Option 2: direct upload

For files that do not have a public URL (they are on your server's disk or in a private space), you upload them directly, in three steps.

<Steps>
  <Step title="You request an upload URL">
    `POST /uploads` with the exact type and size of the file, in bytes:

    ```bash theme={null}
    curl https://housera.ro/api/v1/uploads \
      -H "Authorization: Bearer $HOUSERA_KEY" \
      -H "Accept: application/json" \
      -H "Content-Type: application/json" \
      -d '{ "content_type": "image/jpeg", "content_length": 2483117 }'
    ```

    ```json theme={null}
    {
      "method": "PUT",
      "url": "https://.../tmp/uploads/4812/3f6d2a90-5b1e-4c8f-a7d2-91e0b6c4f3a8.jpg?Expires=...&Signature=...",
      "headers": { "Content-Type": "image/jpeg" },
      "key": "tmp/uploads/4812/3f6d2a90-5b1e-4c8f-a7d2-91e0b6c4f3a8.jpg"
    }
    ```

    `content_type` is `image/jpeg`, `image/png` or `image/webp`. `content_length` is a maximum of 20 MB (20,971,520 bytes).
  </Step>

  <Step title="You upload the file directly to the URL">
    A `PUT` with the file in the body, to the `url` received, **without** the `Authorization` header. `Content-Type` and `Content-Length` must be exactly as declared in the previous step: they are part of the signature, and a file with a different type or size is rejected.

    <CodeGroup>
      ```bash curl theme={null}
      curl -X PUT "$UPLOAD_URL" \
        -H "Content-Type: image/jpeg" \
        --data-binary @living.jpg
      ```

      ```php PHP theme={null}
      $client = new GuzzleHttp\Client();
      $client->put($upload['url'], [
          'headers' => $upload['headers'] + ['Content-Length' => filesize('living.jpg')],
          'body' => fopen('living.jpg', 'r'),
      ]);
      ```

      ```javascript JavaScript theme={null}
      import { readFile } from 'node:fs/promises';

      const file = await readFile('living.jpg');
      await fetch(upload.url, {
        method: upload.method,
        headers: upload.headers, // Content-Length se calculează din corp
        body: file,
      });
      ```

      ```python Python theme={null}
      with open("living.jpg", "rb") as f:
          data = f.read()
      requests.put(upload["url"], headers=upload["headers"], data=data)
      ```
    </CodeGroup>

    The URL is valid **for 15 minutes**. A response `200` from this URL means that the file has arrived.
  </Step>

  <Step title="You add the key to the photo list">
    Send the `key` received in step 1, in `PUT /listings/{code}/photos`, instead of a URL:

    ```json theme={null}
    { "photos": [ { "key": "tmp/uploads/4812/3f6d2a90-5b1e-4c8f-a7d2-91e0b6c4f3a8.jpg", "rooms": ["living"] } ] }
    ```

    An unused key is deleted after about 24 hours, so use it the same day. A key can only be used by the account that requested it.
  </Step>
</Steps>

You can freely mix the two options in the same list.

<Note>
  When creating the listing you can send the directly uploaded keys, in `photo_keys` (a list of keys, in order) and, optionally, `photo_meta` (a list of labels `{floor, rooms}`, aligned with `photo_keys`). For any subsequent changes use `PUT /listings/{code}/photos`.
</Note>

## Labels

Each photo can have two optional labels, displayed on the listing and used to group the photos:

<ParamField body="rooms" type="string[]">
  The rooms in the photo, at most 3 codes: `living`, `kitchen`, `bedroom`, `bathroom`, `balcony`, `yard`, `facade`, `view` and others. The complete list is in `photo_rooms` from [vocabulary](/en/vocabulary).
</ParamField>

<ParamField body="floor" type="string">
  The level at which the photo is taken, as a level code: `ground`, `floor_1`, `basement`, `attic`… (see `levels` from the vocabulary).
</ParamField>

An unknown code receives `422`, with `pointer` on the respective photo (for example `/photos/2/rooms/0`).

## What happens with a photo

Before appearing on the listing, each photo is verified and prepared:

* **the content** must be an actual JPEG, PNG, or WebP image, of no more than 100 megapixels;
* **the location metadata** (GPS coordinates in EXIF) are removed, so the exact location of the photographer does not become public;
* **the antivirus** checks each file;
* **duplicates** (the same image twice in the list) are attached only once.

A photo rejected during verification does not make it to the listing and triggers the event `media.failed`, with the number of photos and the reason:

| `reason` | Reason |
| - | - |
| `location` | The location metadata could not be removed, so the photo is not published. |
| `infected` | The antivirus found an issue in the file. |
| `unprocessable` | The file could not be processed as an image. |
| `failed` | Processing failed on Housera's side; it is retried automatically. |

## Photos on a published listing

On a listing `active`, a new list of photos does not apply immediately: it joins the editing awaiting moderation (`moderation.edit_pending: true`), just like a text modification. On a still unpublished listing (`pending`), photos are applied directly. On a rejected listing (`rejected`), a new list of photos applies and resubmits the listing for moderation, like any correction.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.