# Storage HTTP API

Source: https://modulify.ai/docs/data/storage-http-api

The seven endpoints your site and your integrations use to read and write storage.

Your project's storage is a small REST API hosted on your own CDN link. Every method is a `POST` with a JSON body and a storage key header. Anything you write is then publicly readable at the same host.

Your own base URL, and a copy of this reference with that host filled in, live in the editor's **Storage** tab under **Configuration**. Expand **Using your storage**, switch to the **External** tab, and use **Copy guide** to take the whole thing away as plain text.

## Authentication

Send the key as an `X-Storage-Key` header on every request.

```bash
curl -X POST https://your-cdn-link/list \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prefix":""}'
```

Keys start with `msk_`. Your site reads its own key from `process.env.CDN_PRIVATE_KEY` and the base URL from `process.env.CDN_URL`. Both are server side only. A missing, malformed or unknown key returns `401` with "A valid storage key is required!"

A key is scoped to exactly one project's storage. Rotating it from the Storage panel invalidates the old one immediately.

## Response envelope

Every response, success or failure, has the same shape:

```json
{
    "success": true,
    "message": "Object stored successfully.",
    "data": { "key": "notes/hello.txt", "url": "https://your-cdn-link/notes/hello.txt", "size": 8 },
    "code": 200
}
```

Treat a call as failed unless the HTTP status is ok **and** `success` is `true`. Read the payload from `data`.

## Endpoints

| Method | Path | Body | Returns |
|---|---|---|---|
| POST | `/put` | `{ key, body, contentType, encoding }` | `{ key, url, size }` |
| POST | `/get` | `{ key }` | `{ key, contentType, encoding, body, size }` |
| POST | `/list` | `{ prefix, token }` | `{ folders, files, nextToken }` |
| POST | `/delete` | `{ key }` or `{ keys }` | `{ deleted }` |
| POST | `/create-folder` | `{ name, path }` | `{ name }` |
| POST | `/delete-folder` | `{ path }` | `{ deleted }` |
| POST | `/move` | `{ keys, destination }` | `{ moved, skipped, failed }` |

### /put

Create or overwrite a file. `body` must be a string. Set `encoding` to `base64` for binary data, otherwise it is treated as UTF-8. `contentType` is optional and is guessed from the key when omitted.

```bash
curl -X POST https://your-cdn-link/put \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"notes/hello.txt","body":"hi there"}'
```

Writing to a key that already exists replaces it and purges that object from the CDN cache, so the next fetch returns the new bytes.

### /get

Read a file back. The body always comes back base64 encoded, whatever the file is, so `encoding` in the response is always `base64`.

```bash
curl -X POST https://your-cdn-link/get \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key":"notes/hello.txt"}'
```

A missing key returns `404` with "No object exists at that key!"

### /list

List one level of the bucket. `folders` is an array of folder names relative to the prefix, `files` is an array of `{ key, name, size, url }`. Results come back fifty at a time.

```bash
curl -X POST https://your-cdn-link/list \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prefix":"notes/"}'
```

When `nextToken` is not null, pass it back as `token` to get the next page. Keep going until it is null.

### /delete

Delete one file with `key`, or several with `keys`.

```bash
curl -X POST https://your-cdn-link/delete \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keys":["notes/hello.txt","notes/old.txt"]}'
```

At most 1000 keys per request. Above that you get a `400` saying so.

### /create-folder

Create an empty folder. `name` is the folder, `path` is where to put it, empty for the root.

```bash
curl -X POST https://your-cdn-link/create-folder \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"invoices","path":""}'
```

### /delete-folder

Delete a folder and everything inside it.

```bash
curl -X POST https://your-cdn-link/delete-folder \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"path":"invoices"}'
```

### /move

Move or rename files. `destination` is a folder prefix, empty for the root.

```bash
curl -X POST https://your-cdn-link/move \
  -H "X-Storage-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"keys":["notes/hello.txt"],"destination":"archive/"}'
```

The response splits the outcome into `moved`, `skipped` and `failed`.

## Reading files publicly

Reads do not go through this API. Every file is served straight from your CDN link at its own key:

```bash
curl https://your-cdn-link/uploads/logo.png
```

No key, no headers. That is what makes it good for images and downloads, and it is why private data must be read back server side with `/get` rather than linked to.

## Limits

| Limit | Value |
|---|---|
| Object size on `/put` | 100 MB |
| Request body on `/put` | 140 MB |
| Request body on every other endpoint | 2 MB |
| Object size on `/get` | 25 MB |
| Object key length | 1024 bytes |
| Keys per `/delete` | 1000 |
| Objects per `/list` page | 50 |
| Total storage per site | 500 GB |

Exceeding a body limit returns `413` with "The request body is too large." An object over the `/put` size limit returns `413` with "The object exceeds the storage upload size limit!"

## Key rules

Keys are paths, with `/` separating folders. Leading slashes, `.` and `..` segments and backslashes are stripped before the key is used, so `../../etc/passwd` cannot escape your bucket. A key that reduces to nothing is rejected with "A valid object key is required!"

`.modulify-keep` is reserved. It is the marker file used to keep empty folders alive, and writes to it are rejected.

## From your site

Modulify sites ship with a typed server only wrapper over these endpoints at `src/lib/modulify/storage`, so you rarely call them by hand. See [Storage](/docs/data/storage) for that helper and the panel it belongs to.

## Next

- [Storage](/docs/data/storage) covers the panel, the CDN link and key rotation.
- [Site databases](/docs/data/databases) is the other half of your project's persistence.