---
title: POST /api/upload
description: Host an image on Vercel Blob so the content carries an absolute public URL — the prerequisite for Medium import. Raw body, x-filename header, 4.5 MB max.
---

<Note>

The web editor calls this when you drop an image. There's no dedicated CLI command — the route exists so every image in a story is a fetchable public URL, which is what [Medium import](/concepts/import-model) requires.

</Note>

Accepts an image and hosts it on Vercel Blob, returning an absolute public URL. Medium's bot fetches the story server-side and can't read inline `data:` images, so every image `src` must be a public URL.

## Endpoint

```text
POST https://scripto.codika.io/api/upload
```

## Auth

`Authorization: Bearer scripto_…` (or a session).

## Request

The **raw file** is the request body (not multipart). The original filename goes in an `x-filename` header.

| Constraint | Value |
|---|---|
| Content types | `image/png`, `image/jpeg`, `image/gif`, `image/webp`, `image/avif`, `image/svg+xml` |
| Max size | **4.5 MB** (Vercel Functions reject larger bodies) |

## Response (201)

```json
{ "success": true, "data": { "url": "https://…vercel-storage.com/articles/<user>/<uuid>-hero.png" } }
```

## Errors

| HTTP | `code` | Cause |
|---|---|---|
| 400 | `invalid-argument` | Not an image type, empty file, or > 4.5 MB. |
| 401 | `unauthenticated` | Missing or invalid key. |
| 500 | `internal` | Upload failed. |

## curl

```bash
curl -sS -X POST https://scripto.codika.io/api/upload \
  -H "Authorization: Bearer $SCRIPTO_API_KEY" \
  -H "Content-Type: image/png" \
  -H "x-filename: hero.png" \
  --data-binary @hero.png
```

## Next

- **[Import, don't paste](/concepts/import-model)** — why hosted images matter for Medium.
