Connect any app to UQ Cloud in minutes.
A single REST endpoint to ingest media, a URL-driven transformation grammar, and signed webhooks for asynchronous events. Everything you need to build a production integration is on this page.
One click copies a complete, AI-ready brief of every endpoint, auth flow, and example — paste into ChatGPT, Claude, Cursor, or any model to have it wire your app to UQ Cloud instantly.
Introduction
UQ Cloud is a headless media pipeline. Your app POSTs a source URL, we fetch, validate, store, and return a stable public_id. From that moment, any resized, compressed, or format-converted version of the asset is available at a predictable URL — no re-uploads, no batch jobs.
Quickstart
- 1Create a keyVisit API Keys and generate a key. Copy the secret — it's shown once.
- 2Send your first uploadPoint us at any publicly reachable URL. We fetch it server-side.
- 3Use the returned public_idCompose a transform URL and drop it into an <img> tag.
curl -X POST https://cloud.zeni.host/api/public/uploads \
-H "Authorization: Bearer pfk_XXXX.YYYY" \
-H "Content-Type: application/json" \
-d '{"url":"https://picsum.photos/1200"}'Authentication
Every request must include an Authorization header. Keys are formatted <prefix>.<secret>. The prefix is public (safe to log); the secret is hashed with SHA-256 server-side and compared in constant time.
Authorization: Bearer pfk_9f3a2c.6b41d0…c9e2Upload API
Ingest a remote asset. UQ Cloud fetches it server-side (SSRF-guarded and size-capped at 50 MB), validates the MIME type, stores the original, and returns a stable identifier.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| url | string | yes | Publicly reachable http(s) URL of the source asset. |
| filename | string | no | Override filename. Defaults to the URL's last segment. |
| folder | string | no | Slash-delimited folder path. Nested folders are created on demand (e.g. "clients/acme/logos"). |
| drive | string | no | Destination drive: "supabase" (default), a drive UUID, or a drive label/provider (e.g. "koofr", "r2", "idrive"). Call GET /api/public/drives to discover ids. |
Response · 201
{
"id": "e8b1…",
"public_id": "sunset-cliffs-4k",
"filename": "sunset.jpg",
"bytes": 482113,
"kind": "image",
"drive": "Supabase (built-in)",
"drive_id": "supabase",
"storage_path": "uploads/images/2026/07/sunset-cliffs-4k.jpg"
}Try it live
Drives
Route uploads to the built-in Supabase bucket or to any cloud drive configured by your workspace admin — Cloudflare R2, S3, Backblaze B2, Koofr, IDrive Sync, or any WebDAV endpoint. Each drive advertises its own capacity, so external clients can pick the cheapest or emptiest destination programmatically.
List available drives
curl https://cloud.zeni.host/api/public/drives \
-H "Authorization: Bearer $UQCLOUD_KEY"Response · 200
{
"drives": [
{
"id": "supabase",
"label": "Supabase (built-in)",
"provider": "supabase",
"enabled": true,
"max_bytes": null,
"used_bytes": null,
"free_bytes": null
},
{
"id": "9f2e6c1a-…",
"label": "Koofr Main",
"provider": "webdav",
"enabled": true,
"max_bytes": 10737418240,
"used_bytes": 12345,
"free_bytes": 10737405895
},
{
"id": "3aa8b4d2-…",
"label": "R2 Account 1",
"provider": "r2",
"enabled": true,
"max_bytes": 53687091200,
"used_bytes": 8000,
"free_bytes": 53687083200
}
]
}supabase reports null capacity (managed pool). Cloud drives report the quota assigned to the drive.
Upload to a specific drive
curl -X POST https://cloud.zeni.host/api/public/uploads \
-H "Authorization: Bearer $UQCLOUD_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/photo.jpg",
"filename": "products/photo.jpg",
"drive": "koofr"
}'Drive selection rules
| Value | Matches |
|---|---|
| "supabase" | Built-in Supabase bucket (default). |
| <uuid> | Exact drive id from GET /drives. |
| <label> | Drive label, case-insensitive (e.g. "Koofr Main"). |
| <provider> | First enabled drive of that provider: r2 · s3 · b2 · webdav. |
Storage layout
| Drive | Path pattern |
|---|---|
| supabase | {user_id}/{folder?}/{public_id}.{ext} — required by RLS. |
| cloud (r2 · s3 · webdav) | uploads/{images|videos|models|files}/YYYY/MM/{folder?}/{public_id}.{ext} |
Drive-specific errors
| Status | Meaning |
|---|---|
| 404 | Drive not found — id, label, or provider did not match. |
| 409 | Drive is disabled. |
| 502 | Upstream drive rejected the upload (auth, bucket, network). |
| 507 | Drive is full — free_bytes < file size. Pick another via GET /drives. |
Transformations
Compose transforms in the path. Order is irrelevant, unknown keys are rejected, and every response is cached at the edge with immutable.
https://cdn.cloud.zeni.host/t/<transforms>/<user_id>/<public_id>.<ext>Parameters
| Key | Example | Meaning |
|---|---|---|
| w_ | w_800 | Width in pixels |
| h_ | h_600 | Height in pixels |
| q_ | q_80 | Quality (1–100) |
| f_ | f_webp | Format: webp · avif · jpg · png |
| c_ | c_fill | Fit: cover · contain · fill · inside |
| b_ | b_10 | Gaussian blur sigma |
| g_ | g_1 | Grayscale |
Playground
Delivery & CDN
Every asset is served through one of two URL shapes. Both are designed for edge delivery — use the transform URL in your app whenever possible, even when you don't need a transform.
1. Transform URL (recommended)
https://cdn.cloud.zeni.host/t/<transforms>/<user_id>/<public_id>.<ext>- Served by the UQ Cloud edge worker + transform service.
- Cached
immutablefor 1 year across 200+ Cloudflare POPs. - Works for every drive — the worker fetches the origin object once, transforms it, and stores the derivative. Subsequent requests hit the edge, not your drive.
- Use
/t//(no transforms) to serve the original bytes through the CDN.
2. Raw signed URL (exact original bytes)
| Drive type | URL shape | Edge cache |
|---|---|---|
| R2 · S3 · B2 | Provider presigned URL | R2 is edge-cached by default; S3 needs CloudFront. |
| Supabase | Supabase Storage signed URL | Cached by Supabase's edge. |
| Koofr · IDrive (WebDAV) | /api/storage-proxy/<backendId>/<key>?exp=…&sig=… | Not edge-cached by default — proxied same-origin. |
Rule of thumb
In any external app, always link the /t/…/public_id.exttransform URL. The drive choice becomes an internal storage detail — end users always get sub-100 ms edge delivery, and you can migrate an asset between drives later without changing the URL your customers embed.
Example
<!-- Same asset, three drives, one URL shape -->
<img src="https://cdn.cloud.zeni.host/t/w_800,f_webp/USER_ID/PUBLIC_ID.jpg" />Webhooks
Configure endpoints on the Webhooks page. Every delivery includes an X-UQ Cloud-Signature header — an HMAC-SHA256 of the raw JSON body, computed with your endpoint secret.
Verify a signature (Node)
import { createHmac, timingSafeEqual } from "crypto";
export function verify(rawBody, header, secret) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(header || "", "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}Event catalog
| Event | Fires when |
|---|---|
| asset.uploaded | An asset is successfully ingested. |
| asset.deleted | An asset is removed from your library. |
| asset.analyzed | AI analysis (tags, alt text) completes. |
| preset.applied | A batch preset finishes running. |
SDKs & Snippets
export function useUQ CloudUpload(key: string) {
return async (url: string) => {
const r = await fetch("/api/public/uploads", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ url }),
});
if (!r.ok) throw new Error(await r.text());
return r.json();
};
}"use server";
export async function ingest(url: string) {
const r = await fetch(process.env.PF_URL + "/api/public/uploads", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PF_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ url }),
});
return r.json();
}Errors
Errors return a JSON body { "error": "message" } with a semantic status code.
| Status | Meaning |
|---|---|
| 400 | Invalid input (bad URL, unsupported host). |
| 401 | Missing or invalid Authorization header. |
| 402 | Quota exceeded (assets or storage). |
| 403 | Key lacks the required scope. |
| 404 | Drive not found (see Drives section). |
| 409 | Drive is disabled. |
| 413 | Payload exceeds 50 MB cap. |
| 502 | Upstream drive rejected the upload. |
| 507 | Selected drive is full. |
| 500 | Internal error — safe to retry with backoff. |
Rate limits
Hitting a limit returns 429 with a Retry-After header. Contact us to lift caps.
Generate a key and ship your first integration.
Most teams have their first asset flowing in under five minutes.
Create API key