Skip to content

DocsGet started

Quickstart

Create an API key, send a JSON request, poll the job until it ends and download its output.

Send a JSON request with an input URL and the output settings you need. Videxo returns a job ID. Poll the job or receive a webhook, then download the completed files. Already have an ffmpeg command? Send it as it is.

Videxo is a development preview. Jobs run, store their outputs and send signed webhooks. Uploads up to 100 MB and signed output downloads are available. Sign in with email, GitHub or a passkey. Payments are not connected yet, so credit packs and paid plans cannot be bought.

Before you start#

  • An account. Sign up with your email; new accounts start on Pay as you go with no card, and your personal organization gets 500 free credits once, valid 30 days.
  • curl and jq (opens in a new tab) for the shell steps, or Node 18 or newer for the JavaScript steps.

The steps below are one sequence: each shell step uses the variables the step before it set, and the JavaScript steps go into one file, quickstart.mjs, that you run with node quickstart.mjs.

1. Create an API key#

In the dashboard, open API keys and choose Create key. Name it after where it runs, such as production-worker. The key is shown once: copy it into your secret store, then export it with the API's address:

bash
export VIDEXO_API_URL="https://api.videxo.io"
export VIDEXO_API_KEY="<your key>"

Every example reads these two variables. Check that the key works. The response names your account and its plan:

bash
curl "$VIDEXO_API_URL/v1/me" \
  -H "Authorization: Bearer $VIDEXO_API_KEY"

2. Create a job#

