Replit + reelsdk

Caption videos from a Replit app without slowing it down

Replit can run ffmpeg once you add it as a Nix system dependency. The catch is where it runs: on the default Autoscale deployment the encode shares your app's instance with web traffic, and the instance stays awake and bills compute units while it renders. reelsdk gives each job its own machine, ready in about 1.9 s, and burns word-by-word captions into a 30 s clip in 30 to 43 s for $0.02 per output minute.

  • Each job gets its own machine, ready in 1.87 s at the median
  • Transcription, fonts and word timing come with the call
  • A signed webhook tells your app when the mp4 is ready
What Node prints on Replit before ffmpeg is installed

Error: spawn ffmpeg ENOENT

errno: -2, code: 'ENOENT', syscall: 'spawn ffmpeg', path: 'ffmpeg'

Source: Node.js child_process

The same job as one call to reelsdk

const REELSDK = "https://api.reelsdk.com";

const res = await fetch(`${REELSDK}/v1/captions?wait=0`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.REELSDK_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ video_url, style: "bold", webhook_url }),
});
const { job } = await res.json();
202 with job.id right away, then a signed webhook with the mp4, SRT, VTT and ASS links

Why ffmpeg breaks here

ffmpeg runs on Replit. It just runs on your app's machine

Replit apps run on Linux, so ffmpeg itself isn't the problem: add it as a Nix system dependency and spawn ffmpeg ENOENT goes away. The problem is the machine. A deployment has a fixed amount of vCPU and RAM, and an encode will take all of it, so every other request waits while the video renders.

On Autoscale, the default deployment type, you pay while requests are being served: compute units for CPU and memory, plus a fee per million requests. In raw compute a render is cheap. What costs you is everything around it: a busy instance while users wait, a transcription model, caption fonts and a TTS service for captions and voiceover, or a Reserved VM sized for your busiest hour.

Rendering on Replit next to reelsdk

Rendering on Replit next to reelsdk
TopicOn your Replit deploymentreelsdk
Where the encode runsOn your app's instance, next to web trafficA fresh machine per job, ready in 1.87 s at the median
BillingAutoscale compute units while a request is served$0.02 per output minute for captions, no subscription
Installing ffmpegA Nix system dependencyffmpeg 8.1 already there, raw arguments through POST /v1/ffmpeg
CaptionsYour own transcription model, fonts and timingTranscription and word-by-word timing, plus SRT, VTT and ASS
Fixed capacityReserved VM from $15 a month for a shared 0.5 vCPU and 2 GB3 jobs at once per key, 500 a day

Replit limits from Replit deployment pricing, checked October 2026.

Set it up

Two secrets, one prompt, one raw-body route

