# Media endpoint

> Upload an image as base64 JSON or multipart, then use the returned URL as a featured image or in a gallery.

Source: https://docs.urnextdoor.com/reference/media/

All paths are relative to `https://urnextdoor.com/api/v1`. See [API reference](https://docs.urnextdoor.com/reference.md) for the envelope and headers every endpoint shares.

| Endpoint | What it does | Scope | Daily quota |
| --- | --- | --- | --- |
| [`POST /media`](https://docs.urnextdoor.com/reference/media/#uploadMedia) | Upload an image | `media` | `media` |

## Upload an image {#uploadMedia}

`POST https://urnextdoor.com/api/v1/media`

Uploads one image as JSON (base64) or multipart. It is converted to WebP and must end at 2 MB or less. Use data.url as featured_image or in gallery. Counts against the media quota.

- **Auth:** your API key (Bearer or X-API-Key)
- **Scope:** `media`
- **Daily quota:** counts as `media` (see [limits](https://docs.urnextdoor.com/limits.md))
- **Idempotency-Key:** supported (see [Idempotency](https://docs.urnextdoor.com/idempotency.md))
- **operationId:** `uploadMedia`

### Request body

#### JSON (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `image_base64` | string | Yes | The image as raw base64 or a data: URI (data:image/png;base64,...). PNG, JPEG, GIF or WebP, up to 8 MB before conversion. |
| `filename` | string | No | A file name such as cover.png. Used to name the stored file. |
| `context` | string | No | What the image is for. One of: `post`, `question`, `poll`, `mcq`, `community`, `service`, `job`, `misc`. Default: `post`. |
| `community` | string | No | Optional community slug to file the image under. |

#### Multipart (`multipart/form-data`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | string | Yes | The image file. |
| `context` | string | No | What the image is for. One of: `post`, `question`, `poll`, `mcq`, `community`, `service`, `job`, `misc`. Default: `post`. |
| `community` | string | No | Optional community slug to file the image under. |

### Example

#### Base64 JSON (for AI generated images)

**cURL**

```bash
# Build the body in a file: a base64 image is too long for a command line argument.
printf '{"image_base64": "%s", "filename": "cover.png", "context": "post"}' \
  "$(base64 < cover.png | tr -d '\n')" > upload.json

curl -X POST "https://urnextdoor.com/api/v1/media" \
  -H "Authorization: Bearer $UND_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  --data-binary @upload.json
```

**JavaScript**

```js
import { readFile } from "node:fs/promises";

const image = await readFile("cover.png");

const res = await fetch("https://urnextdoor.com/api/v1/media", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.UND_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    image_base64: image.toString("base64"),
    filename: "cover.png",
    context: "post",
  }),
});
console.log(res.status, await res.json());
```

**Python**

```python
import base64
import os
import uuid

import requests

api_key = os.environ["UND_API_KEY"]

with open("cover.png", "rb") as f:
    image_base64 = base64.b64encode(f.read()).decode("ascii")

response = requests.post(
    "https://urnextdoor.com/api/v1/media",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "image_base64": image_base64,
        "filename": "cover.png",
        "context": "post",
    },
    timeout=30,
)
print(response.status_code, response.json())
```

**PHP**

```php
<?php
$apiKey = getenv('UND_API_KEY');

$payload = [
    'image_base64' => base64_encode(file_get_contents('cover.png')),
    'filename' => 'cover.png',
    'context' => 'post',
];

$ch = curl_init('https://urnextdoor.com/api/v1/media');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS => json_encode($payload),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

echo $status, PHP_EOL, $body, PHP_EOL;
```

Sample response: `201 Created`

```json
{
  "ok": true,
  "data": {
    "id": 318,
    "context": "post",
    "community_id": null,
    "url": "/storage/aarav/post/cover_3f9a1c2b7d4e_1792054800.webp",
    "disk_path": "aarav/post/cover_3f9a1c2b7d4e_1792054800.webp",
    "original_name": "cover.png",
    "extension": "webp",
    "mime_type": "image/webp",
    "size_bytes": 184320,
    "created_at": "2026-10-15 09:00:00",
    "updated_at": "2026-10-15 09:00:00"
  },
  "web_url": "https://urnextdoor.com/storage/aarav/post/cover_3f9a1c2b7d4e_1792054800.webp",
  "request_id": "9f8e7d6c5b4a3921"
}
```

#### Multipart file upload

**cURL**

```bash
curl -X POST "https://urnextdoor.com/api/v1/media" \
  -H "Authorization: Bearer $UND_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F "file=@cover.png" \
  -F 'context=post'
```

**JavaScript**

```js
import { openAsBlob } from "node:fs";

const form = new FormData();
form.append("file", await openAsBlob("cover.png", { type: "image/png" }), "cover.png");
form.append("context", "post");

const res = await fetch("https://urnextdoor.com/api/v1/media", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.UND_API_KEY}`,
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: form,
});
console.log(res.status, await res.json());
```

**Python**

```python
import os
import uuid

import requests

api_key = os.environ["UND_API_KEY"]

response = requests.post(
    "https://urnextdoor.com/api/v1/media",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    files={"file": ("cover.png", open("cover.png", "rb"), "image/png")},
    data={
        "context": "post",
    },
    timeout=30,
)
print(response.status_code, response.json())
```

**PHP**

```php
<?php
$apiKey = getenv('UND_API_KEY');

$ch = curl_init('https://urnextdoor.com/api/v1/media');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $apiKey,
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS => [
        'file' => new CURLFile('cover.png', 'image/png'),
        'context' => 'post',
    ],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);

echo $status, PHP_EOL, $body, PHP_EOL;
```

Sample response: `201 Created`

```json
{
  "ok": true,
  "data": {
    "id": 318,
    "context": "post",
    "community_id": null,
    "url": "/storage/aarav/post/cover_3f9a1c2b7d4e_1792054800.webp",
    "disk_path": "aarav/post/cover_3f9a1c2b7d4e_1792054800.webp",
    "original_name": "cover.png",
    "extension": "webp",
    "mime_type": "image/webp",
    "size_bytes": 184320,
    "created_at": "2026-10-15 09:00:00",
    "updated_at": "2026-10-15 09:00:00"
  },
  "web_url": "https://urnextdoor.com/storage/aarav/post/cover_3f9a1c2b7d4e_1792054800.webp",
  "request_id": "9f8e7d6c5b4a3921"
}
```

### Responses

| Status | Meaning | Codes |
| --- | --- | --- |
| `201 Created` | Stored. |   |
| `400 Bad Request` | The body is not a JSON object, or the Idempotency-Key header is malformed. | `invalid_json`, `invalid_idempotency_key` |
| `401 Unauthorized` | No key, or the key is unknown, malformed, revoked or expired, or the member is not active. | `missing_api_key`, `invalid_api_key` |
| `403 Forbidden` | The key lacks the scope, or the member may not do this. | `insufficient_scope`, `forbidden` |
| `404 Not Found` | Unknown route or item, or the item is not yours. | `not_found` |
| `409 Conflict` | Conflicts with the current state, or the same Idempotency-Key is still being processed. | `conflict`, `idempotency_in_progress` |
| `413 Payload Too Large` | The body is over the limit, or the image is over 8 MB. | `payload_too_large` |
| `415 Unsupported Media Type` | Wrong Content-Type, or a file type that is not allowed. | `unsupported_media_type` |
| `421 Misdirected Request` | Called on a community subdomain. Use https://urnextdoor.com. | `wrong_host` |
| `422 Unprocessable Content` | Field errors, or an Idempotency-Key reused with a different body. | `validation_failed`, `idempotency_key_reused` |
| `429 Too Many Requests` | Over 60 requests a minute from this member (or too many failed key checks from one address), or today's quota for this action is used up. | `rate_limited`, `quota_exceeded` |
| `500 Internal Server Error` | Our fault. Safe to retry with the same Idempotency-Key. | `server_error` |
| `503 Service Unavailable` | The API is switched off. | `api_disabled` |
