Overview
A self-hosted storage & delivery API for images, documents, and video. Upload once, then serve optimized media through public URLs — images resized and converted on the fly, documents downloadable via signed links, and video transcoded to adaptive HLS.
Resize, crop, compress, convert to WebP/AVIF on demand. Cached and served with long-lived headers.
Store PDF, Word, Excel, PowerPoint, CSV and text. Serve them with public, HMAC-signed links.
Resumable uploads of multi-GB files, transcoded to adaptive HLS and streamed behind short-lived tokens.
- Organize images & documents in nested folders per client.
- Image URLs come from signed URLs (any transform) or named presets.
- Image & document URLs are public to view; video playback requires a token.
Base URL
https://api-storage.youssef-soltan.onlineAll management endpoints below are relative to this base.
Authentication
Programmatic calls to /api/v1/* require your API key, sent as an X-Api-Key header. Create and manage keys in the dashboard → API Keys. Keep keys secret — they act on your account.
X-Api-Key: isk_your_api_key_here• Images
/t/… and documents /d/… are authorized by their signature (or a preset), so anyone with the URL can view them.• Video
/playback/… is different — it requires a short-lived playback token you mint per viewer.Quick start
curl -X POST https://api-storage.youssef-soltan.online/api/v1/images \
-H "X-Api-Key: YOUR_API_KEY" \
-F "folder=products/shoes" \
-F "file=@photo.jpg"The folder path is created automatically if it doesn’t exist. Omit folder to upload to the root. The response includes a ready-to-use thumbnailUrl and the image id.
curl -X POST https://api-storage.youssef-soltan.online/api/v1/sign \
-H "X-Api-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{"imageId":"IMAGE_ID","transform":"w_600,f_webp,q_80"}'
# → { "url": "https://api-storage.youssef-soltan.online/t/<clientId>/<sig>/w_600,f_webp,q_80/IMAGE_ID.webp", ... }<img src="https://api-storage.youssef-soltan.online/t/<clientId>/<sig>/w_600,f_webp,q_80/IMAGE_ID.webp" alt="" />Tip: in the dashboard, click any image to copy a public URL instantly — no code needed.
Images
/api/v1/imagesUpload (multipart). Fields: file (required), folder (optional path, auto-created).
/api/v1/images?folder=&q=&page=1&pageSize=24List media. Filter by folder, search by name (q). Returns { items, total, page, pageSize }.
/api/v1/images/:idGet one image's metadata.
/api/v1/images/:idReplace the file (multipart: file). Keeps the same id/URLs; old cached variants are purged.
/api/v1/images/:idDelete the image and all its cached variants.
{
"id": "a124…",
"originalName": "photo.jpg",
"folder": "products/shoes",
"format": "jpeg",
"width": 1200, "height": 800, "bytes": 34567,
"deliveryBase": "/t/<clientId>",
"thumbnailUrl": "https://api-storage.youssef-soltan.online/t/<clientId>/<sig>/w_240,f_webp,q_75/a124….webp"
}Documents
Store and serve files that need no transformation. Every document gets a public, HMAC-signed URL you can hand out without exposing your API key, plus an authenticated download endpoint.
pdf, doc, docx, xls, xlsx, csv, ppt, pptx, txt. Max 100 MB per file./api/v1/documentsUpload (multipart). Fields: file (required), folder (optional path, auto-created).
/api/v1/documents?folder=&q=&page=1&pageSize=24List documents. Filter by folder, search by name (q). Returns { items, total, page, pageSize }.
/api/v1/documents/:idGet one document's metadata.
/api/v1/documents/:id/downloadStream the file. Requires your API key.
/api/v1/documents/:idDelete the document and its stored bytes.
/d/<clientId>/<sig>/<documentId>Public signed download — no API key. A tampered signature returns 403.
curl -X POST https://api-storage.youssef-soltan.online/api/v1/documents \
-H "X-Api-Key: YOUR_API_KEY" \
-F "folder=contracts/2026" \
-F "file=@report.pdf"{
"id": "ca30…",
"originalName": "report.pdf",
"folder": "contracts/2026",
"mimeType": "application/pdf",
"ext": "pdf",
"bytes": 348213,
"createdAt": "2026-07-10T11:46:41.564Z",
"downloadUrl": "/api/v1/documents/ca30…/download",
"signedUrl": "https://api-storage.youssef-soltan.online/d/<clientId>/<sig>/ca30…"
}Share signedUrl directly — it needs no key. Use downloadUrl (with your key) when you want to stream the bytes server-side.
Video
Video uses a three-step flow: create a video to get an upload ticket, upload the file with the resumable tus protocol, then play it as adaptive HLS behind a short-lived token. Uploads are chunked and resume after a dropped connection, so multi-GB, multi-hour recordings are supported.
created→uploaded→processing→readyorfailedPoll GET /api/v1/videos/:id until ready. Only ready videos can be played.
/api/v1/videosCreate a video. Body: { "title": "Lecture 01", "folder"?: "courses/math" }. Returns { video_id, upload_url, upload_token, expires_in }.
/api/v1/videos?folder=&q=&page=1&pageSize=24List videos. Returns { items, total, page, pageSize }.
/api/v1/videos/:idGet status, duration, renditions and thumbnail_url.
/api/v1/videos/:id/playback-tokenMint a viewer token. Body: { "user_id": "user-42" }. Returns { playback_token, master_url, expires_in }.
/api/v1/videos/:idDelete the video, its HLS renditions and any raw upload.
/playback/:videoId/master.m3u8HLS master playlist. Requires the playback token (Authorization: Bearer …, or ?token=…).
import * as tus from 'tus-js-client';
// Create the video record and get a scoped upload ticket.
const ticket = await fetch('https://api-storage.youssef-soltan.online/api/v1/videos', {
method: 'POST',
headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Lecture 01' }),
}).then((r) => r.json());
// Upload with tus. Chunked, so a dropped connection resumes where it left off.
const upload = new tus.Upload(file, {
endpoint: ticket.upload_url,
chunkSize: 50 * 1024 * 1024, // 50 MB per request
retryDelays: [0, 3000, 5000, 10000, 20000],
headers: { Authorization: `Bearer ${ticket.upload_token}` },
metadata: { filename: file.name, filetype: file.type },
onProgress: (sent, total) => console.log(Math.round((sent / total) * 100) + '%'),
onSuccess: () => console.log('Uploaded — transcoding started'),
});
upload.start();Transcoding starts automatically once the last chunk lands. The upload token is scoped to this one video.
{
"id": "c474…",
"title": "Lecture 01",
"status": "ready",
"folder": null,
"duration_seconds": 5412,
"renditions": [
{ "name": "480p", "height": 480, "bandwidth": 964000 },
{ "name": "720p", "height": 720, "bandwidth": 1664000 }
],
"thumbnail_url": "https://api-storage.youssef-soltan.online/playback/c474…/thumbnail.jpg",
"created_at": "2026-07-10T11:50:40.750Z",
"updated_at": "2026-07-10T11:50:44.112Z"
}import Hls from 'hls.js';
// Mint a short-lived token for this viewer.
const pb = await fetch('https://api-storage.youssef-soltan.online/api/v1/videos/VIDEO_ID/playback-token', {
method: 'POST',
headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: 'user-42' }),
}).then((r) => r.json());
const hls = new Hls({
xhrSetup: (xhr) => xhr.setRequestHeader('Authorization', `Bearer ${pb.playback_token}`),
});
hls.loadSource(pb.master_url); // https://api-storage.youssef-soltan.online/playback/VIDEO_ID/master.m3u8
hls.attachMedia(document.querySelector('video'));Mint the token server-side once you’ve checked the viewer is allowed to watch. It expires quickly, so every segment request stays authorized without exposing your API key.
master.m3u8?token=…Folders
One folder tree per client, shared by images, documents and videos — pass folder on upload (or on video create) and the path is created automatically.
/api/v1/foldersGet the folder tree.
/api/v1/foldersCreate a nested folder. Body: { "path": "a/b/c" }.
/api/v1/folders/:idRename a folder. Body: { "name": "newname" }.
/api/v1/folders/:idDelete a folder (and its sub-folders + images).
Search
/api/v1/search?q=shoesSearch your media by image name and folder name. Returns { images, folders }.
Delivery URLs
Every image URL is one of two kinds — both are public to view:
GET /t/<clientId>/<sig>/<transform>/<imageId>.<ext>Mint one with POST /api/v1/sign (below).
GET /t/<clientId>/preset:<name>/<imageId>.<ext>Define a preset once, then build URLs client-side for any image — no signing call needed.
GET /d/<clientId>/<sig>/<documentId> # public, signed
GET /playback/<videoId>/master.m3u8 # needs a playback tokenSign a URL
/api/v1/signBody: { "imageId": "…", "transform": "w_600,f_webp,q_80" }. Returns { url, canonical, format }.
transform is optional — omit it to get the full-size image (WebP by default). Documents get their signedUrl back on upload; no separate signing call is needed.
Presets
/api/v1/presetsList your presets.
/api/v1/presetsCreate a preset. Body: { "name": "thumb", "params": "w_240,c_fill,f_webp,q_75" }.
/api/v1/presets/:idDelete a preset.
Transform parameters
A transform is a comma-separated list of key_value segments, e.g. w_600,h_400,c_cover,f_webp,q_80. Images only.
| Param | Meaning | Values |
|---|---|---|
w_ | Width (px) | e.g. w_600 |
h_ | Height (px) | e.g. h_400 |
c_ | Fit / crop mode | fit · fill · cover · contain · inside |
f_ | Output format | webp · jpeg · png · avif · auto |
q_ | Quality | 1–100 (e.g. q_80) |
dpr_ | Device pixel ratio | e.g. dpr_2 (multiplies w/h) |
Width-only (e.g. w_600) keeps the aspect ratio. f_auto resolves to WebP.
Examples
const fd = new FormData();
fd.append('file', fileInput.files[0]);
fd.append('folder', 'products'); // optional
const res = await fetch('https://api-storage.youssef-soltan.online/api/v1/images', {
method: 'POST',
headers: { 'X-Api-Key': 'YOUR_API_KEY' }, // do NOT set Content-Type for FormData
body: fd,
});
const image = await res.json();
console.log(image.id, image.thumbnailUrl);const res = await fetch('https://api-storage.youssef-soltan.online/api/v1/sign', {
method: 'POST',
headers: { 'X-Api-Key': 'YOUR_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ imageId, transform: 'w_800,f_webp,q_82' }),
});
const { url } = await res.json(); // public, embeddableconst fd = new FormData();
fd.append('file', fileInput.files[0]);
fd.append('folder', 'contracts'); // optional
const res = await fetch('https://api-storage.youssef-soltan.online/api/v1/documents', {
method: 'POST',
headers: { 'X-Api-Key': 'YOUR_API_KEY' },
body: fd,
});
const doc = await res.json();
console.log(doc.signedUrl); // public link, no API key needed