Skip to main content
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

Open an Upload

Name the file, its type, and its size in bytes (wc -c < logo.png prints it).
It answers 201:
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.
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.

Add It to the Library

It answers 200 with the image, now in the brand library and on Assets:
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.

All Three Steps in One Call

The TypeScript SDK’s brew.content.uploadImage opens the upload, sends the bytes, and adds the image:
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

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).

See Also

  • 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 (sidebar): full schemas.
  • Rate limits: the 20 open uploads.
  • 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:

Search Documentation

Type in the “Ask any question” search bar at the top left to instantly find relevant documentation pages.

ChatGPT/Claude Integration

Click “Open in ChatGPT” at the top right of any page to explore it further with ChatGPT or Claude.