# Stow — private media storage API > Stow is media storage as an API for developers. Every account gets isolated, > private storage with a quota; files are only readable through short-lived > signed links. Integration is plain HTTPS + JSON from any language. This file > is the complete integration reference for AI agents and automated tools. ## Configuration - `STOW_API_BASE` = https://www.stow-io.com All endpoint paths below are relative to this base URL. - `STOW_API_KEY` — a bearer credential in the format `sk_.`, created by a human in the web dashboard under "API keys", or obtained by an agent via device pairing (below). Shown once at creation. Keep it server-side only; never embed it in frontend code. ## Zero-config onboarding for agents (device pairing) If no `STOW_API_KEY` is configured, an agent can obtain one itself. The human's only step is logging in and clicking Approve. 1. `POST https://www.stow-io.com/api/pair/start` (no auth, no body). Returns `{"deviceCode", "userCode", "verificationUrl", "expiresInSeconds": 600, "pollIntervalSeconds": 3}`. The key is labeled automatically by Stow — you don't name it. 2. Open the `verificationUrl` in the human's browser for them if you can — run the OS opener (macOS `open `, Linux `xdg-open `, Windows `start `) — and print it too as a fallback. The pairing code is pre-filled from the URL, so the human just logs in and clicks Approve. 3. Poll `POST https://www.stow-io.com/api/pair/claim` with body `{"deviceCode": "..."}` every few seconds. `{"status":"pending"}` means keep waiting. 4. `{"status":"approved","apiKey":"sk_..."}` — store the key in the project's secret store (e.g. a gitignored `.env`). It is released exactly once; a second claim returns `410`. 5. `410` — the pairing expired (10 min) or was already claimed; start over. The `deviceCode` is the claim secret — never show or log it. The `userCode` is the short human-facing code. ## Authentication Send the key in the X-Stow-Key header on every request: X-Stow-Key: sk_. - A key acts as its owner's account for media operations only. - Keys are rejected with 403 on key-management and admin endpoints by design — a leaked key cannot mint more keys or gain admin powers. - Revoked keys get 401 on the next call. There is no key refresh; a human creates a new one in the dashboard. - Browser apps can alternatively sign users in as members and use the member token on the same endpoints. ## Endpoints Prefix every path with `STOW_API_BASE` (https://www.stow-io.com). All require the X-Stow-Key header. Errors are always `{"error": ""}`. - `POST /api/media/upload-url` — reserve an upload slot (quota checked here). Body: `{"filename": string, "sizeBytes": number, "mimeType": string}`. Returns: `{"assetId": string, "uploadConfig": {"uploadUrl": string}}`. - `POST /api/media/confirm/:assetId` — finalize an upload. Required; without it the slot expires in ~15 minutes and the file is reclaimed. - `GET /api/media/files` — list files. Returns: `[{"assetId", "filename", "sizeBytes", "mimeType", "createdAt"}]`. - `GET /api/media/files/:assetId/download-url` — get a signed read link to the **full-resolution original**. Returns: `{"downloadUrl": string, "expiresInSeconds": 600}`. Use this for downloads or non-image files — NOT for displaying images in a UI (use `transform-url` for that; see below). - `GET /api/media/files/:assetId/transform-url?w=&h=&crop=&format=&q=` — on-the-fly image transform (resize/crop/format/quality), applied on delivery with no stored copy (no quota cost). Images only. Params: `w`,`h` px (required, 1–4000); `crop` `fit`|`fill` (default `fit`); `format` `jpg`|`webp`|`avif`|`png` (default `jpg`); `q` 1–100 (default 82); `blur` 0–100 (default 0). Returns: `{"url": string, "expiresInSeconds": 600}`. - `POST /api/media/files/:assetId/transcode` — async video transcode. Body `{"resolutions":["1080p","720p","480p","2160p"]}` (optional; defaults to all four). Video assets only. Writes one file per resolution to `//` (resolutions above the source are skipped). Optional `"hls":true` requests an adaptive HLS stream in `/hls/` (best-effort). Returns `{"groupId","status":"processing","outputDir","resolutions"}`. - `GET /api/media/transcode/:groupId` — poll a transcode. Returns `{"status":"processing"|"done"|"error","renditions":[{"resolution","assetId","path"}],"hls":{"directory"}}`. When done, each rendition is a normal file — fetch via its `assetId` + the download-url endpoint. - `DELETE /api/media/files/:assetId` — delete a file and free its quota. - `GET /api/media/quota` — usage. Returns: `{"tier", "quotaBytes", "maxFileSizeBytes", "usedBytes", "reservedBytes"}`. ## Upload flow (three steps, strictly in order) 1. `POST /api/media/upload-url` with filename, sizeBytes, mimeType → returns `assetId` and `uploadConfig.uploadUrl`. 2. `PUT` the raw file bytes to `uploadConfig.uploadUrl` with the file's Content-Type. Do NOT send file bytes to the Stow API itself — its JSON bodies are capped at ~64KB. 3. `POST /api/media/confirm/:assetId` — quota is finalized from the real stored size, not the declared one. ## Displaying vs. downloading (important) Files are private, served only through short-lived signed links. There are two ways to read a file — pick the right one: - **Displaying an image in a UI** (gallery, thumbnail, avatar, preview, ``): use `transform-url`. It serves a right-sized, optimized image on the fly from the image CDN — no stored copy, no extra quota — e.g. `GET /api/media/files/:assetId/transform-url?w=400&h=400&crop=fill&format=webp&q=80`, then use the returned `url` directly as the image source. This is what the gallery demo on the Stow landing page does. Do NOT wire an `` to the raw `download-url`. - **Downloading the original file** (a download button, non-image assets, processing the exact bytes): use `download-url` for the full-resolution original. Both links expire (~10 min). Store the `assetId` and mint a fresh link on demand; never persist or cache the signed URLs. ## Error handling - `401` — invalid or revoked credentials. Re-authenticate or ask a human for a new key. - `403` — endpoint not allowed for API keys (key management, admin). Use the web dashboard. - `400` — invalid filename. Allowed: letters, digits, spaces, `. _ - ( )`; no slashes, no leading dot, max 128 characters. - `409` — a file with that name already exists. Overwrites are not supported; retrying the same name will never succeed. Pick a new name or delete first. - `413` — over storage quota or per-file size limit. Check `GET /api/media/quota`. ## Rules - One account's credential can only ever see that account's files. There is no path parameter and no cross-account access anywhere in the API. - Filenames are unique per account. - Uploads not confirmed within ~15 minutes are discarded and their reserved quota is released automatically.