---
name: leap
description: Generate images, video and audio from code with the Leap API (api.tryleap.ai). Use when adding AI image, video, speech or music generation to an app; when choosing or calling a model such as Veo, Kling, Seedance, FLUX, GPT Image, Nano Banana or ElevenLabs through Leap; when uploading an image to animate or edit; when waiting for, polling or receiving a webhook for a Leap generation; or when a Leap API call fails.
---

# Leap API

Leap is one REST API and one balance for many image, video and audio models. Every model takes the same request: a model ID and an `input` object described by that model's JSON Schema.

- Base URL: `https://api.tryleap.ai`
- Auth: header `x-api-key: $LEAP_API_KEY` or `Authorization: Bearer $LEAP_API_KEY`. Keys start with `leap_` and come from https://app.tryleap.ai/go/api.
- Docs: https://www.tryleap.ai/docs (not docs.tryleap.ai). Every page is also Markdown (`/docs/quickstart.md`) and the index is `/docs/llms.txt`. The machine-readable spec is `/docs/openapi.json`.
- Leap is not OpenAI-compatible: there is no `/v1/chat/completions`. Everything goes through `POST /v1/generations`.
- The older `reference.tryleap.ai` API (`/api/v1/images/models/{id}/inferences`) is retired. Ignore it if a search finds it.

## Rules

1. Call Leap from server code only. Read the key from the `LEAP_API_KEY` environment variable. Never ship it to a browser or a mobile app, and never write it into a file in the repo.
2. Never guess a model's input fields. Read its `input_schema` first (below) and build `input` from it. Fields you leave out take their defaults.
3. Send an `idempotency-key` header (a fresh UUID per user action) on every `POST /v1/generations`, so a retry never starts or bills a second run.
4. Images: add `prefer: wait=60` and the response usually holds the finished result. Video and music take one to five minutes. In a web app or server, give the request a `webhook` URL (see Getting the result) and return the generation ID to your caller; in a script, long-poll `GET /v1/generations/:id` with `prefer: wait=60` until the status is final. Never hold one serverless request open for minutes.
5. Output URLs expire after 24 hours. Download what you keep to your own storage. Reading the generation again signs fresh URLs.
6. Files go in by ID. Upload with `POST /v1/files` (the raw bytes are the body) and put the returned `file_...` ID in the field the schema names (`first_frame`, `images`, `audio_file`...). A URL is not accepted where the schema asks for a file ID.
7. A generation that fails comes back with `status: "failed"` and an `error`, and costs nothing. That is not an HTTP error. Check `status`, not just the HTTP code.
8. Don't stop for input. Scripts, CLIs and servers you write must run unattended: no "Continue? [y/N]" prompts, no `input()`. Show a quote's price in your UI or log it, but don't block on it unless the user asked for a confirmation step.

## Workflow

### 1. Pick a model

```bash
curl -s https://api.tryleap.ai/v1/public/models          # no key needed: use this to explore
curl -s https://api.tryleap.ai/v1/models -H "x-api-key: $LEAP_API_KEY"   # the same list, as your workspace sees it
```

Use the public catalog when you have no key yet; `GET /v1/models` without a key is a 401. Pick a real model ID from the list (or from references/models.md). Don't leave the model as a required setting with no default.

Each entry has `id` (`creator/name`), `modality` (`image`, `video`, `audio`, `text`, `3d`), `tasks` (such as `text-to-video`, `image-to-video`, `image-edit`, `text-to-speech`), `status` (`stable` or `preview`) and `pricing` (`unit` and `usd`). See [references/models.md](references/models.md) for good defaults by job.

### 2. Read its input schema

```bash
curl -s https://api.tryleap.ai/v1/public/models/google/veo-3.1 | jq .input_schema
```

### 3. Price it (optional; useful to show a price in your UI)

```bash
curl -s https://api.tryleap.ai/v1/quotes -H "x-api-key: $LEAP_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model": "google/veo-3.1-fast", "input": {"prompt": "A heron lands on a still lake at dawn", "duration": 4}}'
# {"object": "quote", "cost_usd": "0.66", "hold_usd": "0.66", "input": {...with defaults, audio: true}}
```

### 4. Run it and get the result

```ts
const LEAP = "https://api.tryleap.ai";
const FINAL = ["succeeded", "failed", "canceled"];

async function leap(path: string, init: RequestInit = {}) {
  const response = await fetch(`${LEAP}${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) {
    const { type, code, message, request_id } = body.error ?? {};
    throw new Error(
      `Leap ${response.status} ${type}/${code}: ${message} (${request_id})`,
    );
  }
  return body;
}

