Upload a file to storage
upload_storage_file
Puts a file into a site's storage bucket, as a public or a private 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 header whenever you might retry it. A retry with the same key gets the first answer back instead of running again.
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.
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 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.
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.
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 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 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 explains every status code a call can answer with.