Blog

Integration3 min read

The fal JavaScript Client: A Tutorial That Respects Your Time

Install, configure, submit, poll, result. Ten minutes from npm install to first rendered video.


Install and set a key

BASH
1npm install @fal-ai/client
TYPESCRIPT
1import { fal } from "@fal-ai/client";
2
3fal.config({ credentials: process.env.FAL_KEY });

Never hardcode the key. Never ship it in a client bundle. The client runs on the server. If you are in Next.js, that means a route handler or a server action. The browser talks to your server, your server talks to fal.

The three modes you will use

@fal-ai/client exposes four ways to call a model. In real code you pick from three.

  • fal.subscribe(endpoint, { input }) submits, waits, returns. Use it for anything under three minutes with a spinner.
  • fal.queue.submit(endpoint, { input, webhookUrl }) submits and returns a request id. Use it with a webhook.
  • fal.queue.status and fal.queue.result check and retrieve queued jobs.

Skip fal.run. It blocks on HTTP with no queue visibility; there is no reason to choose it.

Subscribe versus queue modes of the fal JavaScript client
Subscribe versus queue modes of the fal JavaScript client

First render, end to end

Five second Wan 2.7 clip at 1080p. $0.10 per second, so $0.50.

TYPESCRIPT
1import { fal } from "@fal-ai/client";
2
3fal.config({ credentials: process.env.FAL_KEY });
4
5const result = await fal.subscribe("fal-ai/wan/v2.7/text-to-video", {
6 input: {
7 prompt: "A red kite rising over a wheat field at golden hour, slow push",
8 duration: 5,
9 resolution: "1080p",
10 aspect_ratio: "16:9",
11 },
12 logs: true,
13 onQueueUpdate: (update) => {
14 if (update.status === "IN_PROGRESS") {
15 update.logs?.forEach((l) => console.log(l.message));
16 }
17 },
18});
19
20console.log(result.data.video.url);
21console.log("request id", result.requestId);

duration is an integer on Wan 2.7. "5" is a validation error. logs: true plus onQueueUpdate gives you live progress. result.requestId is your handle for later lookups.

The queue lifecycle

Jobs move through IN_QUEUE, IN_PROGRESS, COMPLETED. subscribe hides this. queue.submit makes you own it.

Queue lifecycle from IN_QUEUE to COMPLETED
Queue lifecycle from IN_QUEUE to COMPLETED
TYPESCRIPT
1const { request_id } = await fal.queue.submit("fal-ai/veo3.1/lite", {
2 input: {
3 prompt: "A matte black laptop opens on a walnut desk, slow dolly in",
4 duration: "6s",
5 resolution: "1080p",
6 aspect_ratio: "16:9",
7 },
8});
9
10const status = await fal.queue.status("fal-ai/veo3.1/lite", {
11 requestId: request_id,
12 logs: true,
13});
14
15if (status.status === "COMPLETED") {
16 const { data } = await fal.queue.result("fal-ai/veo3.1/lite", { requestId: request_id });
17 console.log(data.video.url);
18}

At $0.05 per second, a six second Veo 3.1 Lite draft is $0.30. Use Lite to lock the prompt before paying $0.40 per second for full Veo 3.1.

Errors you will see in week one

ValidationError with 422 means bad input shape. Read the message; the integer versus string duration mismatch bites everyone once.

fetch failed mid subscribe means the connection dropped, not that the job failed. The job is still running. Use fal.queue.status with the request id you captured at submit time.

429 Too Many Requests means you hit concurrency. Reach for p-queue, or switch to queue.submit with a webhook and stop holding connections open per job.

A wrapper that earns its keep

TYPESCRIPT
1import { fal, type Result } from "@fal-ai/client";
2
3export async function generate<T>(
4 endpoint: string,
5 input: Record<string, unknown>,
6): Promise<Result<T>> {
7 try {
8 return await fal.subscribe(endpoint, { input, logs: true });
9 } catch (err) {
10 console.error("[fal]", endpoint, err);
11 throw err;
12 }
13}

npm install, set a key, subscribe or queue, handle the three errors above, wrap the call. Nothing under this layer is going to change.