> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brew.new/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload a Local Image

> Add an image file from your machine to the brand library with the Brew API in three free calls: open an upload, send the bytes, then add it.

`POST /v1/content/add-image` takes a public image URL. A file on your machine
takes three free calls, because a JSON request body can't carry a file that
size. Open an upload, POST the raw bytes to the URL it returns, then add the
image with its `uploadId`.

## What You Can Upload

| Limit | Value |
| - | - |
| Types | PNG, JPEG, GIF, WebP, AVIF, TIFF, and SVG |
| Size | 20,000,000 bytes (20 MB). SVG: 2,097,152 bytes (2 MB). The app's **Upload** dialog takes files up to 25 MB. |
| Time | 15 minutes to send the bytes, then 15 minutes from when they land to add the image |
| Open uploads | 20 per brand at once. An upload stops counting once it is added or expires. |

## Open an Upload

Name the file, its type, and its size in bytes (`wc -c < logo.png` prints it).

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "https://brew.new/api/v1/content/image-uploads" \
    -H "Authorization: Bearer $BREW_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "fileName": "logo.png", "contentType": "image/png", "size": 48213 }'
  ```

  ```ts SDK theme={null}
  import { readFile } from 'node:fs/promises'
  import { createBrewClient } from '@brew.new/sdk'

  const brew = createBrewClient({ apiKey: process.env.BREW_API_KEY! })

  const bytes = await readFile('logo.png')
  const upload = await brew.content.createImageUpload({
    fileName: 'logo.png',
    contentType: 'image/png',
    size: bytes.byteLength,
  })
  ```
</CodeGroup>

It answers `201`:

```json theme={null}
{
  "uploadId": "imgup_V1StGXR8_Z5jdHi6B-myT",
  "uploadUrl": "https://…/uploads/brand-image?uploadId=imgup_V1StGXR8_Z5jdHi6B-myT&token=…",
  "expiresAt": "2026-10-02T18:15:00.000Z",
  "maxBytes": 20000000
}
```

An unsupported `contentType` or a `size` over the cap is `400 INVALID_REQUEST`.
With 20 uploads already open, the brand gets `429 RATE_LIMITED`, and
`Retry-After` is when the oldest one expires. An organization key names the
brand with `X-Brand-Id` here and on the last call, as on every brand-scoped
route.

## Send the File

POST the raw bytes to `uploadUrl` before `expiresAt`. Send no `Authorization`
header. The URL carries its own credential, so keep it as private as an API
key.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST --data-binary @logo.png "<uploadUrl>"
  ```

  ```ts TypeScript theme={null}
  await fetch(upload.uploadUrl, { method: 'POST', body: bytes })
  ```
</CodeGroup>

It answers `200` with `{ uploadId, status: "uploaded", size, expiresAt }`. The
new `expiresAt` is 15 minutes after the bytes landed, and it is your deadline
for the next call. Sending again never replaces the first file.

