# Upload an image

`POST https://app.pozyskajpacjenta.pl/api/v1/media` · operationId `uploadMedia` · since 1.2.0 · tag Media

Uploads an image to the clinic's media library as multipart/form-data. The field `file` is required: JPEG, PNG, WebP or AVIF, at most 10 MB. SVG answers 415 (an SVG file can carry a script; SVGs uploaded earlier are still served, with a sandboxing CSP header). The file is served at the returned `url`; width and height are read from the file.

## Request

Authorization: `Bearer <the clinic's key>`.

### Body (multipart/form-data)

multipart/form-data with the file and its optional texts.

- `file` (`file`, required): The image: JPEG, PNG, WebP or AVIF, at most 10 MB.
- `alt` (`string`): Alternative text, up to 300 characters.
- `caption` (`string`, since 1.4.0): Caption, up to 300 characters.
- `aiGenerated` (`string`, since 1.4.0): 1 or true when the image was generated by AI. One of `1`, `true`, `0`, `false`.

### cURL

```bash
curl -X POST "https://app.pozyskajpacjenta.pl/api/v1/media" \
  -H "Authorization: Bearer $PP_API_KEY" \
  -F "file=@gabinet-zabiegowy.webp" \
  -F "alt=Gabinet zabiegowy z fotelem stomatologicznym" \
  -F "caption=Gabinet zabiegowy, parter"
```

### TypeScript

```ts
import { readFile } from "node:fs/promises";
import { PozyskajPacjentaClient } from "@pozyskajpacjenta/sdk";

const client = new PozyskajPacjentaClient({
  apiKey: process.env.PP_API_KEY ?? "",
  baseUrl: "https://app.pozyskajpacjenta.pl",
});

const bytes = await readFile("gabinet-zabiegowy.webp");
const file = new Blob([new Uint8Array(bytes)], { type: "image/webp" });

const item = await client.uploadMedia(file, {
  filename: "gabinet-zabiegowy.webp",
  alt: "Gabinet zabiegowy z fotelem stomatologicznym",
  caption: "Gabinet zabiegowy, parter",
});
```

### PHP

```php
<?php
// wp_remote_post has no multipart helper: build the body with a boundary
$file     = 'gabinet-zabiegowy.webp';
$boundary = wp_generate_password( 24, false );
$fields   = array(
    'alt'     => 'Gabinet zabiegowy z fotelem stomatologicznym',
    'caption' => 'Gabinet zabiegowy, parter',
);
$body = '';
foreach ( $fields as $name => $value ) {
    $body .= "--$boundary\r\nContent-Disposition: form-data; name=\"$name\"\r\n\r\n$value\r\n";
}
$body .= "--$boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"" . basename( $file ) . "\"\r\n"
    . "Content-Type: image/webp\r\n\r\n" . file_get_contents( $file ) . "\r\n--$boundary--\r\n";

$response = wp_remote_post(
    'https://app.pozyskajpacjenta.pl/api/v1/media',
    array(
        'headers' => array(
            'Authorization' => 'Bearer ' . getenv( 'PP_API_KEY' ),
            'Content-Type'  => "multipart/form-data; boundary=$boundary",
        ),
        'body'    => $body,
        'timeout' => 30,
    )
);
if ( is_wp_error( $response ) ) {
    error_log( $response->get_error_message() );
    return;
}
$status = wp_remote_retrieve_response_code( $response );
$data   = json_decode( wp_remote_retrieve_body( $response ), true );
```

## Response

### 201: The file was stored.

- `id` (`string`, required): Media id.
- `r2Key` (`string`, required, deprecated): Internal storage key. Deprecated: do not use it or build addresses from it; use `url`.
- `url` (`string`, required): Path of the file, e.g. /api/media-file/…
- `filename` (`string`, required): Original file name.
- `contentType` (`string`): MIME type, e.g. image/webp. Listed by GET /media.
- `size` (`integer`): Size in bytes. Listed by GET /media.
- `alt` (`string | null`): Alternative text; null when empty.
- `width` (`integer | null`, since 1.4.0): Width in px; null when it could not be read from the file.
- `height` (`integer | null`, since 1.4.0): Height in px; null when it could not be read.
- `aiGenerated` (`boolean`, since 1.4.0): The image was generated by AI.
- `caption` (`string | null`, since 1.4.0): Caption; null when empty.
- `createdAt` (`string (date-time)`, since 1.9.1): Upload time, ISO 8601. Sent by GET /media (since 1.2.0), not by POST /media.

Example (File stored):

```json
{
  "id": "G4DBcqheiu_BMwt3Y_c_L",
  "r2Key": "KdYaXquTXB/Hp7X9uzN-gabinet-zabiegowy.webp",
  "url": "/api/media-file/KdYaXquTXB/Hp7X9uzN-gabinet-zabiegowy.webp",
  "filename": "gabinet-zabiegowy.webp",
  "alt": "Gabinet zabiegowy z fotelem stomatologicznym",
  "width": 960,
  "height": 600,
  "aiGenerated": false,
  "caption": "Gabinet zabiegowy, parter"
}
```

## Errors

| Status | code | When |
| --- | --- | --- |
| 400 |  | No file field |
| 400 |  | The body is not multipart/form-data |
| 401 |  | No Authorization header |
| 401 |  | Unknown or rotated key |
| 413 |  | The file is larger than 10 MB |
| 415 |  | Not a JPEG, PNG, WebP or AVIF file |
| 429 |  | Over 120 requests in this minute |

400 (No file field):

```json
{
  "error": "multipart pole 'file' jest wymagane"
}
```

400 (The body is not multipart/form-data):

```json
{
  "error": "oczekiwano multipart/form-data"
}
```

401 (No Authorization header):

```json
{
  "error": "missing Authorization: Bearer <klucz z panelu Ustawienia>"
}
```

401 (Unknown or rotated key):

```json
{
  "error": "invalid key"
}
```

413 (The file is larger than 10 MB):

```json
{
  "error": "plik przekracza 10 MB"
}
```

415 (Not a JPEG, PNG, WebP or AVIF file):

```json
{
  "error": "niedozwolony typ image/svg+xml — dozwolone: image/jpeg, image/png, image/webp, image/avif"
}
```

429 (Over 120 requests in this minute):

```json
{
  "error": "przekroczono limit 120 zapytań/min"
}
```

## Rate limit

- 120 requests per minute per key, shared by every endpoint.

## SDK method

`client.uploadMedia(file: Blob, options?: { alt?, caption?, aiGenerated?, filename? }): Promise<MediaItem>`

Throws ApiError 400, 413 or 415.
