RapidfileAPI
Sign in
  1. Upload API
  2. 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.

  1. Your server calls POST /v1/uploads with a secret key and gets back a file record plus a signed upload URL.
  2. 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.
  3. Rapidfile marks the file ready, serves it at https://cdn.rapidfile.cc/<id>/<name> and sends your webhook.
REST base URLhttps://api.rapidfile.cc/v1
CDNhttps://cdn.rapidfile.cc/<id>/<filename>
Browser SDKhttps://api.rapidfile.cc/sdk.js (ES module, MIT, no dependencies)
FormatJSON request and response bodies, UTF-8. CORS is open (*) on every /v1 endpoint.
Dashboardapi.rapidfile.cc/dashboard: apps, keys, webhooks, files and usage

Quickstart

  1. 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 as RAPIDFILE_SECRET_KEY in your server's environment.

  2. Add an upload endpoint to your server

    It checks that the user may upload, then calls POST /v1/uploads and returns the JSON to the browser.

  3. 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
// 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 });
}
browser: upload with the SDK
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.

header
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/uploads carry 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 401 with {"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

POST/v1/uploadsSecret key

Reserves 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.

FieldTypeDescription
name requiredstringFile name. Used in the CDN URL and as the download name.
size requiredintegerExact 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 requiredstringMIME 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.
customIdstringYour own identifier (for example a database row id). Returned on the file and in webhooks.
metadataobjectAny 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"}}'

Response for a file up to 95 MB:

200 · single PUT
{
  "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):

200 · multipart
{
  "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

PUTupload.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:

browser
const res = await fetch(upload.url, { method: 'PUT', body: file });
const { file: ready } = await res.json(); // ready.status === 'ready'
200
{
  "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

PUTupload.partUrl ({n} = 1…partCount)No key (signed URL)
POSTupload.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:

complete
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:

browser · manual multipart
// 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

PUT/v1/files?name=&acl=Secret key

Uploads 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"

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:

bundlers (Next.js, Vite, SvelteKit…)
curl -o src/lib/rapidfile-sdk.js https://api.rapidfile.cc/sdk.js
uploadFile
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>
});

uploadFile(file, options) → Promise<ApiFile>

FieldTypeDescription
getUpload requiredstring | (file) => PromiseYour 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 }) => voidCalled while bytes are sent. percent goes from 0 to 100.
signalAbortSignalCancel the upload with an AbortController. The promise rejects with an AbortError.
concurrencynumberParts sent in parallel for multipart uploads. Default 4.
retriesnumberRetries 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.

several files, custom getUpload, cancel
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:

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

GET/v1/files?limit=&cursor=Secret key

Newest first. Pass nextCursor from the previous page as cursor until it is null. limit sets the page size.

request
curl "https://api.rapidfile.cc/v1/files?limit=50" -H "Authorization: Bearer $RAPIDFILE_SECRET_KEY"
200
{
  "data": [ { "id": "Xy7Kq2Lm9P", "name": "cat.jpg", "status": "ready", "...": "..." } ],
  "nextCursor": "eyJjIjoxNzkyMzAwMDAwMDAwfQ"
}

Get a file

GET/v1/files/:idSecret key

Returns { "file": ApiFile }. Useful to confirm an upload finished (status: "ready") when you don't use webhooks.

Delete a file

DELETE/v1/files/:idSecret key

Deletes the file from storage and the CDN. Returns { "deleted": true }. If the app has a webhook URL, a file.deleted event follows.

request
curl -X DELETE https://api.rapidfile.cc/v1/files/Xy7Kq2Lm9P -H "Authorization: Bearer $RAPIDFILE_SECRET_KEY"

Signed URLs

POST/v1/files/:id/signed-urlSecret key

Creates a temporary link to a file, the way to share private files. expiresIn is in seconds, at most 7 days (604800).

request
curl https://api.rapidfile.cc/v1/files/Xy7Kq2Lm9P/signed-url \
  -H "Authorization: Bearer $RAPIDFILE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"expiresIn":3600}'
200
{ "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

FieldTypeDescription
idstringUnique file id. Part of the CDN URL.
appIdstringThe app that owns the file.
namestringFile name as uploaded.
sizeintegerSize in bytes.
typestringMIME type.
status"uploading" | "ready"ready once every byte is stored.
acl"public" | "private"Who can read the file without a signed URL.
urlstring | nullhttps://cdn.rapidfile.cc/<id>/<name> for public files, null for private files.
customIdstring | nullYour identifier from the create call.
metadataobject | nullYour JSON from the create call.
createdAtnumberCreation time, Unix milliseconds.

Usage

GET/v1/usageSecret key

Current plan and usage for your account (all apps together), the same numbers as the dashboard:

200
{
  "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:

EventWhen
file.uploadedA file finished uploading and is ready (browser, multipart or server upload).
file.deletedA file was deleted through the API or the dashboard.
what your endpoint receives
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);
});

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.

Endpointhttps://api.rapidfile.cc/mcp
TransportMCP Streamable HTTP, JSON-RPC 2.0, protocol version 2025-06-18
AuthAuthorization: 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.

terminal
claude mcp add --transport http rapidfile-api https://api.rapidfile.cc/mcp --header "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:

POST /mcp · 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

ToolArgumentsReturns
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 Code
claude mcp add --transport http rapidfile https://rapidfile.cc/mcp
ToolArgumentsReturns
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.

401
{ "error": "unauthorized", "message": "Missing or invalid API key." }
StatusTypical causeWhat to do
400Invalid request: missing name, bad size, metadata over 2 KB, body size not matching the declared size.Fix the request. Show message while developing.
401Missing, wrong or revoked key, or an expired upload URL token.Check the key; create a new upload for expired URLs.
403The key may not do this, for example a file that belongs to another app.Use the key of the app that owns the file.
404File or upload not found (or already deleted).Don't retry.
413File bigger than your plan allows or a single request over 95 MB.Use multipart or upgrade the plan.
429Too many requests, or a plan limit (storage, daily requests) is reached.Back off and retry; check GET /v1/usage.
5xxSomething failed on our side.Retry with exponential backoff. The SDK does this.

Limits

FreeStarterGrowthScale
Storage2 GB100 GB500 GB2 TB
CDN requests / day100,000500,0002,000,00010,000,000
Max file size512 MB4 GB16 GB50 GB
Apps31050200
  • Single PUT and 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
// 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
// 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
// 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
// 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);
public/upload.js
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
// 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
<!-- 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:

upload.html
<!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>
app.py
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_code

Security 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 size and type before calling POST /v1/uploads.
  • Use acl: "private" for anything personal (IDs, invoices, medical files) and hand out short signed URLs.
  • Verify the Rapidfile-Signature on every webhook and reject timestamps older than a few minutes.
  • Save file.id with 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.