> ## Documentation Index
> Fetch the complete documentation index at: https://www.tryleap.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Leap's API is at https://api.tryleap.ai. Send the key in the x-api-key header or as Authorization: Bearer; keys start with leap_.
> Take model IDs and input schemas from GET /v1/models/{creator}/{name}, or from https://api.tryleap.ai/v1/public/models without a key. Never guess an input field: unknown fields are a 400.
> Model inputs take uploaded file IDs (file_...), never URLs.
> To give a coding agent the whole API, install the Leap skill: curl -fsSL https://www.tryleap.ai/install.sh | sh
> The full OpenAPI 3.1 spec is at https://www.tryleap.ai/docs/openapi.json.

# Add generation to a Next.js app

> Start images and videos from Next.js route handlers with your key on the server, then poll from a client component and show the result.

This guide adds a prompt box to a Next.js app that uses the App Router, on Next.js 15 or later. Two route handlers call Leap with your key on the server. The page asks them to start a run, polls until it's done and shows the image or video. The browser never sees your key.

## Add your key

Put the key in `.env.local`, next to `package.json`:

```bash .env.local theme={"theme":"css-variables"}
LEAP_API_KEY=leap_...
```

Don't give it the `NEXT_PUBLIC_` prefix: Next.js would put it in the code it sends to the browser.

## Call Leap from the server

Both routes call Leap through this helper. It sends your key, and it answers the browser with only what the page shows: the ID, the status, the output links and any error.

```ts lib/leap.ts theme={"theme":"css-variables"}
type Generation = {
  id: string;
  status: "queued" | "running" | "succeeded" | "failed" | "canceled";
  output: { type: string; url: string }[];
  error: { message: string } | null;
};

export async function leap(path: string, init: RequestInit = {}) {
  const response = await fetch(`https://api.tryleap.ai${path}`, {
    ...init,
    headers: {
      "x-api-key": process.env.LEAP_API_KEY ?? "",
      "content-type": "application/json",
      ...init.headers,
    },
  });
  const body = await response.json();

  if (!response.ok) {
    return Response.json({ error: { message: body.error.message } }, { status: response.status });
  }

  const { id, status, output, error }: Generation = body;

  return Response.json({ id, status, error, output: output.map(({ type, url }) => ({ type, url })) });
}
```

## Start a run

`POST /api/generate` takes a prompt and whether to make an image or a video. The browser picks the kind, never the model, so it can only run what you chose.

```ts app/api/generate/route.ts theme={"theme":"css-variables"}
import { randomUUID } from "node:crypto";
import { leap } from "@/lib/leap";

// An image waits up to 50 seconds for its result below, so allow the function a little longer.
export const maxDuration = 60;

export async function POST(request: Request) {
  const { prompt, kind } = await request.json().catch(() => ({}));

  if (typeof prompt !== "string" || !prompt.trim()) {
    return Response.json({ error: { message: "Write a prompt." } }, { status: 400 });
  }

  const video = kind === "video";

  // In a real app, check who's signed in here, and save the new generation's ID with them.
  return leap("/v1/generations", {
    method: "POST",
    headers: {
      "idempotency-key": randomUUID(),
      // Most images are done within the wait. A video takes minutes, so the page polls it.
      ...(video ? {} : { prefer: "wait=50" }),
    },
    body: JSON.stringify(
      video
        ? { model: "google/veo-3.1-fast", input: { prompt, duration: 4 } }
        : { model: "black-forest-labs/flux-2-pro", input: { prompt } },
    ),
  });
}
```

`prefer: wait=50` holds the request open until the image is ready, for up to 50 seconds, so most images come back done and the page doesn't poll at all. The wait has to fit inside your function's time limit. `maxDuration` asks the platform for 60 seconds; if your plan allows less, send a shorter wait, or none, and let the page poll images too. An image that isn't done when the wait ends comes back `queued` or `running`, and the page polls it like a video.

`@/` is the import alias that `create-next-app` sets up. If your app has none, use a relative path.

## Read a run

`GET /api/generations/{id}` reads the run's status. In Next.js 15 and later, a route handler's `params` is a promise, so await it.

```ts app/api/generations/[id]/route.ts theme={"theme":"css-variables"}
import { leap } from "@/lib/leap";

export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;

  // In a real app, answer 404 here unless the signed-in user started this generation.
  return leap(`/v1/generations/${encodeURIComponent(id)}`);
}
```

## Show it on a page

The client component starts a run, then polls every 5 seconds until the status is `succeeded`, `failed` or `canceled`, and shows the image or video.

```tsx app/generate.tsx theme={"theme":"css-variables"}
"use client";

import { useState } from "react";

type Result = {
  id?: string;
  status?: string;
  output?: { type: string; url: string }[];
  error?: { message: string } | null;
};

const FINAL = ["succeeded", "failed", "canceled"];

export function Generate() {
  const [prompt, setPrompt] = useState("");
  const [result, setResult] = useState<Result | null>(null);
  const [busy, setBusy] = useState(false);

  async function start(kind: "image" | "video") {
    setBusy(true);
    setResult(null);

    try {
      let response = await fetch("/api/generate", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ prompt, kind }),
      });
      let data: Result = await response.json();
      setResult(data);

      while (response.ok && data.id && !FINAL.includes(data.status ?? "")) {
        await new Promise((resolve) => setTimeout(resolve, 5_000));
        response = await fetch(`/api/generations/${data.id}`);
        data = await response.json();
        setResult(data);
      }
    } catch {
      setResult({ error: { message: "Couldn't reach the server. Try again." } });
    } finally {
      setBusy(false);
    }
  }

  const output = result?.output?.[0];

  return (
    <div>
      <textarea value={prompt} onChange={(event) => setPrompt(event.target.value)} />
      <button disabled={busy || !prompt.trim()} onClick={() => start("image")}>
        Make an image
      </button>
      <button disabled={busy || !prompt.trim()} onClick={() => start("video")}>
        Make a video
      </button>

      {busy && <p>Working: {result?.status ?? "starting"}</p>}
      {result?.error && <p>{result.error.message}</p>}
      {output?.type === "image" && <img src={output.url} alt={prompt} />}
      {output?.type === "video" && <video src={output.url} controls autoPlay muted loop />}
    </div>
  );
}
```

```tsx app/page.tsx theme={"theme":"css-variables"}
import { Generate } from "./generate";

export default function Page() {
  return <Generate />;
}
```

Run `npm run dev`, open the page, and make an image. With the defaults above, an image costs \$0.033 and a 4-second video with sound \$0.66.

## Before you ship

* Check that each generation belongs to the person asking. Anyone who can call these routes spends your balance, and anyone who knows a generation's ID can read its output. Sign people in, save each ID with the user who started it when you start the run, and in the `GET` route answer `404` unless the ID is theirs.
* Output links expire after 24 hours. To keep a result, download it on the server and store it yourself.
* Every request reaches Leap from your server, so they share its IP address's budget of 300 requests a minute and your key's 600. Each open page polls once every 5 seconds while a run is going. See [Rate limits](/docs/errors#rate-limits).
* A failed run isn't charged, and `error.message` says why. The page shows it as it is. See [When a run fails](/docs/generations#when-a-run-fails).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.