Storage API · Docs

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.

  • 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.online

All 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
Delivery URLs need no API key:
• 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

1Upload an image (optionally into a folder)
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.

2Get a public URL at the size/format you want
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", ... }
3Embed it
<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

POST/api/v1/images

Upload (multipart). Fields: file (required), folder (optional path, auto-created).

GET/api/v1/images?folder=&q=&page=1&pageSize=24

List media. Filter by folder, search by name (q). Returns { items, total, page, pageSize }.

GET/api/v1/images/:id

Get one image's metadata.

PUT/api/v1/images/:id

Replace the file (multipart: file). Keeps the same id/URLs; old cached variants are purged.

DELETE/api/v1/images/:id

Delete the image and all its cached variants.

Upload response
{
  "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.

Allowed types: pdf, doc, docx, xls, xlsx, csv, ppt, pptx, txt. Max 100 MB per file.
POST/api/v1/documents

Upload (multipart). Fields: file (required), folder (optional path, auto-created).

GET/api/v1/documents?folder=&q=&page=1&pageSize=24

List documents. Filter by folder, search by name (q). Returns { items, total, page, pageSize }.

GET/api/v1/documents/:id

Get one document's metadata.

GET/api/v1/documents/:id/download

Stream the file. Requires your API key.

DELETE/api/v1/documents/:id

Delete the document and its stored bytes.

GET/d/<clientId>/<sig>/<documentId>

Public signed download — no API key. A tampered signature returns 403.

Upload
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"
Upload response
{
  "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.

Status lifecycle
createduploadedprocessingreadyorfailed

Poll GET /api/v1/videos/:id until ready. Only ready videos can be played.

POST/api/v1/videos

Create a video. Body: { "title": "Lecture 01", "folder"?: "courses/math" }. Returns { video_id, upload_url, upload_token, expires_in }.

GET/api/v1/videos?folder=&q=&page=1&pageSize=24

List videos. Returns { items, total, page, pageSize }.

GET/api/v1/videos/:id

Get status, duration, renditions and thumbnail_url.

POST/api/v1/videos/:id/playback-token

Mint a viewer token. Body: { "user_id": "user-42" }. Returns { playback_token, master_url, expires_in }.

DELETE/api/v1/videos/:id

Delete the video, its HLS renditions and any raw upload.

GET/playback/:videoId/master.m3u8

HLS master playlist. Requires the playback token (Authorization: Bearer …, or ?token=…).

1. Create + upload (resumable, survives network drops)
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.

2. Video response (when ready)
{
  "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"
}
3. Play it (adaptive HLS)
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.

Players that can’t set headers (Safari’s native HLS) may pass the token as a query param instead: 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.

GET/api/v1/folders

Get the folder tree.

POST/api/v1/folders

Create a nested folder. Body: { "path": "a/b/c" }.

PATCH/api/v1/folders/:id

Rename a folder. Body: { "name": "newname" }.

DELETE/api/v1/folders/:id

Delete a folder (and its sub-folders + images).

Delivery URLs

Every image URL is one of two kinds — both are public to view:

Signed (any transform)
GET /t/<clientId>/<sig>/<transform>/<imageId>.<ext>

Mint one with POST /api/v1/sign (below).

Preset (no signature)
GET /t/<clientId>/preset:<name>/<imageId>.<ext>

Define a preset once, then build URLs client-side for any image — no signing call needed.

Documents & video
GET /d/<clientId>/<sig>/<documentId>        # public, signed
GET /playback/<videoId>/master.m3u8         # needs a playback token

Sign a URL

POST/api/v1/sign

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

GET/api/v1/presets

List your presets.

POST/api/v1/presets

Create a preset. Body: { "name": "thumb", "params": "w_240,c_fill,f_webp,q_75" }.

DELETE/api/v1/presets/:id

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

ParamMeaningValues
w_Width (px)e.g. w_600
h_Height (px)e.g. h_400
c_Fit / crop modefit · fill · cover · contain · inside
f_Output formatwebp · jpeg · png · avif · auto
q_Quality1–100 (e.g. q_80)
dpr_Device pixel ratioe.g. dpr_2 (multiplies w/h)

Width-only (e.g. w_600) keeps the aspect ratio. f_auto resolves to WebP.

Examples

JavaScript — upload an image
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);
JavaScript — get a public image URL
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, embeddable
JavaScript — upload a document & share it
const 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