POST /v1/transcode makes one video from one input. input is an http or https URL, or a completed private upload. This one is our 15-second sample video (https://videxo.io/samples/talk.mp4, public domain), so the request runs as shown. preset names a set of settings (Presets), here an MP4 for the web within 1280 by 720. The ffmpeg tab shows the command the request runs.

The Idempotency-Key header makes the request safe to repeat: if the network fails before you see the response, send the same request with the same key and you get the same job instead of a second one (Idempotency).

bash
idempotency_key="quickstart-$(date +%s)"
job=$(curl --fail-with-body -sS "$VIDEXO_API_URL/v1/transcode" \
  -H "Authorization: Bearer $VIDEXO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $idempotency_key" \
  -d '{
  "input": "https://videxo.io/samples/talk.mp4",
  "preset": "web-720p",
  "filename": "talk-720p.mp4",
  "metadata": {
    "order": "1234"
  }
}')
job_id=$(printf '%s' "$job" | jq -r .id)
echo "$job_id"

The API returns HTTP 202 and a queued job. The shell step saves its id in job_id for the next step. The response also includes your normalized request (spec) and the equivalent ffmpeg command:

JSON
{
  "id": "job_3k9x0c2mfq8w1n7r5t4b6v2z",
  "state": "queued",
  "kind": "operations",
  "operation": "transcode",
  "spec": {
    "inputs": {
      "source": {
        "url": "https://videxo.io/samples/talk.mp4"
      }
    },
    "outputs": [
      {
        "name": "video",
        "type": "video",
        "preset": "web-720p",
        "input": "source",
        "filename": "talk-720p.mp4",
        "format": "mp4",
        "resize": {
          "width": 1280,
          "height": 720,
          "fit": "contain",
          "enlarge": false
        },
        "video": {
          "codec": "h264",
          "crf": 23,
          "encoder_preset": "veryfast",
          "pixel_format": "yuv420p",
          "faststart": true
        },
        "audio": {
          "codec": "aac",
          "bitrate": "128k"
        }
      }
    ]
  },
  "command": "-i {{in_source}} -map 0:v:0 -vf \"scale='min(iw,1280)':'min(ih,720)':force_original_aspect_ratio=decrease:force_divisible_by=2\" -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -map 0:a:0? -c:a aac -b:a 128k -movflags +faststart {{out_video}}",
  "input_files": {
    "in_source": "https://videxo.io/samples/talk.mp4"
  },
  "output_files": {
    "out_video": "talk-720p.mp4"
  },
  "ffmpeg_command": "ffmpeg -i https://videxo.io/samples/talk.mp4 -map 0:v:0 -vf 'scale='\\''min(iw,1280)'\\'':'\\''min(ih,720)'\\'':force_original_aspect_ratio=decrease:force_divisible_by=2' -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart talk-720p.mp4",
  "steps": [
    {
      "tool": "ffmpeg",
      "inputs": [
        "source"
      ],
      "outputs": [
        "video"
      ],
      "command": "ffmpeg -i https://videxo.io/samples/talk.mp4 -map 0:v:0 -vf 'scale='\\''min(iw,1280)'\\'':'\\''min(ih,720)'\\'':force_original_aspect_ratio=decrease:force_divisible_by=2' -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart talk-720p.mp4"
    }
  ],
  "webhook_url": null,
  "metadata": {
    "order": "1234"
  },
  "progress": null,
  "error": null,
  "outputs": [],
  "usage": null,
  "created_at": "2026-11-04T12:00:00.000Z",
  "started_at": null,
  "finished_at": null
}

A request with an invalid field returns 400 invalid_request with a pointer to each field to change, and no job is created (Error codes).

3. Poll the job#

Read the job by its ID until its state is succeeded, failed or cancelled. Wait a few seconds between requests, and read the job only from a successful response: after 429 rate_limited or a 5xx response, try again later (after Retry-After when it is sent); any other error means stop and read its code.

bash
# Ask every 5 seconds until the job ends. A 429, 5xx or dropped connection is asked again after
# Retry-After (or a pause that doubles, up to a minute); any other error stops with its body.
wait_for_job() {
  body=$(mktemp); headers=$(mktemp); wait=5
  while true; do
    status=$(curl -sS -o "$body" -D "$headers" -w '%{http_code}' "$VIDEXO_API_URL/v1/jobs/$1" \
      -H "Authorization: Bearer $VIDEXO_API_KEY")
    if [ "$status" -ge 200 ] && [ "$status" -lt 300 ]; then
      case "$(jq -r .state "$body")" in
        succeeded|failed|cancelled) cat "$body"; rm -f "$body" "$headers"; return 0 ;;
      esac
      wait=5
    elif [ "$status" = 429 ] || [ "$status" -ge 500 ] || [ "$status" = 000 ]; then
      retry=$(grep -i '^retry-after:' "$headers" | tr -dc '0-9')
      if [ -n "$retry" ]; then wait=$retry; else wait=$((wait * 2 > 60 ? 60 : wait * 2)); fi
    else
      cat "$body" >&2; rm -f "$body" "$headers"; return 1
    fi
    sleep "$wait"
  done
}

# $job_id is the id step 2 saved.
job=$(wait_for_job "$job_id") && printf '%s' "$job" | jq '{state, error}'

Each request counts against your plan's rate limit. Polling every 5 seconds uses 12 requests a minute. A webhook tells you when the job ends instead.

4. Download the output#

A job that succeeded lists each output with its name, file name, size, SHA-256 and the time it expires, and its usage in seconds, bytes and credits:

