Skip to main content
All File Manager endpoints authenticate via the EF-Access-Key header. No session cookie or CSRF token is required.

Upload File

Upload a single file to the brand’s asset storage.
POST /api/brands/{brand}/file-manager/upload-file
brand
string
required
The brand/project ID
file
file
required
Binary file (multipart/form-data)
path
string
Target subdirectory within the brand’s assets folder (e.g. media, media/icons). Defaults to the root assets folder. Omit leading/trailing slashes.
custom_filename
string
Override the stored filename. Defaults to the original filename from the file field.
Accepted MIME types: images (image/*), video (video/mp4, video/webm, video/quicktime), fonts (font/ttf, font/woff, font/woff2, font/otf), code/text (text/css, text/javascript, application/json, application/javascript).
cURL
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/upload-file \
  -H "EF-Access-Key: your_api_key_here" \
  -F "file=@/path/to/cards.png" \
  -F "path=media" \
  -F "custom_filename=cards.png"
JavaScript
const form = new FormData();
form.append('file', fileBlob, 'cards.png');
form.append('path', 'media');

const res = await fetch(
  'https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/upload-file',
  { method: 'POST', headers: { 'EF-Access-Key': 'your_api_key_here' }, body: form }
);
const result = await res.json();
// CDN URL: `https://cdn.elasticfunnels.io/{brand_id}/assets/media/${filename}`
{
  "HttpCode": 201,
  "Message": "File Uploaded.",
  "file_path": "ef-files/42/assets/media/cards.png"
}
The CDN URL is not included in the upload response. Derive it as: https://cdn.elasticfunnels.io/{brand_id}/assets/{path}/{filename}Example: https://cdn.elasticfunnels.io/42/assets/media/cards.png

Bulk Upload Files

Upload up to 20 files in a single request. Each file is validated and uploaded independently — files that fail validation are reported per-file without aborting the rest.
POST /api/brands/{brand}/file-manager/bulk-upload-files
brand
string
required
The brand/project ID
files[]
file[]
required
Array of files (multipart/form-data). Maximum 20 files per request.
path
string
Target subdirectory for all files in this batch (e.g. media). All files land in the same folder.
filenames[]
string[]
Optional array of custom filenames, positionally matched to files[]. Use to override individual filenames. filenames[0] overrides the name of files[0], etc.
Per-file validation rules:
  • File must be a valid upload (not corrupted in transit)
  • Maximum 10 MB per file
  • MIME type must be in the allowed list (same as single upload)
cURL
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/bulk-upload-files \
  -H "EF-Access-Key: your_api_key_here" \
  -F "files[]=@/assets/refund-icon.png" \
  -F "files[]=@/assets/guarantee-badge.png" \
  -F "files[]=@/assets/trust-seal.svg" \
  -F "files[]=@/assets/virus.exe" \
  -F "path=media"
Python
import requests

BRAND_ID = 42
BASE = f"https://app.elasticfunnels.io/api/brands/{BRAND_ID}/file-manager"
HEADERS = {"EF-Access-Key": "your_api_key_here"}

asset_paths = [
    "assets/universal/refund-icon.png",
    "assets/universal/guarantee-badge.png",
    "assets/universal/trust-seal.svg",
    "assets/universal/cc-icons.png",
    "assets/product/bottle-shot.png",
    "assets/product/logo.png",
]

files = [("files[]", (Path(p).name, open(p, "rb"))) for p in asset_paths]

response = requests.post(
    f"{BASE}/bulk-upload-files",
    headers=HEADERS,
    files=files,
    data={"path": "media"},
)

result = response.json()
print(f"Uploaded: {result['summary']['uploaded']}, Failed: {result['summary']['failed']}")

for f in result["files"]:
    if f["status"] == "uploaded":
        print(f"  ✓ {f['filename']}{f['url']}")
    else:
        print(f"  ✗ {f['filename']}: {f['error']}")
Node.js
import FormData from 'form-data';
import fs from 'fs';
import fetch from 'node-fetch';

const BRAND_ID = 42;
const form = new FormData();

const assets = [
  'assets/refund-icon.png',
  'assets/guarantee-badge.png',
  'assets/trust-seal.svg',
];

for (const p of assets) {
  form.append('files[]', fs.createReadStream(p));
}
form.append('path', 'media');

const res = await fetch(
  `https://app.elasticfunnels.io/api/brands/${BRAND_ID}/file-manager/bulk-upload-files`,
  { method: 'POST', headers: { 'EF-Access-Key': 'your_api_key_here', ...form.getHeaders() }, body: form }
);

const { summary, files } = await res.json();
console.log(`${summary.uploaded} uploaded, ${summary.failed} failed`);
{
  "summary": {
    "total": 4,
    "uploaded": 3,
    "failed": 1
  },
  "files": [
    {
      "filename": "refund-icon.png",
      "status": "uploaded",
      "url": "https://cdn.elasticfunnels.io/42/assets/media/refund-icon.png",
      "size": 18432,
      "mime_type": "image/png",
      "width": 200,
      "height": 200
    },
    {
      "filename": "guarantee-badge.png",
      "status": "uploaded",
      "url": "https://cdn.elasticfunnels.io/42/assets/media/guarantee-badge.png",
      "size": 34567,
      "mime_type": "image/png",
      "width": 400,
      "height": 400
    },
    {
      "filename": "trust-seal.svg",
      "status": "uploaded",
      "url": "https://cdn.elasticfunnels.io/42/assets/media/trust-seal.svg",
      "size": 1204,
      "mime_type": "image/svg+xml",
      "width": null,
      "height": null
    },
    {
      "filename": "virus.exe",
      "status": "failed",
      "error": "MIME type \"application/x-msdownload\" is not allowed"
    }
  ]
}
HTTP status is always 200 even when some files fail — inspect the status field on each entry. A failed entry never prevents other files from uploading.

Check File Exists

Check whether a file already exists at a given path. Useful for idempotency before uploading.
POST /api/brands/{brand}/file-manager/file-exists
brand
string
required
The brand/project ID
filename
string
required
The filename only (e.g. cards.png)
path
string
The subdirectory to look in (e.g. media). Defaults to the root assets folder.
cURL
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/file-exists \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"filename": "cards.png", "path": "media"}'
{
  "exists": true
}

List Files

Browse the raw Bunny CDN storage directory for the brand. Returns files and folders from storage directly (not the database). Sorted newest-first; folders are listed before files.
GET /api/brands/{brand}/file-manager/files
brand
string
required
The brand/project ID
extensions
string
Comma-separated list of extensions to filter by (e.g. png,jpg,svg)
cURL
curl "https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/files" \
  -H "EF-Access-Key: your_api_key_here"
cURL (filter by extension)
curl "https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/files?extensions=png,jpg,svg" \
  -H "EF-Access-Key: your_api_key_here"
[
  {
    "ObjectName": "media",
    "IsDirectory": true,
    "type": "folder",
    "BrowsePath": "media/",
    "Length": "0 KB",
    "sizeBytes": 0,
    "LastChanged": "2024-12-10T14:00:00Z"
  },
  {
    "ObjectName": "cards.png",
    "IsDirectory": false,
    "type": "file",
    "BrowsePath": "media/cards.png",
    "src": "https://cdn.elasticfunnels.io/42/assets/media/cards.png",
    "Length": "33.76 KB",
    "sizeBytes": 34567,
    "width": 400,
    "height": 400,
    "srcset": "https://cdn.elasticfunnels.io/42/assets/media/cards.png?width=200 200w, ...",
    "sizes": "(max-width: 640px) 100vw, ...",
    "LastChanged": "2024-12-10T14:01:00Z"
  }
]

List Assets

Return all brand assets tracked in the database. Unlike List Files which reads from CDN storage, this returns structured records including file IDs and paths — useful for linking uploaded files to other API operations.
GET /api/brands/{brand}/file-manager/assets
brand
string
required
The brand/project ID
updated_after
string
ISO 8601 timestamp — only return assets updated after this date (e.g. 2024-12-01T00:00:00Z). Useful for incremental sync.
cURL
curl "https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/assets" \
  -H "EF-Access-Key: your_api_key_here"
cURL (incremental sync)
curl "https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/assets?updated_after=2024-12-01T00:00:00Z" \
  -H "EF-Access-Key: your_api_key_here"
[
  {
    "id": 101,
    "file_name": "cards.png",
    "file_path": "media/cards.png",
    "updated_at": "2024-12-10T14:01:00+00:00"
  },
  {
    "id": 102,
    "file_name": "guarantee-badge.png",
    "file_path": "media/guarantee-badge.png",
    "updated_at": "2024-12-10T14:02:00+00:00"
  }
]

Delete File

Delete a file or folder from asset storage. The corresponding database record is also removed.
POST /api/brands/{brand}/file-manager/delete-file
brand
string
required
The brand/project ID
path
string
required
Path of the file to delete, relative to the brand’s assets folder (e.g. media/cards.png). No leading slash.
Deletion is permanent. The following paths are protected and cannot be deleted via API:
  • media (the folder itself)
  • products and anything inside it (used in product forms)
  • Empty path (root assets folder)
cURL
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/delete-file \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"path": "media/old-badge.png"}'
{
  "HttpCode": 200,
  "Message": "File Deleted Successfully."
}

Rename File

Rename a file in asset storage. Because Bunny CDN does not support atomic rename, this downloads the original content, uploads it under the new name, and deletes the original. The database record is updated automatically.
POST /api/brands/{brand}/file-manager/rename-file
brand
string
required
The brand/project ID
path
string
required
Current path of the file relative to the brand’s assets folder (e.g. media/old-name.png). No leading slash.
new_name
string
required
New filename only — not a path (e.g. new-name.png). Must not contain / or \.
cURL
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/rename-file \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"path": "media/old-name.png", "new_name": "cards.png"}'
{
  "success": true,
  "old_path": "media/old-name.png",
  "new_path": "media/cards.png",
  "url": "https://cdn.elasticfunnels.io/42/assets/media/cards.png"
}
Rename folder (POST …/file-manager/rename-folder) is not yet supported via the API. It returns 501 Not Implemented. Use the File Manager UI to rename folders.

Create Folder

Create a new folder inside the brand’s asset storage.
POST /api/brands/{brand}/file-manager/create-folder
brand
string
required
The brand/project ID
path
string
required
Parent directory for the new folder (e.g. media)
name
string
required
Folder name. Must match ^[a-z0-9_-]+$ — lowercase letters, numbers, hyphens, underscores only.
cURL
curl -X POST https://app.elasticfunnels.io/api/brands/{brand_id}/file-manager/create-folder \
  -H "EF-Access-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"path": "media", "name": "product-shots"}'
{
  "message": "Folder created successfully."
}

Automated Product Provisioning

Complete example: bulk-upload all universal + product-specific assets for a new product, then idempotently re-run without re-uploading existing files.
Python
import requests
from pathlib import Path

BRAND_ID = 42
API_KEY = "your_api_key_here"
BASE = f"https://app.elasticfunnels.io/api/brands/{BRAND_ID}/file-manager"
CDN = f"https://cdn.elasticfunnels.io/{BRAND_ID}/assets"
HEADERS = {"EF-Access-Key": API_KEY}


def bulk_upload(local_paths: list[Path], folder: str) -> dict[str, str]:
    """Upload a batch; skip files that already exist. Returns {filename: cdn_url}."""

    # Check which already exist
    to_upload = []
    urls = {}
    for p in local_paths:
        r = requests.post(
            f"{BASE}/file-exists",
            headers=HEADERS,
            json={"filename": p.name, "path": folder},
        )
        if r.json().get("exists"):
            print(f"  skip  {p.name} (already on CDN)")
            urls[p.name] = f"{CDN}/{folder}/{p.name}"
        else:
            to_upload.append(p)

    if not to_upload:
        return urls

    # Bulk upload the rest
    files = [("files[]", (p.name, p.open("rb"))) for p in to_upload]
    r = requests.post(
        f"{BASE}/bulk-upload-files",
        headers=HEADERS,
        files=files,
        data={"path": folder},
    )
    r.raise_for_status()

    for entry in r.json()["files"]:
        if entry["status"] == "uploaded":
            print(f"  ✓  {entry['filename']}")
            urls[entry["filename"]] = entry["url"]
        else:
            print(f"  ✗  {entry['filename']}: {entry['error']}")

    return urls


UNIVERSAL = [
    Path("assets/universal/refund-icon.png"),
    Path("assets/universal/guarantee-badge.png"),
    Path("assets/universal/trust-seal.svg"),
    Path("assets/universal/cc-icons.png"),
]

PRODUCT = [
    Path("assets/product/bottle-shot.png"),
    Path("assets/product/logo.png"),
]

print("Uploading universal assets…")
universal_urls = bulk_upload(UNIVERSAL, "media")

print("Uploading product assets…")
product_urls = bulk_upload(PRODUCT, "media/products")

all_urls = {**universal_urls, **product_urls}
print(f"\nDone. {len(all_urls)} assets ready.")