# The HTTP API av-club is a Bun HTTP service. It exposes as much of sharp, ffmpeg and ffprobe as can be exposed safely, and it keeps each tool's own vocabulary: sharp operations are named what sharp names them, ffmpeg options are spelled the way ffmpeg spells them. If you know the tool, you know this API. What it does **not** let you do is name a file. See `docs/SECURITY.md` — that single restriction is what makes the rest of the surface safe to open up. ## Status | Endpoint | | |---|---| | `POST /v1/assets`, `GET /v1/assets/:id`, `GET /v1/assets/:id/content`, `DELETE /v1/assets/:id` | implemented | | `GET /v1/assets/:id/probe` | implemented | | `POST /v1/sharp` | implemented | | `POST /v1/ffmpeg` | implemented — both asset and path modes | | `GET /health` | implemented | ## The shape of everything **Nothing takes a path or a URL.** You upload bytes, you get an **asset id**, and every subsequent operation refers to that id. Operations produce new assets and return new ids; inputs are never modified. ``` upload ──> as_a1b2… ──sharp──> as_c3d4… ──download ``` Ids look like `as_` followed by 32 hex characters. ## Authentication Every endpoint except `/health` requires: ``` Authorization: Bearer ``` The token is `AV_CLUB_TOKEN` in the service's environment. There are no user accounts — this is an internal service between things we run. ## Errors One envelope, always, so a client can parse a failure the same way it parses a success: ```json { "error": { "message": "resize.width must be an integer between 1 and 16383", "type": "invalid_request", "param": "pipeline[1].resize.width" } } ``` | `type` | Status | Meaning | |---|---|---| | `invalid_request` | 400 | The request is malformed or an argument is out of range. `param` says where. | | `unauthorized` | 401 | Missing or wrong bearer token. | | `not_found` | 404 | No such asset. | | `payload_too_large` | 413 | Upload exceeds the size cap. | | `unprocessable` | 422 | Valid request, but the file could not be processed — corrupt, or not the format it claimed. | | `timeout` | 504 | The job exceeded its wall clock limit and was killed. | | `server_error` | 500 | Our fault. | Messages are written to be readable by a person, because the thing on the other end is often relaying them to one. --- ## Assets ### `POST /v1/assets` `multipart/form-data` with a `file` field. ```bash curl -X POST http://localhost:3020/v1/assets \ -H "Authorization: Bearer $AV_CLUB_TOKEN" \ -F file=@photo.heic ``` ```json { "id": "as_9f86d081884c7d659a2feaa0c55ad015", "bytes": 2847293, "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "kind": "image", "format": "heif", "width": 4032, "height": 3024, "createdAt": "2026-08-20T01:40:00.000Z" } ``` `kind` is one of `image`, `video`, `audio`. It is determined by **inspecting the bytes**, never from the filename or the browser's `Content-Type` — see `docs/SECURITY.md`. The upload is rejected with `422` if it is not media we can decode, and with `413` if it exceeds `AV_CLUB_MAX_UPLOAD_BYTES`. ### `GET /v1/assets/:id` The same JSON as above. ### `GET /v1/assets/:id/content` The bytes, with the right `Content-Type`. Supports `Range` requests, which matters because a browser will not scrub — often will not even play — a `