| Answer | Why | What to do |
| - | - | - |
| [`404 UPLOAD_NOT_FOUND`](/api-reference/api/errors#upload-not-found) | The URL is unknown or expired | Open a new upload |
| [`413 PAYLOAD_TOO_LARGE`](/api-reference/api/errors#payload-too-large) | The file is over `maxBytes` | Upload a smaller file |

## Add It to the Library

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "https://brew.new/api/v1/content/add-image" \
    -H "Authorization: Bearer $BREW_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "uploadId": "imgup_V1StGXR8_Z5jdHi6B-myT" }'
  ```

  ```ts SDK theme={null}
  const image = await brew.content.addImage({ uploadId: upload.uploadId })
  console.log(image.assetId, image.url)
  ```
</CodeGroup>

It answers `200` with the image, now in the brand library and on
[Assets](/brand/assets):

```json theme={null}
{
  "url": "https://cdn.brew.new/uploads/normalized/4d9f6217b5a70ce01e94b6498712ee63643ef769497327f9b2d523de6295d79e.png",
  "width": 1200,
  "height": 675,
  "aspectRatio": "wide",
  "assetId": "5f750e5f"
}
```

The bytes decide the format: a declared `contentType` that differs is
ignored. `fileName` names the converted file, and the image's description in
the library comes from its caption. Use `assetId` to find the image in
`GET /v1/brand/images` or to remove it with `DELETE /v1/brand/images/{assetId}`.

A repeat with the same `uploadId` returns the same answer for 24 hours, even
if the image was deleted since. A retry after a timeout is safe.

| Answer | Why | What to do |
| - | - | - |
| [`404 UPLOAD_NOT_FOUND`](/api-reference/api/errors#upload-not-found) | The upload is unknown, expired, or another brand's | Open a new upload and send the file again |
| [`409 UPLOAD_NOT_RECEIVED`](/api-reference/api/errors#upload-not-received) | The bytes never arrived | Send the file to `uploadUrl`, then call again |
| [`409 UPLOAD_IN_PROGRESS`](/api-reference/api/errors#upload-in-progress) | Another call is converting this upload | Wait a few seconds and call again with the same `uploadId` |
| [`413 PAYLOAD_TOO_LARGE`](/api-reference/api/errors#payload-too-large) | The file is over 20 MB, or an SVG over 2 MB | Upload a smaller file |
| [`422 CONTENT_OPERATION_FAILED`](/api-reference/api/errors#content-operation-failed) | The bytes aren't PNG, JPEG, GIF, WebP, AVIF, TIFF, or SVG | Upload an image file |

## All Three Steps in One Call

The TypeScript SDK's `brew.content.uploadImage` opens the upload, sends the
bytes, and adds the image:

```ts theme={null}
const image = await brew.content.uploadImage({
  file: await readFile('logo.png'),
  fileName: 'logo.png',
})
console.log(image.assetId, image.url)
```

`contentType` is optional here; the file name's extension gives it. Before
sending anything it refuses an empty file, a file over 20 MB, or an SVG over
2 MB with a `TypeError`. It does not retry opening the upload (a retry after a
lost answer would hold a second of the 20 slots), so if that step fails, call
`uploadImage` again. The other answers and errors are the ones above.

## From the CLI

```bash theme={null}
brew-cli content upload-image ./logo.png
```

It checks the type and size locally, runs all three steps, and prints the
image (`--json` for the raw answer). The two-step form is
`brew-cli content create-image-upload --file-name logo.png --size 48213`, then
the `curl` it prints, then `brew-cli content add-image --upload-id <uploadId>`.
`brew-cli brand delete-image <assetId>` removes an image (it asks first; pass
`--yes` in scripts).

## From MCP

Over MCP the flow is `create_image_upload`, the bytes to `uploadUrl`, then
`add_image` with the `uploadId`. Sending the bytes takes a client with a shell
or an HTTP tool on the file's machine, such as Claude Code or Cursor. A
chat-only client should ask for a public link and call `add_image` with
`imageUrl` (see [Assets](/brand/assets#from-an-agent)).

## See Also

* [Assets](/brand/assets): find, add, and delete images in the app and from an agent.
* `POST /v1/content/image-uploads` and `POST /v1/content/add-image` in the [Public API v1 reference](/api-reference/api/api-introduction) (sidebar): full schemas.
* [Rate limits](/api-reference/api/rate-limits#open-image-uploads): the 20 open uploads.
* [Errors](/api-reference/api/errors): every code above.

## Need Help?

Our team is ready to support you at every step of your journey with Brew. Choose the option that works best for you:

<Tabs>
  <Tab title="Self-Service Tools">
    <CardGroup cols="2">
      <Card title="Search Documentation" icon="magnifying-glass" color="#c44925">
        Type in the "Ask any question" search bar at the top left to instantly find relevant documentation pages.
      </Card>

      <Card title="ChatGPT/Claude Integration" icon="robot" color="#c44925">
        Click "Open in ChatGPT" at the top right of any page to explore it further with ChatGPT or Claude.
      </Card>
    </CardGroup>
  </Tab>

  <Tab title="Talk to Our Team">
    <CardGroup cols="2">
      <Card title="Schedule a Call" icon="calendar" color="#c44925" href="https://calendar.google.com/calendar/u/0/appointments/schedules/AcZssZ1iYoRUG1J792XQpbuQLjSRRDupr7MwraFK-HQRCtTYdBmrQi8nZu2qXfzKQigb8gbKJK3KN3-R">
        Book time with our founders for personalized guidance on strategy, best practices, or complex implementation questions.
      </Card>

      <Card title="Call Us Directly" icon="phone" color="#c44925">
        Need immediate assistance? Reach us at **+1-(332)-203-2145** for urgent issues or time-sensitive questions.
      </Card>

      <Card title="Slack Channel" icon="slack" color="#c44925">
        Our preferred support channel. You'll receive an invite after signup for direct founder support and fast responses.
      </Card>

      <Card title="Email Support" icon="envelope" color="#c44925" href="mailto:support@brew.new">
        Contact us at **[support@brew.new](mailto:support@brew.new)** for detailed inquiries or if you prefer not to use Slack.
      </Card>
    </CardGroup>
  </Tab>
</Tabs>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.