# Storage

`GET | POST | PATCH | DELETE /v1/uploads` — Your reference bucket: uploaded files, private folders, one shared quota.

- Base URL: https://eroq.ai/v1
- Auth: `Authorization: Bearer eroq_sk_…` (create keys at https://eroq.ai/dashboard/keys)
- Credits: Free · counts against the shared bucket quota

The workspace's **reference bank** — the files behind /studio/storage. They live in the eroq Store bucket under `workspaces/{workspace_id}/uploads/` (single-level folders are prefixes of the key), share the quota with the library (5 GB free, more on every plan), cost **no credits**, and are NEVER public: every read is a short-lived signed URL, so nothing can hotlink your references off-site. Character and element reference photos live in the same bucket under `workspaces/{ws}/references/{characters|elements}/{id}/` and count against the same quota (`uploadsBytes` in the meter).

There is no metadata database — the key IS the record: `workspaces/{ws}/uploads/{folder?}/{id}-{Name}.ext`. The id prefix survives renames and moves, so a reference keeps working even while you reorganize. File names double as @-mention handles (`@Red Jacket` in a prompt, matched case-insensitively). Root folders are capped at 20 per workspace (409 `folder_limit`), and folder deletion is a single native recursive directory delete — children AND the directory object itself go.

The bucket takes **images, videos and sounds only** — a reference is a face, a place, a prop or a clip, and sounds (mp3, wav, m4a, aac, ogg, opus, flac) feed the Cinema timeline; anything else answers `415 type_not_accepted`. Access follows the workspace role ladder: `storage:read` to browse, `storage:upload` (Creator and above by default) to add or organise, `storage:delete` (Developer and above) to remove; a key acts with its owner's role.

`POST /v1/uploads` takes multipart `file` plus optional `folder` and `name`; over the bucket ceiling it answers 409 `storage_full` (free space or upgrade — generations still work, they just stop auto-saving). `PATCH /v1/uploads/{id}` renames and/or moves. `PATCH /v1/uploads/folders/{name}` and `DELETE /v1/uploads/folders/{name}` rezone or remove a whole folder. Reference an upload in any generation's `elements` array as `upload:<id>` — its photo joins the numbered `references` slots.

GET /v1/uploads/usage returns just the meter (`{ usage: { bucketBytes, limitBytes, over } }`) — cheap enough to consult before every render.

`POST /v1/uploads/link` brings a **public video from a link** — a TikTok, an Instagram Reel or a YouTube Short, 60 s at most — into the `Links` folder as H.264 MP4, ready for Replace (`POST /v1/videos/edits`) or any `video_reference`. A TikTok usually lands at once (`201` with the file); a link the reader takes longer on answers `202` with an `id`: poll `GET /v1/uploads/link/{id}` until `status` is `succeeded`. A private, removed or login-only video answers `422 not_public`, a longer one `400 too_long`. Free within the plan's storage, like an upload; only public videos you have the right to use.

## Operations

| Method | Path | What it does |
| --- | --- | --- |
| `GET` | `/v1/uploads` | List every reference (files + folders + the shared-bucket usage). |
| `POST` | `/v1/uploads` | Upload a reference file (multipart `file`, optional `folder`, `name`). A new folder beyond the 20-folder cap answers 409 `folder_limit`. |
| `PATCH` | `/v1/uploads/{id}` | Rename and/or move a file. |
| `DELETE` | `/v1/uploads/{id}` | Remove a file from the bucket. |
| `PATCH` | `/v1/uploads/folders/{name}` | Rename a folder — every child is rezoned (each keeps its mention-stable id), then the emptied old directory is deleted. |
| `DELETE` | `/v1/uploads/folders/{name}` | Delete a folder and everything in it — one native recursive delete, the directory object included. |
| `POST` | `/v1/uploads/link` | Bring a public TikTok, Instagram Reel or YouTube Short into Storage (`Links` folder). `201` with the file, or `202` with an `id` to poll. |
| `GET` | `/v1/uploads/link/{id}` | A link still being read: `processing`, then the file (`succeeded`) or why it could not be read. |

## GET /v1/uploads

List every reference (files + folders + the shared-bucket usage).

### Example request

```bash
curl https://eroq.ai/v1/uploads \
  -H "Authorization: Bearer $EROQ_API_KEY"
```

```javascript
const res = await fetch('https://eroq.ai/v1/uploads', {
  headers: { Authorization: `Bearer ${process.env.EROQ_API_KEY}` },
})
console.log(await res.json())
```

```python
import os, requests

res = requests.get("https://eroq.ai/v1/uploads", headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"})
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("GET", "https://eroq.ai/v1/uploads", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{
  "files": [{
    "id": "0b52a91f-3c", "name": "Red Jacket", "folder": "Wardrobe",
    "file": "0b52a91f-3c-Red_Jacket.webp", "ext": "webp",
    "bytes": 184320, "lastModified": "2026-09-21T10:11:12.000",
    "key": "workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp",
    "url": "https://store.eroq.ai/workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp?token=…"
  }],
  "folders": ["Wardrobe"],
  "folderLimit": 20,
  "usage": { "totalBytes": 1480000000, "publishedBytes": 0, "libraryBytes": 220000000,
    "uploadsBytes": 640000, "bucketBytes": 220640000, "limitBytes": 5368709120, "over": false }
}
```

## POST /v1/uploads

Upload a reference file (multipart `file`, optional `folder`, `name`). A new folder beyond the 20-folder cap answers 409 `folder_limit`.

### Example request

```bash
curl https://eroq.ai/v1/uploads \
  -H "Authorization: Bearer $EROQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "file": "@photo.webp",
  "folder": "Characters",
  "name": "Red Jacket"
}'
```

```javascript
const res = await fetch('https://eroq.ai/v1/uploads', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.EROQ_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "file": "@photo.webp",
    "folder": "Characters",
    "name": "Red Jacket"
  }),
})
console.log(await res.json())
```

```python
import os, requests

res = requests.post(
    "https://eroq.ai/v1/uploads",
    headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"},
    json={
        "file": "@photo.webp",
        "folder": "Characters",
        "name": "Red Jacket"
    },
)
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{
  "file": "@photo.webp",
  "folder": "Characters",
  "name": "Red Jacket"
}`)

	req, _ := http.NewRequest("POST", "https://eroq.ai/v1/uploads", body)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{
  "files": [{
    "id": "0b52a91f-3c", "name": "Red Jacket", "folder": "Wardrobe",
    "file": "0b52a91f-3c-Red_Jacket.webp", "ext": "webp",
    "bytes": 184320, "lastModified": "2026-09-21T10:11:12.000",
    "key": "workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp",
    "url": "https://store.eroq.ai/workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp?token=…"
  }],
  "folders": ["Wardrobe"],
  "folderLimit": 20,
  "usage": { "totalBytes": 1480000000, "publishedBytes": 0, "libraryBytes": 220000000,
    "uploadsBytes": 640000, "bucketBytes": 220640000, "limitBytes": 5368709120, "over": false }
}
```

## PATCH /v1/uploads/{id}

Rename and/or move a file.

### Example request

```bash
curl -X PATCH https://eroq.ai/v1/uploads/UPLOAD_ID \
  -H "Authorization: Bearer $EROQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Leather Jacket",
  "folder": "Wardrobe"
}'
```

```javascript
const res = await fetch('https://eroq.ai/v1/uploads/UPLOAD_ID', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.EROQ_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Leather Jacket",
    "folder": "Wardrobe"
  }),
})
console.log(await res.json())
```

```python
import os, requests

