---
name: stow-media-storage
description: Use when integrating an app with the Stow media storage API — uploading, listing, or downloading media, storage quotas, API-key auth, or device pairing to obtain a key.
---

# Stow Media Storage — Integration Skill

Stow is media storage as an API: isolated per-account storage, quotas, and
private files served through short-lived signed links. Plain HTTPS + JSON.

- Base URL: `https://www.stow-io.com`
- Full machine reference: `https://www.stow-io.com/stow.txt` (fetch it for the complete spec)
- Auth: send `X-Stow-Key: $STOW_API_KEY` on every request. Keep the
  key server-side only; never embed it in frontend code.

## Get a key (device pairing — the user just clicks Approve)

1. `POST https://www.stow-io.com/api/pair/start` (no auth, no body)
   → `{deviceCode, userCode, verificationUrl}`. Stow labels the key itself.
2. Open `verificationUrl` in the user's browser for them if you can (macOS `open`,
   Linux `xdg-open`, Windows `start`), and print it as a fallback. The code is
   pre-filled — the user just logs in and clicks Approve.
3. Poll `POST https://www.stow-io.com/api/pair/claim` body `{"deviceCode":"..."}` every 3s
   until `{status:"approved", apiKey}`.
4. Save `apiKey` to the project's gitignored `.env` as `STOW_API_KEY`.
   Released once; a second claim returns `410`. Keep `deviceCode` secret.

## Upload flow (three steps, in order)

1. `POST https://www.stow-io.com/api/media/upload-url` body `{filename, sizeBytes, mimeType}`
   → `{assetId, uploadConfig.uploadUrl}`.
2. `PUT` the raw file bytes to `uploadConfig.uploadUrl` (NOT to the API).
3. `POST https://www.stow-io.com/api/media/confirm/:assetId` (required within ~15 min).

## Other endpoints

- `GET /api/media/files` → `[{assetId, filename, sizeBytes, mimeType, createdAt}]`
- `GET /api/media/files/:assetId/download-url` → `{downloadUrl}` (signed, ~10 min;
  store the assetId, never the URL). This is the **full-resolution original** — use it
  for downloads or non-image files. To **display an image** in a UI, use `transform-url`
  (next section), not this.
- `DELETE /api/media/files/:assetId`
- `GET /api/media/quota` → `{tier, quotaBytes, usedBytes, ...}`

## Transform an image (on the fly — no extra storage)

**When to use:** this is the **default way to show any stored image in a UI** — galleries,
thumbnails, list vs. detail views, retina/responsive variants, avatars, social/OG previews.
Point your `<img src>` at the returned `url`. It serves a right-sized, optimized image on the
fly from the image CDN (WebP/AVIF, lower quality → smaller payloads, faster pages), applied on
delivery so you store the original once and never keep resized copies. Reach for this instead of
uploading the same image at multiple sizes — and instead of wiring an `<img>` to `download-url`,
which ships the full-resolution original into a small UI slot.

`GET https://www.stow-io.com/api/media/files/:assetId/transform-url?w=&h=&crop=&format=&q=`
→ `{url}` — a signed image URL you fetch or embed directly. The transform is applied
on delivery; it does not create or store a new file (no quota cost).

- `w`, `h` — target width/height in px (required, 1–4000)
- `crop` — `fit` (scale within w×h, keeps aspect; default) or `fill` (scale + crop to exactly w×h)
- `format` — `jpg` (default), `webp`, `avif`, or `png`
- `q` — quality 1–100 (default 82)
- `blur` — 0–100 (default 0)

Only works on image assets. Example — a 400×400 WebP thumbnail:
`GET https://www.stow-io.com/api/media/files/abc123/transform-url?w=400&h=400&crop=fill&format=webp&q=80`

## Transcode a video (async)

**When to use:** after uploading a source video, produce lower resolutions so each viewer
downloads a size that fits their device and connection (phones / slow links → 480p or 720p;
desktop / TV → 1080p or 4K) and so playback works across devices. Reach for it whenever a video
is large or high-resolution and will be watched on varied clients. Unlike image transforms this is
**asynchronous** (poll for completion) and each resolution is stored as a real file (counts against
quota) — so transcode once per source, then serve the rendition that fits.

`POST https://www.stow-io.com/api/media/files/:assetId/transcode`
body `{"resolutions":["1080p","720p","480p","2160p"]}` (optional; defaults to all four)
→ `{"groupId","status":"processing","outputDir","resolutions"}`. Video assets only.

Each resolution is written to its own folder — `<outputDir>/1080p/`, `/720p/`, … Resolutions
above the source height are skipped (no upscaling). (Optional `"hls":true` requests an adaptive
HLS stream in `<outputDir>/hls/` — best-effort where the platform supports it.)

Poll until done:
`GET https://www.stow-io.com/api/media/transcode/:groupId`
→ `{"status":"processing"|"done"|"error","renditions":[{"resolution","assetId","path"}],"hls":{"directory"}}`.
When `done`, each rendition is a normal file — fetch it with its `assetId` via the download-url
endpoint. Transcoding runs in the background; poll every few seconds.

## Handle these statuses

- `401` invalid/revoked key · `403` endpoint not allowed for API keys
- `400` bad filename (letters, digits, spaces, `. _ - ( )`; no slashes, no leading
  dot, max 128 chars) · `409` filename exists (no overwrites — pick a new name)
- `413` over quota or file too large · `429` rate limited (back off)

## Rules

- A credential only ever sees its own account's files — no cross-account access.
- File bytes never go through the API (64KB JSON cap); only to `uploadUrl`.
- Unconfirmed uploads are reclaimed after ~15 minutes.
