Openinary Docs
Complete reference for Openinary — a self-hosted, open-source media management platform with Cloudinary-compatible transformation URLs, REST API, JavaScript SDK, and webhooks.
Overview
What Openinary is and what it can do.
Openinary is a self-hosted media management platform. Upload images and videos, apply on-the-fly transformations via URL parameters (resize, crop, format convert, quality compression), organize into folders, and serve via a built-in CDN-friendly URL scheme.
Transformation URLs
Cloudinary-style URL parameters — resize, crop, format, quality, filters.
Video Support
Transcode to MP4/WebM/GIF, extract frames, generate sprite sheets.
REST API + SDK
Full REST API with a typed JavaScript/TypeScript SDK.
Webhooks
HTTP callbacks for upload, delete, transcode events.
User Auth
JWT-based auth with Admin / Editor / Viewer roles and API keys.
Upload Presets
Reusable upload configs — format restrictions, size limits, auto-tags.
Installation
Running Openinary with Docker Compose.
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: openinary
POSTGRES_USER: openinary
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
app:
image: ghcr.io/yourorg/openinary:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://openinary:secret@postgres:5432/openinary
UPLOAD_DIR: /data/uploads
CACHE_DIR: /data/cache
PUBLIC_URL: http://localhost:3000
AUTH_ENABLED: "true"
DASHBOARD_USERNAME: admin
DASHBOARD_PASSWORD: change-me-in-production
IMG_SIGNING_SECRET: change-me-in-production
volumes:
- uploads:/data/uploads
- cache:/data/cache
depends_on:
postgres:
condition: service_healthy
volumes:
pgdata:
uploads:
cache:docker compose up -d
# Migrations run automatically on first startup
# Open http://localhost:3000 and sign inConfiguration
Server environment variables.
| Parameter | Type | Description |
|---|---|---|
DATABASE_URLrequired | string | PostgreSQL connection string. |
PUBLIC_URLrequired | string | Public-facing base URL of the app (e.g. https://media.example.com). |
AUTH_ENABLED | boolean | Require login to access the dashboard and API. Default: false. |
DASHBOARD_USERNAME | string | Admin login username. Default: admin. |
DASHBOARD_PASSWORD | string | Admin login password. Default: changeme. |
IMG_SIGNING_SECRET | string | HMAC secret for signed transformation URLs. |
UPLOAD_DIR | string | Absolute path for uploaded files. Default: /data/uploads. |
CACHE_DIR | string | Absolute path for transform cache. Default: /data/cache. |
Quick Start
Upload your first image and serve it with a transformation.
1. Upload via API
curl -X POST http://localhost:3000/api/upload \
-H "Authorization: Bearer <your-jwt-token>" \
-F "file=@photo.jpg" \
-F "folder=marketing"{
"id": "550e8400-e29b-41d4-a716-446655440000",
"filename": "photo.jpg",
"folder": "marketing",
"url": "http://localhost:3000/api/uploads/marketing/photo.jpg",
"size": 245120,
"mime": "image/jpeg",
"width": 1920,
"height": 1080
}2. Serve with transformations
Append transform parameters to the image URL using the path format:
# Pattern
GET /img/<transforms>/<file-key>
# Examples
GET /img/w_400,h_400,c_fill/marketing/photo.jpg → 400×400 crop
GET /img/w_800,f_webp,q_80/marketing/photo.jpg → 800px wide WebP
GET /img/w_200,h_200,r_max/marketing/photo.jpg → circular thumbnail
GET /img/e_grayscale/marketing/photo.jpg → black & white
GET /img/t_thumbnail/marketing/photo.jpg → named preset3. Use the JavaScript SDK
import { OpeninaryClient } from "openinary";
const client = new OpeninaryClient({
baseUrl: "http://localhost:3000",
apiKey: "ok_yourkey",
});
// Upload a file
const asset = await client.assets.upload(file, { folder: "avatars" });
// Build transformation URL
const url = client.assets.url(asset.file_key, {
width: 200, height: 200, crop: "fill", format: "webp", quality: 80,
});
console.log(url);
// → http://localhost:3000/img/w_200,h_200,c_fill,f_webp,q_80/avatars/photo.jpgAuthentication
JWT-based auth and API key auth.
AUTH_ENABLED=false, all endpoints are public. When enabled, every request must include either a Bearer JWT or an ApiKey token in the Authorization header.Login
/api/auth/loginExchange credentials for a JWT token.
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"secret"}'{
"token": "eyJhbGciOiJIUzI1NiJ9...",
"user": {
"id": "...",
"email": "admin@example.com",
"role": "admin"
}
}Using tokens
# JWT token (from login)
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
# API key (created in dashboard → API Keys)
Authorization: ApiKey ok_yourkey_hereOther auth endpoints
/api/auth/me🔐 Auth requiredGet the currently authenticated user.
Upload
Upload files by form, URL, or base64.
File upload
/api/upload🔐 Auth requiredUpload one or more files via multipart/form-data.
| Parameter | Type | Description |
|---|---|---|
filerequired | File (multipart) | The file to upload. Repeat for multiple files. |
folder | string | Destination folder. Default: default. |
tags | string | Comma-separated tags to apply. |
preset | string | Name of an upload preset to apply. |
curl -X POST http://localhost:3000/api/upload \
-H "Authorization: Bearer <token>" \
-F "file=@image.jpg" \
-F "folder=products" \
-F "tags=hero,2024"URL import
/api/upload/from-url🔐 Auth requiredFetch a remote URL and store it as an asset.
curl -X POST http://localhost:3000/api/upload/from-url \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/photo.jpg","folder":"imports"}'Base64 upload
/api/upload/base64🔐 Auth requiredUpload a base64-encoded file payload.
curl -X POST http://localhost:3000/api/upload/base64 \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"data":"data:image/jpeg;base64,/9j/4AAQ...","filename":"avatar.jpg","folder":"users"}'Unsigned upload (preset required)
/api/upload/unsigned/:presetUpload without a token when the preset has unsigned=true.
Assets
CRUD operations on uploaded assets.
/api/assets🔐 Auth requiredList assets with optional filters.
/api/asset/:id🔐 Auth requiredGet a single asset by ID.
/api/asset/:id🔐 Auth requiredUpdate asset metadata (filename, folder, context).
/api/asset/:id🔐 Auth requiredSoft-delete (move to trash).
/api/asset/:id/permanent🔐 Auth requiredPermanently delete from disk.
/api/asset/:id/restore🔐 Auth requiredRestore from trash.
/api/asset/:id/replace🔐 Auth requiredReplace file contents (keeps same ID & metadata).
GET /assets query params
| Parameter | Type | Description |
|---|---|---|
folder | string | Filter by folder name. |
tag | string | Filter by tag. |
mime | string | Filter by MIME prefix (e.g. image/, video/). |
limit | number | Max results per page. Default: 50, max: 200. |
offset | number | Pagination offset. |
sort | string | Sort field: created_at | filename | size. |
order | string | asc or desc. Default: desc. |
Bulk operations
/api/assets/bulk-delete🔐 Auth requiredSoft-delete multiple assets.
/api/assets/bulk-tag🔐 Auth requiredAdd tags to multiple assets.
/api/assets/bulk-move🔐 Auth requiredMove multiple assets to a folder.
# Bulk delete
curl -X POST http://localhost:3000/api/assets/bulk-delete \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"ids":["uuid-1","uuid-2","uuid-3"]}'Tags
/api/asset/:id/tags🔐 Auth requiredAdd tags to an asset.
/api/asset/:id/tags🔐 Auth requiredRemove tags from an asset.
/api/tags🔐 Auth requiredList all tags with usage counts.
Versions
/api/asset/:id/versions🔐 Auth requiredList version history for an asset.
/api/asset/:id/restore-version/:vId🔐 Auth requiredRestore a previous version.
Search
/api/search🔐 Auth requiredFull-text search across filenames and tags.
| Parameter | Type | Description |
|---|---|---|
qrequired | string | Search query. |
folder | string | Scope to folder. |
tag | string | Scope to tag. |
limit | number | Max results. |
Transformation URLs
On-the-fly image and video processing via URL parameters.
URL format
GET /img/<transform-string>/<file-key>The transform string is a comma-separated list of key_value pairs. Multiple transform stages can be chained with /.
Image parameters
| Parameter | Type | Description |
|---|---|---|
w | number | Target width in pixels. |
h | number | Target height in pixels. |
c | string | Crop mode: fill | fit | scale | thumb | pad | crop. |
g | string | Gravity: center | north | south | east | west | face. |
f | string | Output format: webp | avif | jpeg | jpg | png | gif. |
q | number | Quality 1–100. JPEG/WebP/AVIF only. |
r | string | Border radius: number (px) or max (circle). |
e | string | Effect: grayscale | blur | sharpen | negate | flip | flop. |
b | string | Background color for pad mode, hex without #. |
t | string | Named transformation. e.g. t_thumbnail. |
dpr | number | Device pixel ratio multiplier (1–3). |
Video parameters
| Parameter | Type | Description |
|---|---|---|
f | string | Output format: mp4 | webm | ogv | gif. |
w | number | Width (maintains aspect unless h also set). |
h | number | Height. |
c | string | Crop mode for video: fill | fit | scale. |
so | number | Start offset in seconds (trim). |
eo | number | End offset in seconds (trim). |
q | number | Quality / CRF (0–51, lower = better). |
fps | number | Output frame rate. |
vc | string | Video codec: h264 | vp9 | libx265. |
ac | string | Audio codec: aac | libopus | none. |
br | string | Bitrate: 1000k | 2M. |
Examples
# 400×400 fill crop, WebP, quality 80
/img/w_400,h_400,c_fill,f_webp,q_80/photos/portrait.jpg
# Circular avatar, 200px
/img/w_200,h_200,c_fill,r_max/users/avatar.png
# Grayscale + blur
/img/e_grayscale/img/e_blur/product.jpg ← chained stages
# Named preset (t_thumbnail must exist in Transformations page)
/img/t_thumbnail/marketing/hero.jpg
# Device pixel ratio for Retina displays
/img/w_200,dpr_2/logo.png ← serves 400px
# Trim video from 5s to 15s, output WebM
/img/so_5,eo_15,f_webm/videos/clip.mp4Signed URL endpoint
/api/asset/:id/signed-url🔐 Auth requiredGenerate a time-limited signed transformation URL.
| Parameter | Type | Description |
|---|---|---|
transformsrequired | string | Transform string (e.g. w_400,c_fill). |
expires | number | Expiry in seconds from now. Default: 3600. |
Folders & Tags
Organize assets into folder hierarchies and tag collections.
/api/folders🔐 Auth requiredList all folders with asset counts and sizes.
/api/folders/:folder🔐 Auth requiredDelete an empty folder.
/api/tags🔐 Auth requiredList all tags with usage counts.
photos/2024/january.[
{
"name": "marketing",
"asset_count": 42,
"total_size": 104857600
},
{
"name": "marketing/heroes",
"asset_count": 5,
"total_size": 20971520
},
{
"name": "products",
"asset_count": 118,
"total_size": 524288000
}
]Upload Presets
Reusable upload configurations.
/api/upload-presets🔐 Auth requiredList all upload presets.
/api/upload-presets🔐 Auth requiredCreate a new upload preset.
/api/upload-presets/:name🔐 Auth requiredDelete an upload preset.
/api/upload-presets/:name/sign🔐 Auth requiredGenerate a signed upload URL for a preset.
Preset fields
| Parameter | Type | Description |
|---|---|---|
namerequired | string | Unique identifier (alphanumeric, hyphens, underscores). |
folder | string | Default upload folder. Default: default. |
allowed_formats | string[] | Whitelisted extensions. Empty = any. |
max_size | number | Max file size in bytes. Default: 52428800 (50 MB). |
tags | string[] | Tags automatically applied to uploads. |
transform_string | string | Transform applied on ingest. |
unsigned | boolean | Allow uploads without a token. Default: false. |
Named Transformations
Save reusable transform presets and reference them with t_name.
/api/transformations🔐 Auth requiredList all named transformations.
/api/transformations🔐 Auth requiredCreate or upsert a named transformation.
/api/transformations/:name🔐 Auth requiredUpdate the transform string.
/api/transformations/:name🔐 Auth requiredDelete a named transformation.
# Create "thumbnail" preset
curl -X POST http://localhost:3000/api/transformations \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name":"thumbnail","transform_string":"w_200,h_200,c_fill,f_webp,q_80"}'
# Use it in any image URL
GET /img/t_thumbnail/photos/portrait.jpgVideo Transcoding
Async transcoding jobs for format conversion and processing.
transcode.complete webhook event./api/asset/:id/transcode🔐 Auth requiredEnqueue a transcoding job. Returns 202 Accepted.
/api/asset/:id/transcode-status🔐 Auth requiredList all transcode jobs for an asset.
/api/transcode-jobs/:jobId🔐 Auth requiredPoll a specific job status.
/api/asset/:id/sprite🔐 Auth requiredGenerate a sprite sheet of video thumbnails.
/api/asset/:id/frame🔐 Auth requiredExtract a single frame at a timestamp.
Job request body
| Parameter | Type | Description |
|---|---|---|
format | string | Output format: mp4 | webm | ogv | gif. |
width | number | Output width. |
height | number | Output height. |
quality | number | CRF quality (lower = better, 0–51). |
bitrate | string | e.g. 1500k or 2M. |
fps | number | Output frame rate. |
startOffset | number | Trim start in seconds. |
endOffset | number | Trim end in seconds. |
# Submit a transcoding job
curl -X POST http://localhost:3000/api/asset/uuid-here/transcode \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"format":"webm","width":1280,"quality":28}'
# Poll status
curl http://localhost:3000/api/transcode-jobs/<job-id> \
-H "Authorization: Bearer <token>"{
"jobId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"assetId": "550e8400-e29b-41d4-a716-446655440000",
"status": "done",
"outputUrl": "https://openinary.example.com/uploads/transcoded/f47ac10b.webm",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:45Z"
}Analytics
Storage, bandwidth, and transformation usage metrics.
/api/analytics/storage🔐 Auth requiredStorage breakdown by folder and MIME type.
/api/analytics/bandwidth🔐 Auth requiredRequest and bandwidth usage over time.
/api/analytics/transforms🔐 Auth requiredTop transformation presets by usage.
/api/analytics/requests🔐 Auth requiredRaw request log with path/method/status filters.
| Parameter | Type | Description |
|---|---|---|
days | number | Lookback window in days. Default: 30. |
path | string | (requests only) Filter by URL path prefix. |
method | string | (requests only) Filter by HTTP method. |
status | number | (requests only) Filter by HTTP status code. |
limit | number | Max rows. |
offset | number | Pagination offset. |
Webhooks
Receive HTTP POST callbacks on events.
/api/webhooks🔐 Auth requiredList webhooks.
/api/webhooks🔐 Auth requiredCreate a webhook.
/api/webhooks/:id🔐 Auth requiredUpdate URL, events, or enabled state.
/api/webhooks/:id🔐 Auth requiredDelete a webhook.
/api/webhooks/:id/deliveries🔐 Auth requiredList delivery history (last 50).
/api/webhooks/:id/test🔐 Auth requiredSend a test ping.
Available events
upload.completeasset.deletedasset.restoredasset.updatedtranscode.completemoderation.flaggedWebhook payload
{
"event": "upload.complete",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"asset_id": "550e8400-e29b-41d4-a716-446655440000",
"filename": "photo.jpg",
"folder": "marketing",
"url": "https://openinary.example.com/uploads/marketing/photo.jpg"
}
}Signature verification
Every delivery includes an X-Openinary-Signature header.
import { createHmac } from "crypto";
function verifyWebhook(body: string, signature: string, secret: string): boolean {
const expected = "sha256=" + createHmac("sha256", secret)
.update(body)
.digest("hex");
return signature === expected;
}
// Express example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-openinary-signature"] as string;
if (!verifyWebhook(req.body.toString(), sig, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body.toString());
console.log("Event:", payload.event);
res.sendStatus(200);
});Moderation
Review and approve or flag uploaded assets.
/api/moderation🔐 Auth requiredGet assets by moderation status with counts.
/api/asset/:id/moderation🔐 Auth requiredSet moderation status: approved | flagged | pending.
| Parameter | Type | Description |
|---|---|---|
status | approved | flagged | pending | Filter assets by status. Default: flagged. |
limit | number | Max assets to return. Default: 50. |
# Approve an asset
curl -X PATCH http://localhost:3000/api/asset/uuid/moderation \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"status":"approved"}'Users & API Keys
Manage users and programmatic API keys.
Users
/api/users🔐 Auth requiredList all users (admin only).
/api/users/:id🔐 Auth requiredUpdate role (admin only).
/api/users/:id🔐 Auth requiredDelete a user (admin only).
| Parameter | Type | Description |
|---|---|---|
role | admin | editor | viewer | User role. Admins can manage everything. Editors can upload. Viewers are read-only. |
API Keys
/api/api-keys🔐 Auth requiredList your API keys.
/api/api-keys🔐 Auth requiredCreate a new API key. The full key is only shown once.
/api/api-keys/:id🔐 Auth requiredRevoke an API key.
JavaScript SDK — Installation
The official typed SDK for Node.js and browsers.
npm install openinary
# or
pnpm add openinaryopeninary/upload-widget and openinary/media-library) require React ≥ 17 as a peer dependency.OpeninaryClient
Create a client instance and configure authentication.
import { OpeninaryClient } from "openinary";
const client = new OpeninaryClient({
baseUrl: "http://localhost:3000", // required
apiKey: "ok_yourkey", // API key or…
token: "eyJhbGci...", // …JWT token
});Constructor options
| Parameter | Type | Description |
|---|---|---|
baseUrlrequired | string | Base URL of your Openinary server. |
apiKey | string | API key for server-to-server calls. |
token | string | JWT token for browser-side use. |
fetch | function | Custom fetch implementation (for Node <18 or testing). |
Asset Methods
client.assets.*
const client = new OpeninaryClient({ baseUrl, apiKey });
// List assets
const assets = await client.assets.list({ folder: "marketing", limit: 50 });
// Upload a file (Node.js Buffer or browser File)
const asset = await client.assets.upload(file, {
folder: "products",
tags: ["hero", "2024"],
});
// Upload from URL
const asset = await client.assets.uploadFromUrl("https://example.com/img.jpg", {
folder: "imports",
});
// Upload from base64
const asset = await client.assets.uploadFromBase64(
"data:image/jpeg;base64,/9j/4AAQ...",
{ filename: "avatar.jpg", folder: "users" }
);
// Get single asset
const asset = await client.assets.get("550e8400-...");
// Update metadata
const updated = await client.assets.update("550e8400-...", {
filename: "new-name.jpg",
folder: "archived",
context: { author: "Alice", campaign: "summer" },
});
// Delete (soft)
await client.assets.delete("550e8400-...");
// Restore from trash
await client.assets.restore("550e8400-...");
// Search
const results = await client.assets.search("mountains", { folder: "travel" });
// Add / remove tags
await client.assets.addTags("uuid", ["featured", "sale"]);
await client.assets.removeTags("uuid", ["sale"]);
// Bulk operations
await client.assets.bulkDelete(["uuid-1", "uuid-2"]);
await client.assets.bulkMove(["uuid-1", "uuid-2"], "archived");Transform Helpers
Build transformation URLs without a round-trip.
// Build a URL with transform params
const url = client.assets.url(asset.file_key, {
width: 400,
height: 400,
crop: "fill",
format: "webp",
quality: 80,
});
// → http://localhost:3000/img/w_400,h_400,c_fill,f_webp,q_80/photos/img.jpg
// All supported TransformOptions fields:
type TransformOptions = {
width?: number; // w_
height?: number; // h_
crop?: string; // c_ (fill|fit|scale|thumb|pad|crop)
gravity?: string; // g_ (center|face|north|south…)
quality?: number; // q_
format?: string; // f_ (webp|avif|jpeg|png|gif|mp4…)
radius?: string | number; // r_ (px or "max" for circle)
effect?: string; // e_ (grayscale|blur|sharpen…)
background?: string; // b_
named?: string; // t_ (named preset)
dpr?: number; // dpr_
startOffset?: number; // so_ (video trim start)
endOffset?: number; // eo_ (video trim end)
fps?: number; // fps_
bitrate?: string; // br_
};
// Multi-stage transforms (chained with /)
const url = client.assets.buildTransformString([
{ width: 800, crop: "fill" },
{ effect: "grayscale", quality: 70 },
]);
// → w_800,c_fill/e_grayscale,q_70
// Get a signed URL (requires server call)
const { url, expires_at } = await client.assets.signedUrl("uuid", {
transforms: "w_400,c_fill",
expires: 3600,
});Webhook Methods
client.webhooks.*
// List webhooks
const hooks = await client.webhooks.list();
// Create
const hook = await client.webhooks.create({
url: "https://your-server.com/webhook",
events: ["upload.complete", "transcode.complete"],
});
console.log(hook.secret); // ← store securely, shown once
// Toggle enabled
await client.webhooks.update(hook.id, { enabled: false });
// Delivery history
const deliveries = await client.webhooks.deliveries(hook.id);
// Delete
await client.webhooks.delete(hook.id);Admin Methods
client.admin.* — require admin role.
// Users
const users = await client.admin.users.list();
const updated = await client.admin.users.updateRole(userId, "editor");
await client.admin.users.delete(userId);
// API Keys
const keys = await client.admin.apiKeys.list();
const created = await client.admin.apiKeys.create("CI pipeline");
console.log(created.key); // ← shown once
await client.admin.apiKeys.revoke(keyId);
// Named transformations
await client.admin.namedTransformations.create("avatar", "w_200,h_200,c_fill,r_max");
await client.admin.namedTransformations.delete("avatar");
// Upload presets
await client.admin.uploadPresets.create({
name: "strict_images",
allowed_formats: ["jpg","png","webp"],
max_size: 5 * 1024 * 1024,
unsigned: false,
});
// Analytics
const storage = await client.admin.analytics.storage();
const bandwidth = await client.admin.analytics.bandwidth({ days: 7 });Guide: Transform URLs
How to construct transformation URLs manually.
All transformation parameters follow the key_value format separated by commas. Multiple pipeline stages are separated by /.
// Helper to build transform strings in your own frontend
function buildTransformUrl(baseUrl: string, fileKey: string, transforms: string) {
return `${baseUrl}/img/${transforms}/${fileKey}`;
}
// Progressive enhancement: serve WebP with JPEG fallback
const webpUrl = buildTransformUrl(BASE, key, "w_800,f_webp,q_80");
const jpegUrl = buildTransformUrl(BASE, key, "w_800,q_80");
// <picture> tag
// <picture>
// <source srcSet={webpUrl} type="image/webp" />
// <img src={jpegUrl} alt="photo" />
// </picture>
// Responsive srcset
const srcset = [400, 800, 1200].map(
(w) => `${buildTransformUrl(BASE, key, `w_${w},f_webp,q_80`)} ${w}w`
).join(", ");Guide: Upload Widget
Drop-in React component for file uploads.
npm install openinaryimport { OpeninaryUploadWidget } from "openinary/upload-widget";
export function MyUploader() {
return (
<OpeninaryUploadWidget
uploadUrl="http://localhost:3000/api/upload"
authToken={userJwt}
folder="user-uploads"
maxFiles={10}
accept="image/*,video/mp4"
onSuccess={(assets) => {
console.log("Uploaded:", assets);
}}
onError={(err) => console.error(err)}
/>
);
}Props
| Parameter | Type | Description |
|---|---|---|
uploadUrlrequired | string | Server upload endpoint URL. |
authToken | string | JWT or API key for authenticated uploads. |
folder | string | Destination folder. |
maxFiles | number | Max simultaneous uploads. Default: unlimited. |
accept | string | MIME type filter for the file picker. |
onSuccess | function | Called with an array of uploaded Asset objects. |
onError | function | Called with an error message string. |
className | string | CSS class for the root element. |
Guide: Media Library Widget
Embeddable asset picker for selecting existing media.
import { OpeninaryMediaLibrary } from "openinary/media-library";
export function AssetPicker({ onSelect }: { onSelect: (url: string) => void }) {
return (
<OpeninaryMediaLibrary
baseUrl="http://localhost:3000"
authToken={userJwt}
mode="modal" // "modal" | "inline"
multiSelect={false}
onSelect={(assets) => onSelect(assets[0].url)}
onClose={() => {}}
/>
);
}Guide: Verifying Webhooks
How to validate incoming webhook signatures.
import { createHmac, timingSafeEqual } from "crypto";
/**
* Call this in your webhook handler to verify authenticity.
* @param rawBody The raw request body buffer (do NOT parse to JSON first)
* @param signature Value of the X-Openinary-Signature header
* @param secret Your webhook's signing secret
*/
function verifyWebhook(rawBody: Buffer, signature: string, secret: string): boolean {
if (!signature.startsWith("sha256=")) return false;
const expected = "sha256=" + createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
try {
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
} catch {
return false;
}
}
// Next.js App Router example
export async function POST(req: Request) {
const rawBody = Buffer.from(await req.arrayBuffer());
const sig = req.headers.get("x-openinary-signature") ?? "";
if (!verifyWebhook(rawBody, sig, process.env.WEBHOOK_SECRET!)) {
return new Response("Unauthorized", { status: 401 });
}
const payload = JSON.parse(rawBody.toString());
switch (payload.event) {
case "upload.complete":
await indexAsset(payload.data);
break;
case "transcode.complete":
await notifyUser(payload.data.job_id);
break;
}
return new Response(null, { status: 200 });
}timingSafeEqual (or equivalent constant-time comparison) to prevent timing attacks when comparing HMAC signatures.Guide: Video Transcoding
Async job workflow for video conversion.
import { OpeninaryClient } from "openinary";
const client = new OpeninaryClient({ baseUrl, apiKey });
// 1. Enqueue a transcoding job
const { job_id } = await client.assets.enqueueTranscode(assetId, {
format: "webm",
width: 1280,
quality: 28, // CRF — lower = better quality
bitrate: "2M",
});
// 2. Poll for completion
async function waitForTranscode(jobId: string, maxWait = 300_000) {
const start = Date.now();
while (Date.now() - start < maxWait) {
const job = await client.assets.getTranscodeJob(jobId);
if (job.status === "done") return job.outputUrl;
if (job.status === "failed") throw new Error(job.error);
await new Promise((r) => setTimeout(r, 3000));
}
throw new Error("Transcode timed out");
}
const outputUrl = await waitForTranscode(job_id);
console.log("Transcoded video:", outputUrl);
// 3. Alternative: listen for the webhook instead of polling
// Set up a webhook for "transcode.complete" and update your DB there.
// 4. Extract a frame (for thumbnails)
const frameUrl = `${baseUrl}/api/asset/${assetId}/frame?t=5.0`;
// 5. Sprite sheet for video preview
const spriteUrl = `${baseUrl}/api/asset/${assetId}/sprite?cols=5&rows=4&thumb_w=160`;