res = requests.patch(
    "https://eroq.ai/v1/uploads/UPLOAD_ID",
    headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"},
    json={
        "name": "Leather Jacket",
        "folder": "Wardrobe"
    },
)
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{
  "name": "Leather Jacket",
  "folder": "Wardrobe"
}`)

	req, _ := http.NewRequest("PATCH", "https://eroq.ai/v1/uploads/UPLOAD_ID", body)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{
  "files": [{
    "id": "0b52a91f-3c", "name": "Red Jacket", "folder": "Wardrobe",
    "file": "0b52a91f-3c-Red_Jacket.webp", "ext": "webp",
    "bytes": 184320, "lastModified": "2026-09-21T10:11:12.000",
    "key": "workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp",
    "url": "https://store.eroq.ai/workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp?token=…"
  }],
  "folders": ["Wardrobe"],
  "folderLimit": 20,
  "usage": { "totalBytes": 1480000000, "publishedBytes": 0, "libraryBytes": 220000000,
    "uploadsBytes": 640000, "bucketBytes": 220640000, "limitBytes": 5368709120, "over": false }
}
```

## DELETE /v1/uploads/{id}

Remove a file from the bucket.

### Example request

```bash
curl -X DELETE https://eroq.ai/v1/uploads/UPLOAD_ID \
  -H "Authorization: Bearer $EROQ_API_KEY"
```

