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.

View as Markdown

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

RuleValue
Formats inPNG, JPEG, GIF or WebP
Size inUp to 8 MB per image
Stored asWebP, which must end at 2 MB or less after conversion
JSON request bodyUp to 12 MB, so an 8 MB image still fits after base64 encoding
galleryUp to 12 images per item; jobs take featured_image only
One requestOne 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
# 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
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
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
$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
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
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
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
$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
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
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
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
$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.

Common errors

Status and codeCauseFix
403 insufficient_scopeThe key lacks mediaCreate a key with media
413 payload_too_largeThe image is over 8 MB, or the body is over the limitResize or compress
415 unsupported_media_typeNot PNG, JPEG, GIF or WebP, or a wrong Content-TypeConvert the image; send JSON or multipart
422 validation_failedNo image_base64 and no file, or base64 that does not decode to an imageCheck the field name and the encoding
429 quota_exceededToday's media quota is used upWait 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

Last updated