# Upload images, including AI generated ones

> Upload an image to Ur Next Door as base64 JSON or a multipart file, then use data.url as a featured image or in a gallery. Limits and AI image tips.

Source: https://docs.urnextdoor.com/guides/upload-images/

Images go up in their own request, then you point a post (or question, poll, MCQ, service or job) at them. Two steps:

1. `POST /api/v1/media` with the image. The response contains `data.url`, a path like `/storage/aarav/post/cover_3f9a1c2b7d4e_1792054800.webp`.
2. Send that path as `featured_image`, or in `gallery`, when you create the content.

**Endpoint:** `POST /api/v1/media` · **Scope:** `media` · **Quota:** `media`

## Limits

| Rule | Value |
| --- | --- |
| Formats in | PNG, JPEG, GIF or WebP |
| Size in | Up to 8 MB per image |
| Stored as | WebP, which must end at 2 MB or less after conversion |
| JSON request body | Up to 12 MB, so an 8 MB image still fits after base64 encoding |
| `gallery` | Up to 12 images per item; jobs take `featured_image` only |
| One request | One image |

If the converted image is still over 2 MB, the upload fails. Resize it (for example to 1600 pixels on the long side) or compress it, and try again. Very large photos straight from a camera or an image model usually need this.

## Option A: base64 JSON (best for AI tools)

Send the image as base64 in `image_base64`. It can be raw base64 or a data URI such as `data:image/png;base64,iVBOR...`. Add a `filename` (used to name the stored file) and a `context` that says what the image is for: `post` (the default), `question`, `poll`, `mcq`, `community`, `service`, `job` or `misc`.

**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"
}
```

> **Build big bodies in a file:**
>
> A base64 image is too long to pass as a command line argument (Linux limits one argument to about 128 KB). The cURL example writes the body to `upload.json` and sends the file with `--data-binary @upload.json`. Agents should do the same.

## Option B: multipart file upload

The same upload the website uses: a `multipart/form-data` request with the image in a field named `file`, plus optional `context` and `community` fields. Do not set `Content-Type` yourself; your HTTP client adds it with the boundary.

**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"
}
```

## Use the image

Pass `data.url` from the upload as `featured_image`, or put several in `gallery`:

**cURL**

```bash
curl -X POST "https://urnextdoor.com/api/v1/posts" \
  -H "Authorization: Bearer $UND_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "title": "Five easy ways to save water at home",
  "body": "<p>Small habits add up. These five saved our building a lot of water this summer.</p><ul><li>Fix dripping taps the same week.</li><li>Run the washing machine only when it is full.</li></ul>",
  "featured_image": "/storage/aarav/post/cover_3f9a1c2b7d4e_1792054800.webp",
  "tags": [
    "water",
    "home"
  ]
}'
```

**JavaScript**

```js
const res = await fetch("https://urnextdoor.com/api/v1/posts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.UND_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    title: "Five easy ways to save water at home",
    body: "<p>Small habits add up. These five saved our building a lot of water this summer.</p><ul><li>Fix dripping taps the same week.</li><li>Run the washing machine only when it is full.</li></ul>",
    featured_image: "/storage/aarav/post/cover_3f9a1c2b7d4e_1792054800.webp",
    tags: ["water", "home"],
  }),
});
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/posts",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "title": "Five easy ways to save water at home",
        "body": "<p>Small habits add up. These five saved our building a lot of water this summer.</p><ul><li>Fix dripping taps the same week.</li><li>Run the washing machine only when it is full.</li></ul>",
        "featured_image": "/storage/aarav/post/cover_3f9a1c2b7d4e_1792054800.webp",
        "tags": ["water", "home"],
    },
    timeout=30,
)
print(response.status_code, response.json())
```

**PHP**

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

$payload = [
    'title' => 'Five easy ways to save water at home',
    'body' => '<p>Small habits add up. These five saved our building a lot of water this summer.</p><ul><li>Fix dripping taps the same week.</li><li>Run the washing machine only when it is full.</li></ul>',
    'featured_image' => '/storage/aarav/post/cover_3f9a1c2b7d4e_1792054800.webp',
    'tags' => ['water', 'home'],
];

$ch = curl_init('https://urnextdoor.com/api/v1/posts');
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,
  "id": 41,
  "slug": "five-easy-ways-to-save-water-at-home",
  "url": "/posts/five-easy-ways-to-save-water-at-home",
  "web_url": "https://urnextdoor.com/posts/five-easy-ways-to-save-water-at-home",
  "item": {
    "id": 41,
    "type": "post",
    "slug": "five-easy-ways-to-save-water-at-home"
  },
  "request_id": "9f8e7d6c5b4a3921"
}
```

You can also skip the upload and pass any `https://` image URL. Ur Next Door links to it rather than copying it, so the image disappears if the other site removes it. The API does not fetch images from URLs for you.

## Images made by AI tools

A typical agent flow, for example in Claude Code or Codex CLI:

1. Generate or receive the image file (for example `cover.png` from your image model).
2. Check its size. If it is over 8 MB, or likely to be over 2 MB as WebP, resize it first.
3. Upload it with base64 JSON, building the body in a file.
4. Read `data.url` from the response.
5. Create the post with `featured_image` set to that path.
6. Give the member the post's `web_url`.

ChatGPT custom GPTs are different: an Action can only send text, up to about 100,000 characters per request, so base64 only works for very small images (about 70 KB). In a GPT, pass an `https://` image URL instead. See [ChatGPT Actions](https://docs.urnextdoor.com/ai/chatgpt-actions.md).

## Common errors

| Status and code | Cause | Fix |
| --- | --- | --- |
| `403 insufficient_scope` | The key lacks `media` | Create a key with `media` |
| `413 payload_too_large` | The image is over 8 MB, or the body is over the limit | Resize or compress |
| `415 unsupported_media_type` | Not PNG, JPEG, GIF or WebP, or a wrong `Content-Type` | Convert the image; send JSON or multipart |
| `422 validation_failed` | No `image_base64` and no `file`, or base64 that does not decode to an image | Check the field name and the encoding |
| `429 quota_exceeded` | Today's media quota is used up | Wait until 00:00 UTC |

## Frequently asked questions

### Can I use an image URL instead of uploading?

Yes. featured_image and gallery accept any https image URL. It is linked, not copied, so it breaks if the source removes it. Upload the image if you want Ur Next Door to keep its own copy.

### Can the API fetch an image from a URL and store it?

Not today. Importing an image from a URL is on the roadmap. Download it yourself and upload it with POST /api/v1/media.

### Does an upload count against my quota even if I never use the image?

Yes. Every successful upload uses one unit of the daily media quota. Failed uploads do not count.

## Next steps

- [Create a post](https://docs.urnextdoor.com/guides/create-post.md) with your image.
- [Prompt recipes](https://docs.urnextdoor.com/ai/prompt-recipes.md) for agents that generate images.
- [Rate limits and daily quotas](https://docs.urnextdoor.com/limits.md).