```javascript
const res = await fetch('https://eroq.ai/v1/uploads/UPLOAD_ID', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.EROQ_API_KEY}` },
})
console.log(await res.json())
```

```python
import os, requests

res = requests.delete("https://eroq.ai/v1/uploads/UPLOAD_ID", headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"})
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("DELETE", "https://eroq.ai/v1/uploads/UPLOAD_ID", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{
  "files": [{
    "id": "0b52a91f-3c", "name": "Red Jacket", "folder": "Wardrobe",
    "file": "0b52a91f-3c-Red_Jacket.webp", "ext": "webp",
    "bytes": 184320, "lastModified": "2026-09-21T10:11:12.000",
    "key": "workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp",
    "url": "https://store.eroq.ai/workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp?token=…"
  }],
  "folders": ["Wardrobe"],
  "folderLimit": 20,
  "usage": { "totalBytes": 1480000000, "publishedBytes": 0, "libraryBytes": 220000000,
    "uploadsBytes": 640000, "bucketBytes": 220640000, "limitBytes": 5368709120, "over": false }
}
```

## PATCH /v1/uploads/folders/{name}

Rename a folder — every child is rezoned (each keeps its mention-stable id), then the emptied old directory is deleted.

### Example request

```bash
curl -X PATCH https://eroq.ai/v1/uploads/folders/{name} \
  -H "Authorization: Bearer $EROQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Wardrobe"
}'
```

```javascript
const res = await fetch('https://eroq.ai/v1/uploads/folders/{name}', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.EROQ_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "name": "Wardrobe"
  }),
})
console.log(await res.json())
```

```python
import os, requests

res = requests.patch(
    "https://eroq.ai/v1/uploads/folders/{name}",
    headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"},
    json={
        "name": "Wardrobe"
    },
)
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{
  "name": "Wardrobe"
}`)

	req, _ := http.NewRequest("PATCH", "https://eroq.ai/v1/uploads/folders/{name}", body)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{
  "files": [{
    "id": "0b52a91f-3c", "name": "Red Jacket", "folder": "Wardrobe",
    "file": "0b52a91f-3c-Red_Jacket.webp", "ext": "webp",
    "bytes": 184320, "lastModified": "2026-09-21T10:11:12.000",
    "key": "workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp",
    "url": "https://store.eroq.ai/workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp?token=…"
  }],
  "folders": ["Wardrobe"],
  "folderLimit": 20,
  "usage": { "totalBytes": 1480000000, "publishedBytes": 0, "libraryBytes": 220000000,
    "uploadsBytes": 640000, "bucketBytes": 220640000, "limitBytes": 5368709120, "over": false }
}
```

## DELETE /v1/uploads/folders/{name}

Delete a folder and everything in it — one native recursive delete, the directory object included.

### Example request

```bash
curl -X DELETE https://eroq.ai/v1/uploads/folders/{name} \
  -H "Authorization: Bearer $EROQ_API_KEY"
```