JSON
{
  "id": "job_3k9x0c2mfq8w1n7r5t4b6v2z",
  "state": "succeeded",
  "kind": "operations",
  "operation": "transcode",
  "spec": {
    "inputs": {
      "source": {
        "url": "https://videxo.io/samples/talk.mp4"
      }
    },
    "outputs": [
      {
        "name": "video",
        "type": "video",
        "preset": "web-720p",
        "input": "source",
        "filename": "talk-720p.mp4",
        "format": "mp4",
        "resize": {
          "width": 1280,
          "height": 720,
          "fit": "contain",
          "enlarge": false
        },
        "video": {
          "codec": "h264",
          "crf": 23,
          "encoder_preset": "veryfast",
          "pixel_format": "yuv420p",
          "faststart": true
        },
        "audio": {
          "codec": "aac",
          "bitrate": "128k"
        }
      }
    ]
  },
  "command": "-i {{in_source}} -map 0:v:0 -vf \"scale='min(iw,1280)':'min(ih,720)':force_original_aspect_ratio=decrease:force_divisible_by=2\" -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -map 0:a:0? -c:a aac -b:a 128k -movflags +faststart {{out_video}}",
  "input_files": {
    "in_source": "https://videxo.io/samples/talk.mp4"
  },
  "output_files": {
    "out_video": "talk-720p.mp4"
  },
  "ffmpeg_command": "ffmpeg -i https://videxo.io/samples/talk.mp4 -map 0:v:0 -vf 'scale='\\''min(iw,1280)'\\'':'\\''min(ih,720)'\\'':force_original_aspect_ratio=decrease:force_divisible_by=2' -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart talk-720p.mp4",
  "steps": [
    {
      "tool": "ffmpeg",
      "inputs": [
        "source"
      ],
      "outputs": [
        "video"
      ],
      "command": "ffmpeg -i https://videxo.io/samples/talk.mp4 -map 0:v:0 -vf 'scale='\\''min(iw,1280)'\\'':'\\''min(ih,720)'\\'':force_original_aspect_ratio=decrease:force_divisible_by=2' -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -map '0:a:0?' -c:a aac -b:a 128k -movflags +faststart talk-720p.mp4"
    }
  ],
  "webhook_url": null,
  "metadata": {
    "order": "1234"
  },
  "progress": 1,
  "error": null,
  "outputs": [
    {
      "alias": "out_video",
      "name": "video",
      "filename": "talk-720p.mp4",
      "size_bytes": 300000000,
      "sha256": "9f2c4e1a7b3d5f60c8e2a4b6d8f0e1c3a5b7d9f1e3c5a7b9d1f3e5c7a9b1d3f5",
      "content_type": "video/mp4",
      "expires_at": "2026-11-05T12:01:09.000Z",
      "download_url": "https://api.videxo.io/v1/files/job_3k9x0c2mfq8w1n7r5t4b6v2z/out_video?account=acc_example&expires=1793797269&signature=example",
      "download_url_expires_at": "2026-11-04T13:01:09.000Z",
      "deleted_at": null
    }
  ],
  "usage": {
    "seconds": 61,
    "input_bytes": 1200000000,
    "output_bytes": 300000000,
    "credits": 45,
    "bill": {
      "lines": [
        {
          "kind": "video",
          "seconds": 300,
          "per_minute": 6,
          "multiplier": 1,
          "credits": 30
        }
      ],
      "data": {
        "input_bytes": 1200000000,
        "output_bytes": 300000000,
        "credits": 15
      },
      "processing": {
        "seconds": 61,
        "included_seconds": 900,
        "credits": 0
      }
    }
  },
  "created_at": "2026-11-04T12:00:00.000Z",
  "started_at": "2026-11-04T12:00:04.000Z",
  "finished_at": "2026-11-04T12:01:09.000Z"
}

Download an output from its download_url, without your API key:

bash
curl --fail-with-body -sS "$(printf '%s' "$job" | jq -r '.outputs[0].download_url')" -o talk-720p.mp4

A link lasts up to one hour, and never past the output's retention. Read the job again for a fresh link. Files covers range requests, private input uploads up to 100 MB and expiry.

A failed job has an error with a code and a message instead of outputs. Jobs and their states explains every state and every error code.

Several outputs in one job#

POST /v1/jobs takes named inputs and a list of outputs. Each output reads an input and has its own type, preset and options. A job reads each input once, so this costs less than one job per output:

bash
curl "$VIDEXO_API_URL/v1/jobs" \
  -H "Authorization: Bearer $VIDEXO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "inputs": {
    "talk": "https://videxo.io/samples/talk.mp4"
  },
  "outputs": [
    {
      "name": "web",
      "preset": "web-720p"
    },
    {
      "name": "poster",
      "preset": "poster"
    }
  ],
  "metadata": {
    "order": "1234"
  }
}'

Operations lists every output type and option; the guides show them by task, starting with Compress and convert.

Next#