export async function generate(model: string, input: Record<string, unknown>) {
  let generation = await leap("/v1/generations", {
    method: "POST",
    headers: { "idempotency-key": crypto.randomUUID(), prefer: "wait=60" },
    body: JSON.stringify({ model, input }),
  });
  // Long-poll: each read waits up to 60 s for the run to finish, so no sleep is needed.
  while (!FINAL.includes(generation.status)) {
    generation = await leap(`/v1/generations/${generation.id}`, {
      headers: { prefer: "wait=60" },
    });
  }
  if (generation.status !== "succeeded")
    throw new Error(generation.error?.message ?? generation.status);
  return generation; // generation.output[0].url, generation.usage.cost_usd
}
```

This loop is right for scripts and CLIs. Python and more patterns: [references/examples.md](references/examples.md).

## Getting the result

| Where your code runs                      | Use                                                                                                                                                                                            |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Image or speech, anywhere                 | `prefer: wait=60` on the create request; the answer usually holds the result                                                                                                                   |
| A script, CLI or notebook (no public URL) | Long-poll: `GET /v1/generations/:id` with `prefer: wait=60` in a loop; each read returns when the run finishes or after 60 s. Without the header, wait the `Retry-After` seconds between reads |
| A web app or server, especially for video | A webhook: Leap calls your URL when the run finishes                                                                                                                                           |

### Webhooks

Add `"webhook": "https://your-app.com/api/leap-webhook"` to the create body (or to each request in `POST /v1/batches`). When the run ends, Leap POSTs an event to that URL:

```json
{
  "id": "evt_...",
  "object": "event",
  "type": "generation.succeeded",
  "api_version": "2026-10-04",
  "created_at": "...",
  "data": {
    "object": {
      "id": "gen_...",
      "status": "succeeded",
      "output": [{ "url": "https://..." }]
    }
  }
}
```

Types: `generation.succeeded`, `generation.failed`, `generation.canceled`, `batch.completed`. `data.object` is the generation exactly as `GET /v1/generations/:id` returns it.

Verify every delivery before you trust it. Deliveries follow the Standard Webhooks spec (`webhook-id`, `webhook-timestamp`, `webhook-signature` headers), so use the `standardwebhooks` package (npm and PyPI) with your workspace's secret, read once from `GET /v1/webhooks/default/secret` and stored as `LEAP_WEBHOOK_SECRET`:

```ts
import { Webhook } from "standardwebhooks";

export async function POST(request: Request) {
  const body = await request.text(); // the raw body, not re-serialized JSON
  const event = new Webhook(process.env.LEAP_WEBHOOK_SECRET!).verify(
    body,
    Object.fromEntries(request.headers),
  ) as {
    id: string;
    type: string;
    data: { object: { id: string; status: string } };
  };
  // Deliveries can repeat: skip an event.id you've already handled.
  // Save the result against the generation ID you stored when you started the run.
  return new Response(null, { status: 204 }); // answer 2xx fast; do slow work after
}
```

Delivery is at least once, unordered, retried for up to 72 hours, and only a 2xx within 15 seconds counts. Webhook URLs must be `https`. To get events for every run in the workspace instead (including studio runs), register an endpoint with `POST /v1/webhook_endpoints` `{"url": "https://..."}`; its response holds that endpoint's own `secret`, shown once. Keep polling as a fallback: if no event arrived, `GET /v1/generations/:id` tells you where the run is.

## Errors

Errors look like `{"error": {"type", "code", "message", "param", "request_id"}}`. Branch on `type` and `code`.

| Status | Meaning                                                         | Do                                                              |
| ------ | --------------------------------------------------------------- | --------------------------------------------------------------- |
| 400    | Input doesn't match the schema; `param` names the field         | Fix the input against `input_schema`                            |
| 401    | Missing or bad key; the message says which                      | Send `x-api-key` or `Authorization: Bearer`                     |
| 402    | `insufficient_credit`                                           | Tell the user to add credits at app.tryleap.ai; quote first     |
| 403    | Key lacks the scope (`generations:read` or `generations:write`) | Use a key with the scope                                        |
| 409    | Same idempotency key, different body                            | Use a new key for a new request                                 |
| 429    | Rate limited                                                    | Wait `Retry-After` seconds, retry with the same idempotency key |
| 5xx    | Leap or the provider failed                                     | Retry with backoff and the same idempotency key                 |

Full endpoint list: [references/api.md](references/api.md).

## Using Leap as a tool instead of code

If the user wants to generate media from this chat rather than build it into an app, use Leap's MCP server: `claude mcp add --transport http leap https://app.tryleap.ai/api/mcp` (signs in through the browser). Without a browser, or in Cursor, pass a key instead: `--header "Authorization: Bearer $LEAP_API_KEY"`. Tools: `search_models`, `get_model`, `quote`, `generate`, `get_generation`.
