# upload_storage_file

Source: https://modulify.ai/docs/api/storage/upload-storage-file

Puts a file into a site's storage bucket, as a public or a private file.

- Title: Upload a file to storage
- Scope: `data:write`
- Access: Makes changes
- Endpoint: `POST /v1/upload_storage_file`

Pass `visibility` to choose public or private. When it is left out, overwriting an existing file keeps that file's visibility, and a new file follows the folder it lands in, or the site's own top level default, which differs from site to site: `get_storage_info` reports it as `rootVisibility` and `set_storage_root_visibility` changes it. The tool tells the client to pass `public` explicitly for a file meant to be embedded in a page or handed to every visitor, because otherwise it can land private with no URL.

A public file comes back with its permanent CDN address, ready to embed in a page. A private file has no public address: its `url` is null, or for an image a signed preview that works for 15 minutes, and only the site's own server code can read it. On a site whose CDN has not finished being provisioned, a public file has no address yet either, and the response carries a signed link valid for 7 days for an image or a null `url` for anything else, so check the `url` before embedding it.

## Request

Call it with a `POST` to `https://api.modulify.ai/v1/upload_storage_file`, sending the inputs below as a JSON object. The token needs the `data:write` scope.

It makes changes, so send an [Idempotency-Key](https://modulify.ai/docs/api/idempotency) header whenever you might retry it. A retry with the same key gets the first answer back instead of running again.

```bash
curl -X POST https://api.modulify.ai/v1/upload_storage_file \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"projectId":"PROJECT_ID","filename":"FILENAME","file":"FILE"}'
```

Over MCP, the same method is the [upload_storage_file tool](https://modulify.ai/docs/mcp/storage/upload-storage-file).

## Size and overwrites

Send the bytes base64 encoded in `file`, up to 100 MB, remembering that base64 is about a third larger than the raw file. A file over that is refused with a `413`. That is the most one request can carry, so a file up to 1 GB is uploaded from the [Storage panel](https://modulify.ai/docs/data/storage#browse-and-upload-files) instead.

Uploading over a key that already exists replaces that file with no warning and no way back, so pass `uniqueFilename` to have a suffix added instead when you are not deliberately overwriting. When `staleKept` comes back true the file was stored, but an older copy of the same key with the other visibility could not be removed, so an older public copy may still be reachable. Upload the same file again without `visibility` to finish.

## What is refused

The name is sanitized, so unsafe path characters are stripped. The reserved name `.modulify-keep` is refused with a `400`, as is a key over 1016 bytes once the folder path is added, or a key whose folder path is longer than 1001 bytes, because that folder's `.modulify-keep` file would not fit.

A filename of `put`, `get`, `list`, `delete`, `create-folder`, `delete-folder`, `move`, `set-visibility` or `signed-url`, in any folder and in any case, is refused with a `400` too, and so is a key whose last two segments are `email/send`, `email/quota`, `email/email`, `email/emails`, `analytics/status`, `analytics/realtime`, `analytics/stats`, `analytics/timeseries`, `analytics/breakdown`, `analytics/overview` or `analytics/export`, at any depth and in any case, such as a file named `send` in a folder named `email`. The CDN routes those paths to the storage, email and analytics APIs, so `list.json` is fine and `list` is not, and `email/send.json` is fine and `email/send` is not. The tool tells the client to pick another name or folder rather than retrying.

A file whose key falls inside the `CMS` folder at the top level is refused with a `403` and `The CMS folder holds the files of your CMS items, so it cannot be changed from storage. Add files from a CMS field, and they are deleted with their item!` The CMS manages that folder, and a CMS file is deleted with the item that uses it. See [The CMS folder](https://modulify.ai/docs/data/storage#the-cms-folder).

While a storage backup is being restored, the call is refused with a `409` and `Your storage is being restored from a backup. Try again when the restore finishes!` See [While a restore runs](https://modulify.ai/docs/editor/backups#while-a-restore-runs).

## Inputs

| Input | Type | Required | Description |
| --- | --- | --- | --- |
| `projectId` | string | Yes | The site id. |
| `filename` | string | Yes | The name to store it under, such as `logo.png`. No slashes, so put the folder in `path` instead. |
| `file` | string | Yes | The file contents, base64 encoded, without a data URI prefix, up to 100 MB. |
| `path` | string | No | The folder to put it in, such as `images/logos/`. Leave it out or pass an empty string for the top level of the bucket. |
| `type` | string | No | The MIME type, such as `image/png`. Defaults to `application/octet-stream`, which makes a browser download the file rather than display it. |
| `visibility` | string | No | `public` to serve the file at its CDN address, `private` to keep it readable by the site's server code only. Leave it out to keep an existing file's visibility, or to inherit the folder default for a new file. |
| `uniqueFilename` | boolean | No | True to always append a random suffix to the name, whether or not the name is taken, so an existing file is never replaced. The stored name is then never the one you sent, so read the `key` and `url` out of the response rather than composing them from `filename`. |

## Response

Every call answers with the [JSON envelope](https://modulify.ai/docs/api/requests-and-responses#the-response) of `success`, `message`, `data`, `code` and `version`. `data` holds the result described above, and on a method that returns a total, `count` carries it. The [response headers](https://modulify.ai/docs/api/requests-and-responses#headers-on-every-method-call) carry the call's `X-Request-Id` and what is left of your per-minute budget in `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. [Errors](https://modulify.ai/docs/api/errors) explains every status code a call can answer with.