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

# Upload Image

> Upload an image to Gomry's storage and get back a hosted URL

Uploads an image to Gomry's own storage and returns a permanent public URL.

This is the write-then-reference counterpart to the image fields elsewhere in
the API: `cover_img` on an event and the photo fields on an experience only ever
accept a URL you already have hosted. This endpoint does the hosting.

**This endpoint does NOT attach the image to anything.** Take the `url` it
returns and set it yourself with `PATCH /v1/events/{eventId}` (`cover_img`) or
`PATCH /v1/experiences/{experience_id}` (photos).

Requires the `events:write` **or** the `experiences:write` scope. There is no
separate `uploads` scope: uploading is only useful in service of one of those
two resources.

<Warning>
  The returned URL is permanent, public and unauthenticated — anyone with the
  link can open it, and there is no delete endpoint. Only upload images you
  intend to publish.
</Warning>

## Idempotency

Pass an `Idempotency-Key` header (max 255 chars) to make retries safe. The first
request performs the upload; subsequent requests with the same key within 24
hours replay the original response verbatim and add an `Idempotent-Replay: true`
response header. Keys are scoped per API key.

## Request Body

<ParamField body="image_base64" type="string" required>
  Base64-encoded image data. A `data:<mime>;base64,` prefix is tolerated and
  stripped.

  Max 6,000,000 characters of base64 (\~4.4MB encoded, \~3.3MB decoded). The cap
  is on the encoded STRING because the request body limit applies to the JSON
  we receive; the decoded image is separately capped at 4MB.
</ParamField>

<ParamField body="content_type" type="string" required>
  MIME type of the image. One of `image/jpeg`, `image/png`, `image/webp`,
  `image/gif`.
</ParamField>

## Response

<ResponseField name="url" type="string">
  The hosted URL of the uploaded image. Use this as `cover_img` or a photo URL.
</ResponseField>

<ResponseField name="content_type" type="string">
  The stored MIME type.
</ResponseField>

<ResponseField name="size_bytes" type="integer">
  Size of the decoded image in bytes.
</ResponseField>

Returns `201 Created` on success.

## Errors

| Status | Error                | When                                                                                 |
| ------ | -------------------- | ------------------------------------------------------------------------------------ |
| `400`  | `Invalid JSON body`  | The request body was not valid JSON.                                                 |
| `400`  | `Validation failed`  | Missing or malformed `image_base64` / `content_type`, or base64 over the length cap. |
| `403`  | `insufficient_scope` | The API key has neither `events:write` nor `experiences:write`.                      |
| `413`  | —                    | The decoded image exceeds the 4MB limit.                                             |

<RequestExample>
  ```bash cURL theme={null}
  curl -X POST https://www.gomry.com/api/v1/uploads/images \
    -H "Authorization: Bearer $GOMRY_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: upload-poster-2026-03-01" \
    -d '{
      "image_base64": "iVBORw0KGgoAAAANSUhEUgAA...",
      "content_type": "image/png"
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 201 theme={null}
  {
    "url": "https://storage.googleapis.com/gomry/uploads/org_abc123/9f2c1e.png",
    "content_type": "image/png",
    "size_bytes": 248192
  }
  ```
</ResponseExample>
