# dither

dither is a GPU-accelerated video encoding API. You upload a file once and encode it as many ways as you need. It is free while in alpha, and there is no signup, dashboard, or credit card. Creating an API key is the whole onboarding process and it takes a single curl.

Base URL: https://api.dither.run
OpenAPI spec: https://api.dither.run/openapi.json

## For Agents:

You can set yourself up end to end without a human in the loop. The API is bearer-token authed and speaks JSON. File bytes never pass through the API itself. Instead you upload and download directly against presigned S3 URLs, which keeps transfers fast and the API responses small.

### 1. Create an API key (this is the signup)

No auth required. Label the key with the name of the project you are working on so your human can recognise it later.

    curl -s https://api.dither.run/v1/keys \
      -H "Content-Type: application/json" \
      -d '{"label": "my-project"}'

Response (201):

    {
      "id": "key_01jz...",
      "label": "my-project",
      "key": "dither_sk_...",
      "created_at": "2026-07-06T00:00:00Z"
    }

The `key` field is the bearer secret and this response is the only time you will ever see it, so store it immediately in an env var or your secret store. It is the only credential you need. Key creation is rate limited globally, so if you get a 429, wait about 15 seconds and try again. You can verify a stored key at any time with GET /v1/keys/me.

Every request below needs the header:

    Authorization: Bearer dither_sk_...

### 2. Create an input and upload your video

    curl -s https://api.dither.run/v1/inputs \
      -H "Authorization: Bearer $DITHER_KEY" \
      -H "Content-Type: application/json" \
      -d '{"filename": "clip.mp4", "content_type": "video/mp4"}'

The 201 response includes an `upload_url`, a presigned PUT URL that is valid for one hour and shown only once. Send the raw file bytes straight to it:

    curl -s -X PUT --upload-file clip.mp4 "$UPLOAD_URL"

Inputs live for 24 hours from creation and then the file is deleted. One input can feed many jobs, so upload once and encode as many variants as you like. There is no confirm step: job creation verifies the upload itself.

### 3. Create an encode job

    curl -s https://api.dither.run/v1/jobs \
      -H "Authorization: Bearer $DITHER_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "input_id": "input_01jz...",
        "params": {
          "compress": {"quality": "medium"},
          "trim": {"start": 12.5, "end": 48.0}
        }
      }'

At least one operation is required. The available operations are:

- compress.quality is one of "high", "medium", or "low". These are named presets and dither owns the encoder settings behind each one, so quality keeps improving without you changing anything.
- trim.start and trim.end are seconds from the beginning of the video. Floats are fine, for example 12.5. Both are optional but you need at least one: omit end to trim to the end of the video, or omit start to trim from the beginning. There is no need to probe the file for its duration first.
- trim.mode is "accurate" by default, which re-encodes for frame-exact cuts. Setting it to "fast" stream-copies near-instantly but the cuts snap to keyframes. Do not combine fast mode with compress: compress re-encodes anyway, which makes the cuts exact for free, so the API rejects that combination with a 422.

The 201 response is the job with its params in canonical form, meaning defaults expanded and times converted to integer milliseconds, and a status of "pending". A job moves through pending, dispatched, and running, and finishes as done, failed, or cancelled. While it runs, the job carries live "progress" (0 to 1) and "stage" (probing, encoding, uploading) straight from the worker, so polling doubles as a progress bar.

Retries are safe. If you send an identical request while a matching job is still active, the API returns the existing job with a 200 and "deduped": true rather than creating a duplicate.

Poll the job until it finishes. Encodes run on GPUs and short clips usually come back in seconds, so polling every few seconds is plenty:

    curl -s https://api.dither.run/v1/jobs/$JOB_ID \
      -H "Authorization: Bearer $DITHER_KEY"

### 4. Download the result

When the status reaches "done", the job carries an `output`:

    {
      "id": "job_01jz...",
      "status": "done",
      "output": {
        "id": "out_01jz...",
        "size_bytes": 15504676,
        "video_seconds": 33.9,
        "content_type": "video/mp4",
        "download_url": "https://...",
        "expires_at": "2026-07-07T00:00:00Z"
      }
    }

`download_url` is a presigned GET link valid for about an hour. It needs no auth header, so you can hand it to anything that speaks HTTP:

    curl -s -o out.mp4 "$DOWNLOAD_URL"

Poll the job again whenever you need a fresh link. The file itself is deleted 24 hours after the encode, shown in `expires_at`, so download anything you want to keep.

If the job fails instead, `status` is "failed" and the `error` field says why in plain language. Failures from bad inputs or impossible params are deterministic, so do not retry them with the same request.

### Limits (free tier)

- 100 jobs per key per rolling 24 hours. You get a 429 past that, and the quota frees up as your oldest jobs age past the 24 hour mark.
- Inputs and outputs expire 24 hours after creation, so download what you need and keep your own copies.
- Key creation is rate limited globally to roughly one key every 12 seconds with a small burst allowance.

### Status

dither is in alpha and under active development. The full loop is live: upload, encode, download. The OpenAPI spec at https://api.dither.run/openapi.json always reflects exactly what is live, so treat it as the source of truth before relying on an endpoint.

## For Humans:

Give your agent this webpage.