```javascript
const res = await fetch('https://eroq.ai/v1/uploads/folders/{name}', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.EROQ_API_KEY}` },
})
console.log(await res.json())
```

```python
import os, requests

res = requests.delete("https://eroq.ai/v1/uploads/folders/{name}", headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"})
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("DELETE", "https://eroq.ai/v1/uploads/folders/{name}", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{
  "files": [{
    "id": "0b52a91f-3c", "name": "Red Jacket", "folder": "Wardrobe",
    "file": "0b52a91f-3c-Red_Jacket.webp", "ext": "webp",
    "bytes": 184320, "lastModified": "2026-09-21T10:11:12.000",
    "key": "workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp",
    "url": "https://store.eroq.ai/workspaces/1f0c…/uploads/Wardrobe/0b52a91f-3c-Red_Jacket.webp?token=…"
  }],
  "folders": ["Wardrobe"],
  "folderLimit": 20,
  "usage": { "totalBytes": 1480000000, "publishedBytes": 0, "libraryBytes": 220000000,
    "uploadsBytes": 640000, "bucketBytes": 220640000, "limitBytes": 5368709120, "over": false }
}
```

## POST /v1/uploads/link

Bring a public TikTok, Instagram Reel or YouTube Short into Storage (`Links` folder). `201` with the file, or `202` with an `id` to poll.

### Example request

```bash
curl https://eroq.ai/v1/uploads/link \
  -H "Authorization: Bearer $EROQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://www.tiktok.com/@creator/video/7419999999999999999"
}'
```

```javascript
const res = await fetch('https://eroq.ai/v1/uploads/link', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.EROQ_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "url": "https://www.tiktok.com/@creator/video/7419999999999999999"
  }),
})
console.log(await res.json())
```

```python
import os, requests

res = requests.post(
    "https://eroq.ai/v1/uploads/link",
    headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"},
    json={
        "url": "https://www.tiktok.com/@creator/video/7419999999999999999"
    },
)
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
)

func main() {
	body := strings.NewReader(`{
  "url": "https://www.tiktok.com/@creator/video/7419999999999999999"
}`)

	req, _ := http.NewRequest("POST", "https://eroq.ai/v1/uploads/link", body)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{
  "object": "upload.link",
  "status": "succeeded",
  "file": { "id": "9c1e0a7b44d2f310", "name": "TikTok Morning routine", "folder": "Links", "ext": "mp4", "bytes": 4455255,
    "key": "workspaces/1f0c…/uploads/Links/9c1e0a7b44d2f310-TikTok_Morning_routine.mp4", "url": "https://store.eroq.ai/…?token=…" },
  "source": { "platform": "tiktok", "seconds": 10.5, "width": 576, "height": 1024, "title": "Morning routine" }
}
```

## GET /v1/uploads/link/{id}

A link still being read: `processing`, then the file (`succeeded`) or why it could not be read.

### Example request

```bash
curl https://eroq.ai/v1/uploads/link/LINK_ID \
  -H "Authorization: Bearer $EROQ_API_KEY"
```

```javascript
const res = await fetch('https://eroq.ai/v1/uploads/link/LINK_ID', {
  headers: { Authorization: `Bearer ${process.env.EROQ_API_KEY}` },
})
console.log(await res.json())
```

```python
import os, requests

res = requests.get("https://eroq.ai/v1/uploads/link/LINK_ID", headers={"Authorization": f"Bearer {os.environ['EROQ_API_KEY']}"})
print(res.json())
```

```go
package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	req, _ := http.NewRequest("GET", "https://eroq.ai/v1/uploads/link/LINK_ID", nil)
	req.Header.Set("Authorization", "Bearer "+os.Getenv("EROQ_API_KEY"))

	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	fmt.Println(string(out))
}
```

### Response


```json
{ "object": "upload.link", "id": "9c1e0a7b44d2f310.sync-4f1a…", "status": "processing" }
```

---
Canonical: https://eroq.ai/docs/uploads · Index for agents: https://eroq.ai/llms.txt · OpenAPI: https://eroq.ai/openapi.json