You need a reelsdk key, its webhook secret and the public address of your published app. Replit Agent writes the routes from the prompt below, and the snippets show what to expect so you can check its work.

  1. Add the secrets

    Create a key in the reelsdk dashboard. Open the Secrets tool in your Replit App and add three with New Secret. PUBLIC_URL is your published address, for example https://your-app.replit.app, so reelsdk knows where to send the webhook. Secrets reach your code as environment variables and carry over to Autoscale and Reserved VM deployments.

    Replit secrets
    REELSDK_KEY
    rk_live_...
    REELSDK_WEBHOOK_SECRET
    whsec_...
    PUBLIC_URL
    https://your-app.replit.app
  2. Give Replit Agent the prompt

    It adds a captions route, a webhook route and a page with a URL field and a player. The prompt tells the Agent not to install ffmpeg or a transcription model.

    Prompt for Replit Agent
    Add a "Caption this video" feature to this Express app using reelsdk (https://reelsdk.com). Don't install ffmpeg or any transcription model.
    
    1. Read REELSDK_KEY, REELSDK_WEBHOOK_SECRET and PUBLIC_URL from Secrets (process.env).
    2. Add POST /api/captions. It takes { video_url } and POSTs to https://api.reelsdk.com/v1/captions?wait=0 with the header "Authorization: Bearer REELSDK_KEY" and the JSON { "video_url": video_url, "style": "bold", "webhook_url": PUBLIC_URL + "/webhooks/reelsdk" }. The response is JSON { job } with HTTP 202. Save job.id with status "queued" in the database.
    3. Add POST /webhooks/reelsdk using express.raw({ type: "*/*" }) so the body stays raw. Verify the Reel-Signature header (t=<unix seconds>,v1=<hex>): HMAC-SHA256 of "<t>.<raw body>" keyed with REELSDK_WEBHOOK_SECRET, compared with crypto.timingSafeEqual. Return 401 if it fails or t is more than 300 seconds old. Then save job.status and job.outputs.video for job.id.
    4. Add a page with a video URL field, a Caption button, and a player that shows the result when the status is "succeeded".
  3. Check the captions route

    It posts the video URL to POST /v1/captions?wait=0 with your webhook_url, so reelsdk answers with a 202 and the job id in well under a second. Your instance is free again while the captions render, 30 to 43 s for a 30 s clip. If reelsdk can't be reached, the route answers with a 502 instead of hanging.

    index.js
    const crypto = require("node:crypto");
    const express = require("express");
    
    const app = express();
    const REELSDK = "https://api.reelsdk.com";
    
    app.post("/api/captions", express.json(), async (req, res) => {
      try {
        const response = await fetch(`${REELSDK}/v1/captions?wait=0`, {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.REELSDK_KEY}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify({
            video_url: req.body.video_url,
            style: "bold",
            webhook_url: `${process.env.PUBLIC_URL}/webhooks/reelsdk`,
          }),
        });
    
        const { job, error } = await response.json();
        if (!response.ok) return res.status(response.status).json({ error });
        res.status(202).json({ id: job.id, status: job.status });
      } catch (error) {
        console.error("reelsdk request failed", error);
        res.status(502).json({ error: "render service unreachable" });
      }
    });
  4. Check the webhook route

    The signature is an HMAC-SHA256 of the exact bytes reelsdk sent, so this route uses express.raw and must not sit behind an app-wide express.json(). It checks Reel-Signature with crypto.timingSafeEqual, rejects timestamps more than 300 s old, then saves the status and the mp4 link. reelsdk tries up to 3 times if your app doesn't answer with a 2xx.

    index.js (continued)
    app.post("/webhooks/reelsdk", express.raw({ type: "*/*" }), (req, res) => {
      const body = req.body.toString("utf8");
      if (!verifyReelSignature(req.get("Reel-Signature"), body, process.env.REELSDK_WEBHOOK_SECRET)) {
        return res.status(401).send("invalid signature");
      }
    
      const job = JSON.parse(body);
      console.log("reelsdk job finished", job.id, job.status, job.outputs.video);
      // Save job.status and job.outputs.video for job.id in your database here
      res.send("ok");
    });
    
    function verifyReelSignature(header, body, secret) {
      const parts = Object.fromEntries((header ?? "").split(",").map((part) => part.split("=")));
      const age = Math.abs(Date.now() / 1000 - Number(parts.t));
      if (!/^[0-9a-f]{64}$/i.test(parts.v1 ?? "") || !(age <= 300)) return false;
    
      const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${body}`).digest();
      return crypto.timingSafeEqual(expected, Buffer.from(parts.v1, "hex"));
    }
    
    app.listen(3000, "0.0.0.0");

FAQ

Video on Replit, answered

How do I fix Error: spawn ffmpeg ENOENT on Replit?

Node can't find an ffmpeg binary on the PATH. Add ffmpeg as a Nix system dependency (pkgs.ffmpeg in replit.nix) and restart the app. That's enough for small, occasional jobs. For captions or long renders, send the work to an API so it doesn't run on the same instance as your web app.

Is ffmpeg on Replit more expensive than an API?

Not in raw compute. A 60 s encode on 1 vCPU and 2 GB is about 1,320 Autoscale compute units, well under a cent. The cost is capacity and plumbing: the encode competes with your web traffic, and captions need a transcription model, fonts and word timing on top. reelsdk charges $0.02 per output minute for captions with all of that included.

How do I handle long renders without HTTP timeouts?

Pass ?wait=0 and a webhook_url, as the route above does. reelsdk answers with a 202 and the job id right away, then POSTs the finished job to your app with up to 3 delivery attempts. You can also hold the request open with ?wait=N for up to 300 s, or long-poll GET /v1/jobs/{id}?wait=N.

Can I make complete faceless shorts from a Replit app?

Yes. POST /v1/shorts takes a topic, writes the script, generates scene images in one of 8 visual styles, adds a voiceover in English, Spanish, French, Japanese, Chinese or Korean and burns word-by-word captions with music. A short takes about 100 s and costs $0.25 up to 60 s or $0.35 up to 90 s.

What does it cost and how do I get a key?

Sign up with Google and create a key in the dashboard; each key comes with its webhook secret. The first $1 is free with no subscription. Captions and slideshows are $0.02 per output minute and raw ffmpeg is $0.01 per compute minute. Each key runs 3 jobs at once and 500 a day, inputs go up to 500 MB each, and download links stay up for 7 days.

All integrations

Ready to build?

Ship video from your app today

Your first $1 of renders is free, and one key covers every endpoint.

Moonlit lake with a lit cabin under the mountains