- Upload API
- Docs
Upload API documentation
Everything you need to accept user uploads in your app: create an upload on your server, send the file from the browser, get a CDN link and a webhook.
Overview
The Rapidfile Upload API stores files that your users upload from your web or mobile app and serves them from a CDN. It follows the direct upload pattern: your server never handles file bytes.
- Your server calls
POST /v1/uploadswith a secret key and gets back a file record plus a signed upload URL. - The browser sends the file to that URL (one
PUT, or parallel parts for files over 95 MB). No API key is needed for this. - Rapidfile marks the file
ready, serves it athttps://cdn.rapidfile.cc/<id>/<name>and sends your webhook.
| REST base URL | https://api.rapidfile.cc/v1 |
|---|---|
| CDN | https://cdn.rapidfile.cc/<id>/<filename> |
| Browser SDK | https://api.rapidfile.cc/sdk.js (ES module, MIT, no dependencies) |
| Format | JSON request and response bodies, UTF-8. CORS is open (*) on every /v1 endpoint. |
| Dashboard | api.rapidfile.cc/dashboard: apps, keys, webhooks, files and usage |
Quickstart
Create an app
Sign in to the dashboard and create an app. Copy the secret key (
rk_live_…) right away; it is shown once. Store it asRAPIDFILE_SECRET_KEYin your server's environment.Add an upload endpoint to your server
It checks that the user may upload, then calls
POST /v1/uploadsand returns the JSON to the browser.Upload from the browser
Call
uploadFile()from the SDK with your endpoint. You get back the file with its CDN URL.
// app/api/upload/route.ts: runs on your server, so the secret key stays private
import { auth } from '@/lib/auth'; // your own session check
export async function POST(req: Request) {
const user = await auth(req);
if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 });
const { name, size, type } = await req.json();
const res = await fetch('https://api.rapidfile.cc/v1/uploads', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RAPIDFILE_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, size, type, metadata: { userId: user.id } }),
});
return Response.json(await res.json(), { status: res.status });
}// server.js: Express creates the upload, the browser sends the bytes
import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/upload', requireUser, async (req, res) => {
const { name, size, type } = req.body;
const r = await fetch('https://api.rapidfile.cc/v1/uploads', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RAPIDFILE_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, size, type, metadata: { userId: req.user.id } }),
});
res.status(r.status).json(await r.json());
});
app.listen(3000);// In the browser: no SDK, no API key
const picked = document.querySelector('input[type=file]').files[0];
// 1. Ask your own server for an upload URL
const { upload } = await fetch('/api/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: picked.name, size: picked.size, type: picked.type }),
}).then((r) => r.json());
// 2. Send the bytes straight to Rapidfile (files up to 95 MB use one PUT)
const res = await fetch(upload.url, { method: 'PUT', body: picked });
const { file } = await res.json();
console.log(file.url); // https://cdn.rapidfile.cc/<id>/<name># 1. Create an upload (server side, with your secret key)
curl https://api.rapidfile.cc/v1/uploads \
-H "Authorization: Bearer $RAPIDFILE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"cat.jpg","size":48213,"type":"image/jpeg"}'
# 2. PUT the bytes to upload.url from the response (no key needed)
curl -T cat.jpg "$UPLOAD_URL"
# Or upload from your server in one request (up to 95 MB)
curl -T cat.jpg -H "Content-Type: image/jpeg" \
-H "Authorization: Bearer $RAPIDFILE_SECRET_KEY" \
"https://api.rapidfile.cc/v1/files?name=cat.jpg"import { uploadFile } from 'https://api.rapidfile.cc/sdk.js';
const input = document.querySelector('input[type=file]');
input.addEventListener('change', async () => {
const file = await uploadFile(input.files[0], {
getUpload: '/api/upload', // your endpoint that calls POST /v1/uploads
onProgress: ({ percent }) => console.log(`${percent.toFixed(0)}%`),
});
console.log(file.url); // https://cdn.rapidfile.cc/<id>/<name>
});Authentication
Server requests use a secret key in the Authorization header. Each key belongs to one app; files created with it belong to that app.
Authorization: Bearer rk_live_…- Never put a secret key in browser or mobile code. Anyone could read it and upload to your account. Keep it in server environment variables.
- Upload URLs returned by
POST /v1/uploadscarry their own signed token, are valid for 1 hour, only accept that one file and need no key. They are safe to hand to the browser. - An app can have several keys, so you can rotate without downtime: create a new key in the dashboard, deploy it, then revoke the old one.
- A missing, wrong or revoked key returns
401with{"error":"unauthorized"}.
Uploading files
Every browser upload starts on your server with POST /v1/uploads. The response tells the client how to send the bytes: a single PUT for files up to 95 MB, or a MULTIPART upload for anything larger. The SDK handles both.
Create an upload
/v1/uploadsSecret keyReserves a file and returns a signed upload URL. Validate the user, the file type and the size on your server before calling it: your endpoint is where you decide what is allowed.
| Field | Type | Description |
|---|---|---|
name required | string | File name. Used in the CDN URL and as the download name. |
size required | integer | Exact size in bytes. The upload must match it. Must not exceed your plan's file size limit (Free 512 MB, Starter 4 GB, Growth 16 GB, Scale 50 GB). |
type required | string | MIME type, for example image/png. Served as the file's Content-Type. |
acl | "public" | "private" | Default public. Private files have url: null; create signed URLs to share them. |
customId | string | Your own identifier (for example a database row id). Returned on the file and in webhooks. |
metadata | object | Any JSON object up to 2 KB. Returned on the file and in webhooks; handy for user ids. |
curl https://api.rapidfile.cc/v1/uploads \
-H "Authorization: Bearer $RAPIDFILE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"cat.jpg","size":48213,"type":"image/jpeg","metadata":{"userId":"42"}}'const res = await fetch('https://api.rapidfile.cc/v1/uploads', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.RAPIDFILE_SECRET_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'cat.jpg', size: 48213, type: 'image/jpeg', metadata: { userId: '42' } }),
});
const { file, upload } = await res.json();import os, requests
r = requests.post(
"https://api.rapidfile.cc/v1/uploads",
headers={"Authorization": f"Bearer {os.environ['RAPIDFILE_SECRET_KEY']}"},
json={"name": "cat.jpg", "size": 48213, "type": "image/jpeg", "metadata": {"userId": "42"}},
)
data = r.json() # {"file": {...}, "upload": {...}}Response for a file up to 95 MB:
{
"file": {
"id": "Xy7Kq2Lm9P",
"appId": "app_9fK2mQ",
"name": "cat.jpg",
"size": 48213,
"type": "image/jpeg",
"status": "uploading",
"acl": "public",
"url": "https://cdn.rapidfile.cc/Xy7Kq2Lm9P/cat.jpg",
"customId": null,
"metadata": { "userId": "42" },
"createdAt": 1792300000000
},
"upload": {
"method": "PUT",
"url": "https://api.rapidfile.cc/v1/upload/<signed-token>",
"expiresAt": 1792303600000
}
}Response for a larger file. Upload partCount parts of exactly partSize bytes (the last part is the remainder):
{
"file": { "id": "Bq4Wn8Rt2Z", "name": "talk.mp4", "size": 2147483648, "status": "uploading", "...": "..." },
"upload": {
"method": "MULTIPART",
"partSize": 67108864,
"partCount": 32,
"partUrl": "https://api.rapidfile.cc/v1/upload/<signed-token>/{n}",
"completeUrl": "https://api.rapidfile.cc/v1/upload/<signed-token>/complete",
"expiresAt": 1792303600000
}
}The file keeps status: "uploading" until the last byte arrives. The returned url works once the file is ready. Upload URLs expire after 1 hour (expiresAt); create a new upload if one runs out.
Single PUT upload
upload.urlNo key (signed URL)Send the raw file as the request body. Content-Length must equal the size you declared (browsers and curl set it for you). The response arrives after the last byte:
const res = await fetch(upload.url, { method: 'PUT', body: file });
const { file: ready } = await res.json(); // ready.status === 'ready'{
"file": {
"id": "Xy7Kq2Lm9P",
"appId": "app_9fK2mQ",
"name": "cat.jpg",
"size": 48213,
"type": "image/jpeg",
"status": "ready",
"acl": "public",
"url": "https://cdn.rapidfile.cc/Xy7Kq2Lm9P/cat.jpg",
"customId": "avatar-42",
"metadata": { "userId": "42" },
"createdAt": 1792300000000
}
}Use XMLHttpRequest (as the SDK does) if you need upload progress events; fetch doesn't report upload progress in every browser.
Multipart upload
upload.partUrl ({n} = 1…partCount)No key (signed URL)upload.completeUrlNo key (signed URL)For files over 95 MB, cut the file into partCount slices of partSize bytes. PUT slice n (starting at 1) to partUrl with {n} replaced by the part number. Each part responds with its etag. Parts can go in parallel and in any order; retry a failed part by sending it again. When every part is in, POST the list to completeUrl:
POST upload.completeUrl
Content-Type: application/json
{ "parts": [ { "partNumber": 1, "etag": "…" }, { "partNumber": 2, "etag": "…" } ] }The response is { "file": ApiFile } with status: "ready". Without the SDK it looks like this:
// Without the SDK: upload a MULTIPART response yourself
const { upload } = created; // the JSON from POST /v1/uploads
const parts = [];
for (let n = 1; n <= upload.partCount; n++) {
const start = (n - 1) * upload.partSize;
const chunk = picked.slice(start, Math.min(picked.size, start + upload.partSize));
const res = await fetch(upload.partUrl.replace('{n}', n), { method: 'PUT', body: chunk });
const { etag } = await res.json();
parts.push({ partNumber: n, etag });
}
const done = await fetch(upload.completeUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ parts }),
});
const { file } = await done.json(); // status: "ready"Server-side upload
/v1/files?name=&acl=Secret keyUploads a file from your own server in one request, for example a generated report or an image you fetched. The raw body is the file (up to 95 MB); send its MIME type as Content-Type. acl is public (default) or private. Returns { "file": ApiFile }.
curl -T report.pdf -H "Content-Type: application/pdf" \
-H "Authorization: Bearer $RAPIDFILE_SECRET_KEY" \
"https://api.rapidfile.cc/v1/files?name=report.pdf&acl=private"import { readFile } from 'node:fs/promises';
const res = await fetch('https://api.rapidfile.cc/v1/files?name=report.pdf&acl=private', {
method: 'PUT',
headers: { Authorization: `Bearer ${process.env.RAPIDFILE_SECRET_KEY}`, 'Content-Type': 'application/pdf' },
body: await readFile('report.pdf'),
});
const { file } = await res.json();import os, requests
with open("report.pdf", "rb") as f:
r = requests.put(
"https://api.rapidfile.cc/v1/files",
params={"name": "report.pdf", "acl": "private"},
headers={"Authorization": f"Bearer {os.environ['RAPIDFILE_SECRET_KEY']}", "Content-Type": "application/pdf"},
data=f,
)
file = r.json()["file"]Browser SDK
https://api.rapidfile.cc/sdk.js is a small ES module with no dependencies (MIT). Import it straight from the URL in plain HTML, or save a copy into your project when you use a bundler:
curl -o src/lib/rapidfile-sdk.js https://api.rapidfile.cc/sdk.jsimport { uploadFile } from 'https://api.rapidfile.cc/sdk.js';
const input = document.querySelector('input[type=file]');
input.addEventListener('change', async () => {
const file = await uploadFile(input.files[0], {
getUpload: '/api/upload', // your endpoint that calls POST /v1/uploads
onProgress: ({ percent }) => console.log(`${percent.toFixed(0)}%`),
});
console.log(file.url); // https://cdn.rapidfile.cc/<id>/<name>
});uploadFile(file, options) → Promise<ApiFile>
| Field | Type | Description |
|---|---|---|
getUpload required | string | (file) => Promise | Your endpoint URL (the SDK POSTs {"name","size","type"} as JSON with cookies) or a function that returns the JSON of POST /v1/uploads. |
onProgress | ({ loaded, total, percent }) => void | Called while bytes are sent. percent goes from 0 to 100. |
signal | AbortSignal | Cancel the upload with an AbortController. The promise rejects with an AbortError. |
concurrency | number | Parts sent in parallel for multipart uploads. Default 4. |
retries | number | Retries per request for network errors, 429 and 5xx, with exponential backoff. Default 3. |
uploadFiles(files, options) → Promise<ApiFile[]>
Uploads a FileList or array. Same options, plus parallelFiles (default 2) and onFileComplete(apiFile, index). onProgress reports all files together.
const controller = new AbortController();
cancelButton.onclick = () => controller.abort();
try {
const files = await uploadFiles(input.files, {
getUpload: (f) => fetch('/api/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf },
body: JSON.stringify({ name: f.name, size: f.size, type: f.type }),
}).then((r) => r.json()),
onFileComplete: (file, i) => console.log('done', i, file.url),
signal: controller.signal,
});
} catch (err) {
if (err.name === 'AbortError') console.log('cancelled');
else console.error(err.status, err.code, err.message); // UploadError
}Errors are UploadError instances with status (HTTP status, 0 for network errors), code and message. For TypeScript, put this next to your copy of the SDK as rapidfile-sdk.d.ts:
export type ApiFile = {
id: string; appId: string; name: string; size: number; type: string;
status: 'uploading' | 'ready'; acl: 'public' | 'private'; url: string | null;
customId: string | null; metadata: Record<string, unknown> | null; createdAt: number;
};
export type Progress = { loaded: number; total: number; percent: number };
export type UploadOptions = {
getUpload: string | ((file: File) => Promise<unknown>);
onProgress?: (p: Progress) => void;
signal?: AbortSignal;
concurrency?: number;
retries?: number;
};
export function uploadFile(file: File, opts: UploadOptions): Promise<ApiFile>;
export function uploadFiles(
files: FileList | File[],
opts: UploadOptions & { parallelFiles?: number; onFileComplete?: (file: ApiFile, index: number) => void },
): Promise<ApiFile[]>;
export class UploadError extends Error { status: number; code: string }Files
Manage the files of the app that owns the key. All file endpoints need the secret key.
List files
/v1/files?limit=&cursor=Secret keyNewest first. Pass nextCursor from the previous page as cursor until it is null. limit sets the page size.
curl "https://api.rapidfile.cc/v1/files?limit=50" -H "Authorization: Bearer $RAPIDFILE_SECRET_KEY"{
"data": [ { "id": "Xy7Kq2Lm9P", "name": "cat.jpg", "status": "ready", "...": "..." } ],
"nextCursor": "eyJjIjoxNzkyMzAwMDAwMDAwfQ"
}Get a file
/v1/files/:idSecret keyReturns { "file": ApiFile }. Useful to confirm an upload finished (status: "ready") when you don't use webhooks.
Delete a file
/v1/files/:idSecret keyDeletes the file from storage and the CDN. Returns { "deleted": true }. If the app has a webhook URL, a file.deleted event follows.
curl -X DELETE https://api.rapidfile.cc/v1/files/Xy7Kq2Lm9P -H "Authorization: Bearer $RAPIDFILE_SECRET_KEY"Signed URLs
/v1/files/:id/signed-urlSecret keyCreates a temporary link to a file, the way to share private files. expiresIn is in seconds, at most 7 days (604800).
curl https://api.rapidfile.cc/v1/files/Xy7Kq2Lm9P/signed-url \
-H "Authorization: Bearer $RAPIDFILE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"expiresIn":3600}'{ "url": "https://cdn.rapidfile.cc/Xy7Kq2Lm9P/contract.pdf?…signature…", "expiresAt": 1792303600000 }Create signed URLs on demand (for example when a page renders) instead of storing them; they stop working at expiresAt.
The ApiFile object
| Field | Type | Description |
|---|---|---|
id | string | Unique file id. Part of the CDN URL. |
appId | string | The app that owns the file. |
name | string | File name as uploaded. |
size | integer | Size in bytes. |
type | string | MIME type. |
status | "uploading" | "ready" | ready once every byte is stored. |
acl | "public" | "private" | Who can read the file without a signed URL. |
url | string | null | https://cdn.rapidfile.cc/<id>/<name> for public files, null for private files. |
customId | string | null | Your identifier from the create call. |
metadata | object | null | Your JSON from the create call. |
createdAt | number | Creation time, Unix milliseconds. |
Usage
/v1/usageSecret keyCurrent plan and usage for your account (all apps together), the same numbers as the dashboard:
{
"plan": "free",
"storageBytes": 734003200,
"storageLimit": 2147483648,
"requestsToday": 1840,
"requestsLimit": 100000,
"maxFileBytes": 536870912,
"apps": 1,
"appLimit": 3
}Webhooks
Set a webhook URL per app in the dashboard. Rapidfile then POSTs JSON to it:
| Event | When |
|---|---|
file.uploaded | A file finished uploading and is ready (browser, multipart or server upload). |
file.deleted | A file was deleted through the API or the dashboard. |
POST /webhooks/rapidfile
Content-Type: application/json
Rapidfile-Signature: t=1792300042,v1=5f2b8c...e91a
{
"type": "file.uploaded",
"data": {
"id": "Xy7Kq2Lm9P",
"appId": "app_9fK2mQ",
"name": "cat.jpg",
"size": 48213,
"type": "image/jpeg",
"status": "ready",
"acl": "public",
"url": "https://cdn.rapidfile.cc/Xy7Kq2Lm9P/cat.jpg",
"customId": "avatar-42",
"metadata": { "userId": "42" },
"createdAt": 1792300000000
},
"createdAt": 1792300042000
}Respond with any 2xx status quickly and do slow work afterwards. Make handlers idempotent (key on data.id) and, if a webhook might be missed, confirm with GET /v1/files/:id.
Verifying signatures
Every webhook has a Rapidfile-Signature header: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of "<t>.<raw body>" keyed with the app's webhook secret (whsec_…, shown once when you create or rotate it). Compute it over the raw body bytes, compare in constant time and reject old timestamps to stop replays.
// Node 18+: verify the Rapidfile-Signature header before trusting a webhook
import crypto from 'node:crypto';
export function verifyRapidfileSignature(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries((header || '').split(',').map((p) => {
const i = p.indexOf('=');
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
}));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest();
const given = Buffer.from(parts.v1 || '', 'hex');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}
// Express: keep the raw body for the signature check
app.post('/webhooks/rapidfile', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifyRapidfileSignature(req.body, req.get('Rapidfile-Signature'), process.env.RAPIDFILE_WEBHOOK_SECRET);
if (!ok) return res.sendStatus(400);
const event = JSON.parse(req.body.toString('utf8'));
if (event.type === 'file.uploaded') {
// event.data is the ApiFile: save event.data.id and event.data.url with your record
}
res.sendStatus(200);
});# Python 3: verify the Rapidfile-Signature header (Flask example)
import hashlib, hmac, json, os, time
from flask import Flask, request, abort
def verify_rapidfile_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p)
try:
t = int(parts.get("t", "0"))
except ValueError:
return False
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
app = Flask(__name__)
@app.post("/webhooks/rapidfile")
def rapidfile_webhook():
raw = request.get_data()
if not verify_rapidfile_signature(raw, request.headers.get("Rapidfile-Signature"), os.environ["RAPIDFILE_WEBHOOK_SECRET"]):
abort(400)
event = json.loads(raw)
if event["type"] == "file.uploaded":
pass # event["data"] is the ApiFile
return "", 200// app/api/webhooks/rapidfile/route.ts
import { verifyRapidfileSignature } from '@/lib/rapidfile-webhook'; // the Node function above
export async function POST(req: Request) {
const raw = Buffer.from(await req.arrayBuffer());
const ok = verifyRapidfileSignature(raw, req.headers.get('rapidfile-signature'), process.env.RAPIDFILE_WEBHOOK_SECRET!);
if (!ok) return new Response('bad signature', { status: 400 });
const event = JSON.parse(raw.toString('utf8'));
// event.type: 'file.uploaded' | 'file.deleted', event.data: ApiFile
return new Response('ok');
}MCP for AI agents
The Upload API is also a remote Model Context Protocol server, so AI agents (Claude Code, Cursor, VS Code and other MCP clients) can store files and get CDN links without you writing glue code.
| Endpoint | https://api.rapidfile.cc/mcp |
|---|---|
| Transport | MCP Streamable HTTP, JSON-RPC 2.0, protocol version 2025-06-18 |
| Auth | Authorization: Bearer rk_live_…, a secret key of one app. The agent works with that app's files. |
An agent connected with a key can do what the key can: upload, read and delete the app's files. Give agents their own key (or their own app) so you can revoke it on its own, and keep keys out of config files you commit.
Connect a client
Create an app and a key in the dashboard, then add the server to your client. Replace rk_live_… with your key.
claude mcp add --transport http rapidfile-api https://api.rapidfile.cc/mcp --header "Authorization: Bearer rk_live_…"{
"mcpServers": {
"rapidfile": {
"type": "http",
"url": "https://api.rapidfile.cc/mcp",
"headers": { "Authorization": "Bearer rk_live_…" }
}
}
}{
"mcpServers": {
"rapidfile": {
"url": "https://api.rapidfile.cc/mcp",
"headers": { "Authorization": "Bearer rk_live_…" }
}
}
}{
"servers": {
"rapidfile": {
"type": "http",
"url": "https://api.rapidfile.cc/mcp",
"headers": { "Authorization": "Bearer rk_live_…" }
}
}
}Then ask the agent something like "Upload notes.md to Rapidfile and give me the link". Under the hood the client sends a JSON-RPC tools/call:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "upload_file",
"arguments": { "name": "notes.md", "content": "# Meeting notes\n…", "type": "text/markdown" }
}
}Tools
| Tool | Arguments | Returns |
|---|---|---|
upload_file | { name, content?, contentBase64?, url?, type?, acl?, customId?, metadata? } | The stored file (ApiFile) with its CDN url. Send one source: content (UTF-8 text), contentBase64 (binary) or url (an https URL Rapidfile fetches, up to 95 MB). acl is "public" (default) or "private". |
create_upload_url | { name, size, type?, acl?, customId?, metadata? } | Signed upload instructions for a browser or end user, the same upload object as POST /v1/uploads (single PUT or multipart). No key is needed to use them. |
list_files | { limit?, cursor? } | A page of the app's files, newest first, and the cursor for the next page. |
get_file | { id } | One file (ApiFile), including status and url. |
delete_file | { id } | Deletes the file from storage and the CDN. Sends file.deleted if the app has a webhook. |
get_signed_url | { id, expiresIn? } | A temporary link to a file (the way to share private files). expiresIn in seconds, up to 7 days. |
get_usage | {} | Plan, storage, CDN requests today and limits, like GET /v1/usage. |
Files uploaded by an agent are ordinary API files: they appear in the dashboard, count towards your plan and trigger file.uploaded webhooks.
Transfers MCP (no key)
To send a file to a person rather than store it in your app, use the Rapidfile transfers server at https://rapidfile.cc/mcp. It needs no key. To send with your account's limits (2 GB and more per transfer instead of the anonymous 500 MB), add Authorization: Bearer rf_cli_…, the token that rapidfile login saves.
claude mcp add --transport http rapidfile https://rapidfile.cc/mcp| Tool | Arguments | Returns |
|---|---|---|
send_file | { name, content? | contentBase64? | url?, expires?, downloads?, password? } | A transfer link and its 6-digit code. expires is "1h", "1d", "3d" or "7d". |
get_transfer | { code or link } | The transfer's details, such as its files and when it expires. |
read_transfer | { code or link, maxBytes? } | The text content of small text files in a transfer, so an agent can read what was sent to it. |
delete_transfer | { id, ownerToken } | Deletes a transfer you sent (the owner token comes from send_file). |
whoami | {} | Whether a rf_cli_… token is in use, and for which account. |
Errors
Errors use HTTP status codes and a JSON body with a machine-readable error code and a human message. Branch on the status; show message in logs or to developers. New error codes may be added over time.
{ "error": "unauthorized", "message": "Missing or invalid API key." }| Status | Typical cause | What to do |
|---|---|---|
400 | Invalid request: missing name, bad size, metadata over 2 KB, body size not matching the declared size. | Fix the request. Show message while developing. |
401 | Missing, wrong or revoked key, or an expired upload URL token. | Check the key; create a new upload for expired URLs. |
403 | The key may not do this, for example a file that belongs to another app. | Use the key of the app that owns the file. |
404 | File or upload not found (or already deleted). | Don't retry. |
413 | File bigger than your plan allows or a single request over 95 MB. | Use multipart or upgrade the plan. |
429 | Too many requests, or a plan limit (storage, daily requests) is reached. | Back off and retry; check GET /v1/usage. |
5xx | Something failed on our side. | Retry with exponential backoff. The SDK does this. |
Limits
| Free | Starter | Growth | Scale | |
|---|---|---|---|---|
| Storage | 2 GB | 100 GB | 500 GB | 2 TB |
| CDN requests / day | 100,000 | 500,000 | 2,000,000 | 10,000,000 |
| Max file size | 512 MB | 4 GB | 16 GB | 50 GB |
| Apps | 3 | 10 | 50 | 200 |
- Single
PUTand server-side uploads: up to 95 MB per request. Larger files use multipart. - Upload URLs are valid for 1 hour. Signed download URLs for up to 7 days.
metadata: a JSON object up to 2 KB.- CDN requests count each delivery of one of your files from
cdn.rapidfile.cc; the counter resets daily. Bandwidth itself is not billed. - API calls are rate limited per account to keep the service fast for everyone. On
429, wait and retry with backoff.
Need more? Compare plans or write to admin@enzonic.com.
Framework guides
Copy-paste setups. Each one has a server endpoint that creates the upload with your secret key and a client that sends the file with the SDK.
Next.js (App Router)
Put the key in .env.local as RAPIDFILE_SECRET_KEY and save the SDK as lib/rapidfile-sdk.js. More detail in the Next.js file upload guide.
// app/api/upload/route.ts: runs on your server, so the secret key stays private
import { auth } from '@/lib/auth'; // your own session check
export async function POST(req: Request) {
const user = await auth(req);
if (!user) return Response.json({ error: 'unauthorized' }, { status: 401 });
const { name, size, type } = await req.json();
const res = await fetch('https://api.rapidfile.cc/v1/uploads', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RAPIDFILE_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, size, type, metadata: { userId: user.id } }),
});
return Response.json(await res.json(), { status: res.status });
}// app/upload/page.tsx
'use client';
import { useState } from 'react';
import { uploadFile } from '@/lib/rapidfile-sdk'; // copy of https://api.rapidfile.cc/sdk.js
export default function UploadPage() {
const [percent, setPercent] = useState(0);
const [url, setUrl] = useState('');
return (
<div>
<input
type="file"
onChange={async (e) => {
const picked = e.target.files?.[0];
if (!picked) return;
const file = await uploadFile(picked, {
getUpload: '/api/upload',
onProgress: (p) => setPercent(Math.round(p.percent)),
});
setUrl(file.url);
}}
/>
<progress max={100} value={percent} />
{url && <a href={url}>{url}</a>}
</div>
);
}// app/api/webhooks/rapidfile/route.ts
import { verifyRapidfileSignature } from '@/lib/rapidfile-webhook'; // the Node function above
export async function POST(req: Request) {
const raw = Buffer.from(await req.arrayBuffer());
const ok = verifyRapidfileSignature(raw, req.headers.get('rapidfile-signature'), process.env.RAPIDFILE_WEBHOOK_SECRET!);
if (!ok) return new Response('bad signature', { status: 400 });
const event = JSON.parse(raw.toString('utf8'));
// event.type: 'file.uploaded' | 'file.deleted', event.data: ApiFile
return new Response('ok');
}Express
Any Node server works the same way. Serve a page that imports the SDK from https://api.rapidfile.cc/sdk.js.
// server.js: Express creates the upload, the browser sends the bytes
import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/upload', requireUser, async (req, res) => {
const { name, size, type } = req.body;
const r = await fetch('https://api.rapidfile.cc/v1/uploads', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.RAPIDFILE_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, size, type, metadata: { userId: req.user.id } }),
});
res.status(r.status).json(await r.json());
});
app.listen(3000);import { uploadFile } from 'https://api.rapidfile.cc/sdk.js';
const input = document.querySelector('input[type=file]');
input.addEventListener('change', async () => {
const file = await uploadFile(input.files[0], {
getUpload: '/api/upload', // your endpoint that calls POST /v1/uploads
onProgress: ({ percent }) => console.log(`${percent.toFixed(0)}%`),
});
console.log(file.url); // https://cdn.rapidfile.cc/<id>/<name>
});SvelteKit
Add RAPIDFILE_SECRET_KEY to .env and save the SDK as src/lib/rapidfile-sdk.js.
// src/routes/api/upload/+server.ts
import { json, error } from '@sveltejs/kit';
import { RAPIDFILE_SECRET_KEY } from '$env/static/private';
export async function POST({ request, locals }) {
if (!locals.user) error(401, 'Sign in first');
const { name, size, type } = await request.json();
const res = await fetch('https://api.rapidfile.cc/v1/uploads', {
method: 'POST',
headers: { Authorization: `Bearer ${RAPIDFILE_SECRET_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ name, size, type, metadata: { userId: locals.user.id } }),
});
return json(await res.json(), { status: res.status });
}<!-- src/routes/upload/+page.svelte -->
<script>
import { uploadFile } from '$lib/rapidfile-sdk.js'; // copy of https://api.rapidfile.cc/sdk.js
let percent = 0;
let url = '';
async function onChange(e) {
const file = await uploadFile(e.currentTarget.files[0], {
getUpload: '/api/upload',
onProgress: (p) => (percent = Math.round(p.percent)),
});
url = file.url;
}
</script>
<input type="file" on:change={onChange} />
<progress max="100" value={percent}></progress>
{#if url}<a href={url}>{url}</a>{/if}Plain HTML
No build step: the page imports the SDK from our URL. The backend can be anything that calls POST /v1/uploads, here in Python:
<!doctype html>
<input type="file" id="pick" multiple>
<progress id="bar" max="100" value="0"></progress>
<ul id="out"></ul>
<script type="module">
import { uploadFiles } from 'https://api.rapidfile.cc/sdk.js';
document.getElementById('pick').addEventListener('change', async (e) => {
const files = await uploadFiles(e.target.files, {
getUpload: '/api/upload', // any backend that calls POST /v1/uploads
onProgress: ({ percent }) => (document.getElementById('bar').value = percent),
});
for (const f of files) {
const li = document.createElement('li');
li.textContent = f.url;
document.getElementById('out').append(li);
}
});
</script>import os, requests
from flask import Flask, request
app = Flask(__name__)
@app.post("/api/upload")
def create_upload():
body = request.get_json()
if body["size"] > 50 * 1024 * 1024:
return {"error": "too_large", "message": "Max 50 MB"}, 413
r = requests.post(
"https://api.rapidfile.cc/v1/uploads",
headers={"Authorization": f"Bearer {os.environ['RAPIDFILE_SECRET_KEY']}"},
json={"name": body["name"], "size": body["size"], "type": body["type"]},
)
return r.json(), r.status_codeSecurity checklist
- Secret keys only on the server, in environment variables. Rotate by adding a key, deploying, then revoking the old one.
- Authenticate the user in your upload endpoint and check
sizeandtypebefore callingPOST /v1/uploads. - Use
acl: "private"for anything personal (IDs, invoices, medical files) and hand out short signed URLs. - Verify the
Rapidfile-Signatureon every webhook and reject timestamps older than a few minutes. - Save
file.idwith your record so you can delete the file when the user deletes their content. - Store the
whsec_…webhook secret like a password; rotate it in the dashboard if it leaks.
Last updated 2026-10-18. Questions or something unclear? Email admin@enzonic.com.