CDN

Uploads

Uploads use a three-step flow: your backend mints a short-lived permit, the client uploads with it to your account's CDN host, and the CDN records the upload to Oblien (async, durable node→API recording — retried until acknowledged). Your backend then reads the file from the Oblien API. See How it works for the mental model.

your backend ── POST /cdn/token ─────────────▶ Oblien API   (quota gate; mints a one-shot permit for a namespace)
client ── POST https://<host>/api  Bearer <permit> + file ──▶ CDN edge
       ◀── { url, variants, recorded } ──
CDN edge ── records (async, retried) ─────────▶ Oblien API   (authoritative record)
your backend ── GET /cdn/files ───────────────▶ Oblien API   (sees the upload)

1. Mint a permit

Your backend calls POST /cdn/token with its API key. The permit is a short-lived (≈1 min), single-use JWT — reusing it returns 401 permit_already_used, so mint one per upload operation.

const { token } = await oblien.cdn.token({
  namespace: 'customer-123',        // API-side grouping: quota + sort (optional)
  tag: 'avatar',                    // classification string — fetch by it later (optional)
  variants: {                       // API-issued processing directives (optional)
    variantNames: ['thumb', 'card'],
    customVariants: { card: { width: 600, height: 400, fit: 'cover' } },
  },
  metadata: { orderId: 'A-1001' },  // OPAQUE — stored + echoed, never interpreted
  // cdnHost: 'cdn.yourbrand.com',  // optional verified custom delivery domain
});
POST /cdn/token
X-Client-ID: <id>
X-Client-Secret: <secret>
Content-Type: application/json

{
  "namespace": "customer-123",
  "variants": {
    "variantNames": ["thumb", "card"],
    "customVariants": { "card": { "width": 600, "height": 400, "fit": "cover" } }
  },
  "metadata": { "orderId": "A-1001" }
}
curl -X POST https://api.oblien.com/cdn/token \
  -H "X-Client-ID: $OBLIEN_CLIENT_ID" \
  -H "X-Client-Secret: $OBLIEN_CLIENT_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"namespace":"customer-123","variants":{"variantNames":["thumb"]}}'

The permit carries your delivery host — your account's <slug>.cdn.oblien.com — the namespace tag (for quota/sort, recorded as cdn_files.namespace), and your variants directives. The host defaults to your account host; pass cdnHost only to use a custom domain you've verified (also account-level). Any other host is rejected with 400 invalid_cdn_host. Namespace is not the host — all your namespaces serve from the one account host.

An optional tag is a plain classification string (≤64 chars, stored verbatim) recorded on every file uploaded with this permit as cdn_files.tag, so you can later fetch all files of a kind — e.g. every avatar — optionally scoped to a namespace (see step 3). It's a first-class, indexed field, distinct from the opaque metadata: use tag to classify ("what kind of file"), metadata for everything else.

Mint on your backend with your API key, then hand only the permit to the client. Never ship X-Client-Secret to a browser.

Token scopes & permissions

POST /cdn/token (oblien.cdn.token()) mints a user permit — permissions upload + process — the default for client uploads. For backend management at the edge (read / list / info / delete), mint an admin permit with POST /cdn/token/admin (oblien.cdn.adminToken()), which requires an admin API key. Both are ~1-minute and single-use; a permit missing a required permission is rejected with 403 forbidden.

ScopeMinted byPermissions
userany admin- or namespace-scoped keyupload, process
adminadmin key onlyupload, process, read, list, info, delete

2. Upload

The client uploads to your account host (<slug>.cdn.oblien.com, from the mint). The field is file (single) or files (batch), authorized with the permit.

// Server-side one-shot (mints + uploads): oblien.cdn.upload(file, { namespace })
const out = await oblien.cdn.upload(
  { data: bytes, filename: 'photo.jpg', contentType: 'image/jpeg' },
  { namespace: 'customer-123' },
);

