MarvX
/Storage API

Object storage

MarvX gives each workspace S3-compatible buckets for user uploads, media and backups. You manage buckets and files through the MarvX REST API, and file bytes move over short-lived presigned URLs, so they never pass through your server or ours.

API base

<your MarvX API URL>/v1

Auth

Bearer API token (mv_…)

URL lifetime

15 minutes

Server requirement. File endpoints work once the MarvX server is configured with S3 credentials (S3_ACCESS_KEY, S3_SECRET_KEY, S3_REGION, S3_BUCKET). Until then they return a "not configured" error. There are no per-bucket access keys: use presigned URLs from the API.

Quick start

1. Create an API token

Go to Dashboard → Settings → API Tokens and create a token. It's shown once, so store it as a secret. Never commit it to git.

MARVX_API_URL=https://your-marvx-api.example.com
MARVX_TOKEN=mv_xxxxxxxxxxxxxxxxxxxx
MARVX_ORG_ID=your-workspace-id

2. Set up a client

// Node.js 18+ (built-in fetch)
const API    = `${process.env.MARVX_API_URL}/v1`;
const ORG_ID = process.env.MARVX_ORG_ID;
const headers = {
  Authorization:  `Bearer ${process.env.MARVX_TOKEN}`,
  "Content-Type": "application/json",
};

3. Find your workspace ID

Every storage call takes an org_id. List your workspaces to get it.

const orgs = await fetch(`${API}/orgs`, { headers }).then((r) => r.json());
console.log(orgs.map((o) => [o.id, o.name]));

4. Create a bucket and upload a file

See Buckets and Upload files below.

Authentication

Send your API token as a Bearer token on every request to the MarvX API. Presigned URLs carry their own signature, so requests to them need no Authorization header.

Authorization: Bearer mv_xxxxxxxxxxxxxxxxxxxx

Tokens act as the user who created them, so you can only reach buckets in workspaces you belong to. Revoke a token from the same settings page at any time.

Buckets

Create as many buckets as you need in a workspace. Files are stored under that workspace and bucket, isolated from other workspaces.

org_idrequiredstringWorkspace ID
namerequiredstringBucket name, e.g. uploads
const bucket = await fetch(`${API}/storage/buckets`, {
  method:  "POST",
  headers,
  body:    JSON.stringify({ org_id: ORG_ID, name: "uploads" }),
}).then((r) => r.json());

console.log(bucket.id); // use this ID in the file endpoints

GET /v1/storage/buckets?org_id=… lists buckets and DELETE /v1/storage/buckets/{id} removes one.

Upload files

Uploading is two steps: request a presigned upload URL from MarvX, then PUT the file bytes to that URL with the same Content-Type.

POST /v1/storage/buckets/{id}/upload-url

org_idrequiredstringWorkspace ID
keyrequiredstringFile path inside the bucket, e.g. uploads/photo.jpg
content_typestringMIME type. Send the same value as Content-Type on the PUT.
import { readFile } from "node:fs/promises";

const BUCKET_ID = "your-bucket-id";

// 1. Ask MarvX for a presigned upload URL
const { upload_url } = await fetch(
  `${API}/storage/buckets/${BUCKET_ID}/upload-url`,
  {
    method: "POST",
    headers,
    body: JSON.stringify({
      org_id:       ORG_ID,
      key:          "uploads/photo.jpg",
      content_type: "image/jpeg",
    }),
  },
).then((r) => r.json());

// 2. PUT the file straight to storage (same Content-Type as above)
await fetch(upload_url, {
  method:  "PUT",
  headers: { "Content-Type": "image/jpeg" },
  body:    await readFile("./photo.jpg"),
});

Download files

Request a presigned download URL, then GET it. You can also hand the URL to a browser or another service. It works without credentials until it expires.

POST /v1/storage/buckets/{id}/download-url

org_idrequiredstringWorkspace ID
keyrequiredstringFile path inside the bucket
import { writeFile } from "node:fs/promises";

const { download_url } = await fetch(
  `${API}/storage/buckets/${BUCKET_ID}/download-url`,
  {
    method: "POST",
    headers,
    body: JSON.stringify({ org_id: ORG_ID, key: "uploads/photo.jpg" }),
  },
).then((r) => r.json());

const file = await fetch(download_url);
await writeFile("photo.jpg", Buffer.from(await file.arrayBuffer()));

List files

Returns every file in the bucket with its size and last-modified time, plus the bucket's total size.

GET /v1/storage/buckets/{id}/files?org_id=…

const { files, total_bytes } = await fetch(
  `${API}/storage/buckets/${BUCKET_ID}/files?org_id=${ORG_ID}`,
  { headers },
).then((r) => r.json());

files.forEach((f) => console.log(f.key, f.size_bytes, f.last_modified));
console.log("Total:", total_bytes, "bytes");

Delete files

Permanently remove a file from a bucket. This cannot be undone.

DELETE /v1/storage/buckets/{id}/files?org_id=…&key=…

const params = new URLSearchParams({ org_id: ORG_ID, key: "uploads/photo.jpg" });

await fetch(`${API}/storage/buckets/${BUCKET_ID}/files?${params}`, {
  method: "DELETE",
  headers,
});

Presigned URLs

Upload and download URLs are signed for one object and expire after 15 minutes (expires_in_seconds: 900). Request a fresh one each time you need it rather than storing it.

// POST /upload-url
{ "upload_url": "https://…", "key": "uploads/photo.jpg", "expires_in_seconds": 900 }

// POST /download-url
{ "download_url": "https://…", "expires_in_seconds": 900 }

Common patterns

  • Browser uploads: your backend requests an upload URL and the browser PUTs the file directly to it
  • Private downloads: generate a download URL when a signed-in user asks for a file
  • Share a file with another service for a short window without giving it your token

Uploading directly from a browser requires CORS to be allowed on the S3 bucket the MarvX server is configured with.