UploadCenter

Getting started

From an empty account to your first uploaded file, in five steps.

Install the SDK
Official, fully-typed SDKs for TypeScript/JavaScript (Node.js and the browser) and Python. Every step below shows the equivalent cURL request too, if you'd rather call the API directly.
bash
npm install @uploadcenter/sdk-js
typescript
import { createClient } from "@uploadcenter/sdk-js";

const client = createClient({
  baseUrl: "https://api.uploadscenter.com",
  token: process.env.UPLOADCENTER_API_KEY, // an API key from your project settings
});

Every method is generated from UploadCenter's OpenAPI schema, so autocomplete covers the full API surface — view the JS package on npm or the Python package on PyPI.

1
Create an account
A workspace (organization) is created for you automatically on sign-up.
2
Create a project and an API key
From your dashboard: create a project, then generate an API key scoped to it — the full key is shown once, so copy it immediately.
3
Presign an upload
Ask the API for a presigned URL — files never pass through your own backend.
bash
curl -X POST https://api.uploadscenter.com/v1/uploads/presign \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": "project_...",
    "filename": "photo.jpg",
    "size_bytes": 128000,
    "mime_type": "image/jpeg",
    "visibility": "private"
  }'
4
Upload the file
PUT the file directly to the returned upload_url — this is what makes uploads fast at any size.
bash
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg
5
Confirm the upload
This is what triggers antivirus scanning and processing — the file isn't marked ready until it passes.
bash
curl -X POST https://api.uploadscenter.com/v1/uploads/complete \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"file_id": "file_..."}'

Reading a file back — GET /v1/files/{file_id}/url — always requires a valid API key or session for private files. Public files work differently — see below.

Public URLs
Upload with visibility: "public" and, once the file is ready, its url field is a permanent, unauthenticated link — safe to embed directly, cached at the edge, never expires.
json
GET /v1/files/{file_id}

{
  "id": "file_...",
  "visibility": "public",
  "status": "ready",
  "url": "https://cdn.uploadscenter.com/o/…/photo.jpg"
}

Private files (the default) don't get a url field — call GET /v1/files/{id}/url instead, which returns a fresh signed link valid for a limited time.

React to events
Add a webhook from your project settings to get notified on upload, processing, and quota events. Every delivery is signed — verify it before trusting the payload.
http
POST <your webhook URL>
Content-Type: application/json
X-Signature: sha256=<hex-encoded HMAC-SHA256>
X-Timestamp: <unix timestamp, seconds>

{"event":"file.processed","created_at":"2026-01-01T12:00:00Z","data":{"id":"file_...","project_id":"project_...","status":"ready", ...}}

Events: file.uploaded, file.processed, file.failed, file.quarantined, file.flagged, storage.limit_reached. Up to 3 delivery attempts (30s, 2min, 10min backoff) before a delivery is marked dead.

typescript
import crypto from "node:crypto";

// Sign over "${timestamp}.${rawBody}" — rawBody must be the exact bytes
// received, not a re-serialized JSON.stringify(JSON.parse(rawBody)).
function verifyWebhook(rawBody: string, timestamp: string, signatureHeader: string, secret: string): boolean {
  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const provided = signatureHeader.replace(/^sha256=/, "");
  return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(provided, "hex"));
}

// signatureHeader = req.headers["x-signature"]
// timestamp       = req.headers["x-timestamp"]
Transform images on the fly
Resize, crop, and convert format with query parameters — the result is cached after the first request, not recomputed on every hit.
http
GET /v1/files/{file_id}/transform?w=480&h=320&format=auto&fit=cover

# fit: "cover" (crop to fill) or "inside" (contain)
# format: auto (default) | webp | jpeg | png
# quality: auto (default) | 1-100
#
# format=auto picks WebP or JPEG based on the request's Accept header —
# no more branching client-side for older browsers. quality=auto applies
# a sensible per-format default (80 for WebP, 82 for JPEG). Pass explicit
# values any time you want full control instead.
Watermark your images
Overlay your project's logo on any transform — position, opacity, and size are fully configurable, and the result is cached like any other transform.
http
PUT /v1/projects/{project_id}/watermark
{"file_id": "file_..."}   # any image already uploaded to the project

GET /v1/files/{file_id}/transform?w=1200&watermark=true&gravity=south_east&opacity=60&overlay_scale=0.2

# gravity: north_west | north_east | south_west | south_east | center
# opacity: 1-100 (default 60)
# overlay_scale: 0.05-1.0 (default 0.2, fraction of the output image's width)
Custom domains
Serve public files from your own domain instead of the shared CDN, once ownership is verified over DNS.
http
POST /v1/domains
{"project_id": "project_...", "hostname": "cdn.yourapp.com"}

# Add the returned TXT record at your DNS provider, then:
POST /v1/domains/{domain_id}/verify

# Once verified, point cdn.yourapp.com at UploadCenter (A/CNAME — shown in
# the dashboard) and a TLS certificate is issued automatically. From then
# on, every public file's url uses your domain instead of the shared CDN.
Content moderation
Uploaded images are automatically screened for explicit content — detection only, nothing is ever blocked or deleted on your behalf.
json
GET /v1/files/{file_id}

{
  "id": "file_...",
  "status": "ready",
  "moderation_status": "flagged",  // null (not evaluated) | "clean" | "flagged" | "error"
  "moderation_score": 0.87
}

A flagged file stays exactly as uploaded — your app decides what "flagged" should mean for your product. Available on paid plans; see the features page.

Video thumbnails & derived files
Every video upload gets a JPEG thumbnail automatically; smaller videos also get a 720p MP4 transcode. Each variant's own url is ready to use directly — no extra signing step.
json
GET /v1/files/{file_id}/variants

[
  { "variant": "thumbnail", "format": "jpg", "width": 640, "height": 360, "url": "https://…" },
  { "variant": "720p", "format": "mp4", "width": 1280, "height": 720, "url": "https://…" }
]
AI image & video generation
Generate, edit, remove backgrounds, upscale, or create variations of an image; generate video from text or an existing image. Every call is async — 202 with a generation id, then poll it or listen for the ai.generation.completed / ai.generation.failed webhook.
typescript
const generation = await client.ai.generateImageEndpointV1AiImagesGeneratePost({
  project_id: projectId,
  prompt: "a minimalist logo of a fox, flat vector style",
  use_brand_kit: true, // pulls in this org's brand kit colors/fonts
});

// 202 Accepted — poll until it settles (or listen for the webhook below).
const result = await client.ai.getGenerationEndpointV1AiGenerationsGenerationIdGet(
  generation.id,
  projectId,
);
// result.file_ids — the new file(s), same shape as anything you upload

The result is an ordinary file — file_ids on the finished generation — with its own visibility, delivery, and lifecycle, exactly like anything you upload directly.

Reliability, honestly
UploadCenter is a young, actively developed platform — we don't have years of uptime history to point to, and we won't invent a number. What's in place today: every file is antivirus-scanned before it's ever served, uploads are processed atomically (nothing is left half-done if a step fails), and delivery runs on Cloudflare's network. There's no formal SLA yet — if your use case needs contractual guarantees, reach out and we'll talk about what's realistic.

Want the full picture?

See every capability — versioning, custom domains, team roles, and more — on the features page.