// Or upload from the client with a permit your backend minted:
const form = new FormData();
form.append('file', file);
const res = await fetch(`https://${cdnHost}/api`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}` },
  body: form,
});
const out2 = await res.json();
POST https://<host>/api
Authorization: Bearer <permit>
Content-Type: multipart/form-data        (field: "file")

Batch: POST https://<host>/api/multiple (field files), or ingest by URL: POST https://<host>/api/process-urls with { "urls": ["https://…"], "maxBatch": 10, "concurrency": 3 }.

curl -X POST "https://$CDN_HOST/api" \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@./photo.jpg"

# batch
curl -X POST "https://$CDN_HOST/api/multiple" \
  -H "Authorization: Bearer $TOKEN" \
  -F "files=@./a.jpg" -F "files=@./b.jpg"

Response:

{
  "success": true,
  "file_id": "…",
  "url": "https://<slug>.cdn.oblien.com/static/ab/cd/file.jpg",
  "filename": "file.jpg",
  "size": 123456,
  "mime": "image/jpeg",
  "tag": "avatar",
  "variants": [{ "variant": "card", "url": "…", "size": 20480, "width": 600, "height": 400 }],
  "expires_at": 1730000000000,
  "recorded": true
}

recorded: true means Oblien has registered the file and all its variants before returning this response (server-to-server) — so the file_id is immediately fetchable from the API (see step 3). On the rare API hiccup it comes back false: the file is stored and registration is queued for retry, so it will appear shortly. expires_at appears only when a TTL is set.

Variants

Variants are chosen per request via the token's dedicated variants object — there are no server-side presets, and the default is none: an upload with no variants stores just the original. Fields (all optional):

  • variantNames — which variants to produce (omit → none).
  • customVariants{ name: { width?, height?, fit?, quality?, blur?, format?, withoutEnlargement? } }. A spec with no width/height is a same-size re-encode (just quality/format/blur).
  • keepOriginal — keep the uploaded original (default true; can't discard all copies).
  • primary — which produced variant is the top-level url (default full).
  • optimizeOriginal{ maxBytes, quality, format } to re-encode an original larger than maxBytes.
  • ttl / expireAt — auto-delete the upload (see Auto-delete).
  • maxBytes — lower the per-file size cap for this upload.

Save only a compressed copy (no original, no resize):

variants: {
  keepOriginal: false,
  variantNames: ['c'],
  customVariants: { c: { quality: 60, format: 'webp' } }, // same size, recompressed
  primary: 'c',
}
// → add e.g. thumb:{width:150,height:150} + blur:{width:75,height:75,blur:5} for compressed + thumb + blur

variants is validated at mint; malformed input is rejected with a specific error code — e.g. variant_name_invalid, variant_width_invalid, variant_quality_invalid, variant_spec_empty, variants_too_many, custom_variant_key_invalid, variant_ttl_invalid. metadata is separate and opaque: stored with the file and echoed back, never interpreted for processing.

Discover limits & schema

The edge exposes two read-only helpers to introspect what's allowed:

  • GET https://<host>/api/limits (oblien.cdn.getLimits()) — max file size per type + max files per request.
  • GET https://<host>/api/variants (oblien.cdn.getVariantOptions()) — the custom-variant field schema (width, height, fit, quality, blur, format).

/api/multiple (batch) and /api/process-urls (ingest by URL) share the token's variants and return a files[] array, each entry with its own file_id.

3. See the upload

The CDN registers every upload to Oblien server-to-server, so your backend doesn't need to trust the client's response. Two ways to update your own DB:

Fetch by file_id — the cleanest for the direct-client-upload flow. Your client relays the file_id it got back to your backend, which fetches the authoritative record (GET /cdn/files/<file_id> accepts the file_id). Because recorded: true means it's already registered, this succeeds immediately:

// Your backend, given the file_id the client relayed:
const { data } = await oblien.cdn.get(fileId);
// data → { id, node_file_id, filename, cdn_url, size, total_bytes, mime_type,
//          namespace, tag, variants:[{variant,url,size,width,height}],
//          metadata, created_at }  → store in your DB
GET /cdn/files/<file_id>
X-Client-ID: <id>
X-Client-Secret: <secret>
curl "https://api.oblien.com/cdn/files/$FILE_ID" \
  -H "X-Client-ID: $OBLIEN_CLIENT_ID" -H "X-Client-Secret: $OBLIEN_CLIENT_SECRET"

Or list — reconcile files (filter/sort/paginate), optionally by tag:

const { data } = await oblien.cdn.list({ namespace: 'customer-123' });
const avatars = await oblien.cdn.list({ tag: 'avatar' });          // all files tagged 'avatar'
const scoped  = await oblien.cdn.list({ namespace: 'customer-123', tag: 'avatar' });
const { data: t } = await oblien.cdn.tags({ namespace: 'customer-123' }); // → { tags: [...] }

Fetches are scoped to your account, so you only ever read your own files. Anything you put in the permit's opaque metadata is stored with the file and returned here, so you can correlate an upload to your own records.

Prefer not to route the id through the client at all? An outbound webhook (Oblien → your backend on register) is on the roadmap — until then, the fetch-by-file_id pull above is the authoritative path.