> ## 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.

# Generations

> Start a run of any model or preset, get its result by waiting in the request, long-polling, a webhook or polling, read its outputs, cancel it, list your runs, and start up to 50 at once.

A generation is one run of a model or preset. Its `status` starts at `queued`, moves to `running`, and ends at `succeeded`, `failed` or `canceled`. Only a succeeded generation is charged.

## Start one

`POST /v1/generations` takes the model and its input. Every model takes the same request shape; only `input` differs, as the model's [input schema](/docs/models#read-an-input-schema) says.

```bash theme={"theme":"css-variables"}
curl https://api.tryleap.ai/v1/generations \
  -H "x-api-key: $LEAP_API_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: 0b8f6c1e-5d2a-4c7b-9e3f-1a2b3c4d5e6f" \
  -d '{"model": "google/veo-3.1-fast", "input": {"prompt": "A heron lands on a still lake at dawn", "duration": 4}}'
```

The answer is `201 Created` with the generation, `queued`, and its path in the `Location` header.

### Retry safely

Send an `idempotency-key` you generate for each new request, such as a UUID. If the network drops and you retry with the same key and body, you get the same generation back, marked with `Idempotent-Replayed: true`, and no second run starts. Reusing a key with a different body is a `409` with the code `conflict`. Keys belong to your workspace and don't expire, so never reuse one for a new request.

## Get the result

Pick by how long the model takes and where your code runs:

| How | Best for | What you do |
| - | - | - |
| [Wait in the request](#wait-in-the-request) | Images, speech, anything that takes seconds | Send `prefer: wait=60` with `POST /v1/generations`. One request gets the result. |
| [Long-poll](#long-poll) | Video from a script, a CLI, a notebook or an agent | Read `GET /v1/generations/{id}` with `prefer: wait=60` in a loop. A 4-minute video takes about five reads. |
| [Webhooks](#webhooks) | Video in a web app or a server | Name a `webhook` URL on the request, or set up a [webhook endpoint](/docs/webhooks). Leap calls you when the run ends. |
| [Poll](#poll) | Clients that can't hold a request open | Read `GET /v1/generations/{id}` as often as its `Retry-After` header says. |

### Wait in the request

`prefer: wait=N` holds the request open for up to `N` seconds (at most 60) until the run finishes, and the `Preference-Applied` header echoes the wait used. Most images and speech finish inside the wait. If the run is still going when the wait ends, you get it back `queued` or `running`, and you carry on with a [long-poll](#long-poll).

```bash theme={"theme":"css-variables"}
curl https://api.tryleap.ai/v1/generations \
  -H "x-api-key: $LEAP_API_KEY" \
  -H "content-type: application/json" \
  -H "prefer: wait=60" \
  -d '{"model": "black-forest-labs/flux-2-pro", "input": {"prompt": "A lighthouse in a storm, oil painting"}}'
```

### Long-poll

`GET /v1/generations/{id}` takes the same `prefer: wait=N`. It answers as soon as the run reaches `succeeded`, `failed` or `canceled`, or after `N` seconds with the run as it is, whichever comes first. Either way the status is `200`: read `status` to tell which. Loop until it's final.

```bash theme={"theme":"css-variables"}
curl https://api.tryleap.ai/v1/generations/$GENERATION_ID \
  -H "x-api-key: $LEAP_API_KEY" \
  -H "prefer: wait=60"
```

A 4-second clip usually takes one to three minutes, so a loop reads it two or three times. These helpers also ride out dropped connections, `429` and `5xx`:

<CodeGroup>
  ```ts TypeScript theme={"theme":"css-variables"}
  const FINAL = ["succeeded", "failed", "canceled"];

  const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

  async function waitForGeneration(id: string, timeoutMs = 15 * 60_000) {
    const deadline = Date.now() + timeoutMs;

    while (Date.now() < deadline) {
      let response: Response | undefined;

      try {
        response = await fetch(`https://api.tryleap.ai/v1/generations/${id}`, {
          headers: { "x-api-key": process.env.LEAP_API_KEY ?? "", prefer: "wait=60" },
          // The server holds the read for up to 60 seconds; give it a little more.
          signal: AbortSignal.timeout(75_000),
        });
      } catch {
        // The connection dropped or the request timed out: read again.
      }

      if (response && response.status !== 429 && response.status < 500) {
        const body = await response.json();
        if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
        if (FINAL.includes(body.status)) return body;
      }

      // Still running, busy or unreachable: wait as long as Retry-After asks.
      const retryAfter = Number(response?.headers.get("retry-after"));
      await sleep((retryAfter > 0 ? retryAfter : 5) * 1_000);
    }

    throw new Error(`Generation ${id} is still running. Read it again later.`);
  }
  ```

  ```python Python theme={"theme":"css-variables"}
  import os
  import time

  import requests

  FINAL = {"succeeded", "failed", "canceled"}


  def wait_for_generation(generation_id, timeout_s=15 * 60):
      deadline = time.monotonic() + timeout_s

      while time.monotonic() < deadline:
          try:
              response = requests.get(
                  f"https://api.tryleap.ai/v1/generations/{generation_id}",
                  headers={"x-api-key": os.environ["LEAP_API_KEY"], "prefer": "wait=60"},
                  # The server holds the read for up to 60 seconds; give it a little more.
                  timeout=75,
              )
          except requests.RequestException:
              # The connection dropped or the request timed out: read again.
              response = None

          if response is not None and response.status_code != 429 and response.status_code < 500:
              body = response.json()
              if not response.ok:
                  raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
              if body["status"] in FINAL:
                  return body

          # Still running, busy or unreachable: wait as long as Retry-After asks.
          retry_after = response.headers.get("retry-after", "") if response is not None else ""
          time.sleep(int(retry_after) if retry_after.isdigit() else 5)

      raise TimeoutError(f"Generation {generation_id} is still running. Read it again later.")
  ```
</CodeGroup>

Any other error stops the loop, because reading again won't help. When your deadline passes, the run carries on: a timeout on your side doesn't cancel it, and it's charged if it succeeds. Keep its ID, then read it again later or [cancel it](#cancel).

### Webhooks

To hear when a run ends without holding anything open, name a URL on the request:

```bash theme={"theme":"css-variables"}
curl https://api.tryleap.ai/v1/generations \
  -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 },
    "webhook": "https://example.com/webhooks/leap"
  }'
```

When the run ends, Leap POSTs a signed `generation.succeeded`, `generation.failed` or `generation.canceled` event to that URL, with the generation inside. Each request in a [batch](#batches) can name its own `webhook`. To get every run's events at one URL instead, set up a webhook endpoint. [Webhooks](/docs/webhooks) covers both, and how to verify the signature. Keep a long-poll as a fallback, to catch a run whose event you missed.

### Poll

Some clients can't hold a request open for a minute. They can read the generation without `prefer` and get an answer at once. While the run isn't final, the answer carries a `Retry-After` header with the seconds to wait before the next read: 2 for images and audio, 5 for video and 3D. A final answer has none. The answer to `POST /v1/generations` carries it too.

Polling counts against your [rate limit](/docs/errors#rate-limits) like any request, so follow `Retry-After` rather than reading faster.

## The generation object

```json theme={"theme":"css-variables"}
{
  "id": "gen_7Hq2mVx9Lr4Kp8Tn3Wc6Yb1D",
  "object": "generation",
  "model": "google/veo-3.1-fast",
  "modality": "video",
  "status": "succeeded",
  "provider": "gateway",
  "revision": "2026-10-02",
  "input": {
    "prompt": "A heron lands on a still lake at dawn",
    "aspect_ratio": "16:9",
    "n": 1,
    "duration": 4,
    "resolution": "1080p",
    "audio": true
  },
  "output": [
    {
      "type": "video",
      "url": "https://api.tryleap.ai/files/eyJrIjoibWVkaWEv...",
      "expires_at": "2026-10-05T18:24:12.004Z",
      "content_type": "video/mp4",
      "width": 1920,
      "height": 1080,
      "poster": {
        "url": "https://api.tryleap.ai/files/eyJrIjoibWVkaWEv...",
        "expires_at": "2026-10-05T18:24:12.004Z",
        "width": 720,
        "height": 405
      }
    }
  ],
  "error": null,
  "usage": { "cost_usd": "0.66" },
  "source": "api",
  "batch_id": null,
  "created_at": "2026-10-04T18:22:41.512Z",
  "started_at": "2026-10-04T18:22:42.090Z",
  "completed_at": "2026-10-04T18:24:11.877Z"
}
```

| Field | Meaning |
| - | - |
| `input` | The input as it ran, with the defaults filled in. |
| `output` | One entry per file, empty until the run succeeds. `url` is a signed link that works until `expires_at`, 24 hours after it was signed; download what you keep. Reading the generation again signs fresh links. |
| `output[].poster` | For a video: a JPEG of a frame from its middle, about 720 pixels on the long side. |
| `preview` | While a preset that makes a still before its video is running: that still, once it exists. Absent otherwise, and for a run whose video also reads words or a photo you sent, such as a line to say or a photo the clip starts from: that still comes with the video, once the run succeeds. |
| `error` | Why a `failed` run failed, as `{ "message": "..." }`; otherwise null. |
| `usage.cost_usd` | What the run cost, once it's charged; null before. |
| `provider` | Which upstream ran it. Informational: model IDs and inputs stay the same whichever runs it. |
| `revision` | The model's schema and price revision the run used. |
| `source` | Where it was started: `api`, `mcp` or `studio`. |
| `batch_id` | The batch it belongs to, if any. |

## When a run fails

A run that fails is still a `200` when you read it: its `status` is `failed`, `error.message` says why (the model refused the prompt, or the provider had an error), and you aren't charged. Check `status` before you read `output`.

What to do next depends on the reason:

* When the model's safety filter turned the run down, `error.message` says so and names what to change: the prompt, or the photo you sent. The same input would most likely be turned down again, so change it, or try another model.
* Any other failure, such as a provider error or a run that took too long, may succeed if you start it again as it was.

Either way, start the new run with a new `idempotency-key`. Sending the old key with the same body returns the same failed generation, and no new run starts.

## Cancel

```bash theme={"theme":"css-variables"}
curl -X POST https://api.tryleap.ai/v1/generations/$GENERATION_ID/cancel -H "x-api-key: $LEAP_API_KEY"
```

A queued or running run stops at once: you get it back `canceled`, its hold is released, and if the provider finishes it anyway, the result is thrown away and nothing is charged. A run that has already ended comes back as it is.

Two kinds of run can't be stopped once they've started, and get a `409` with the code `conflict`: a run at a provider that bills it either way, and a preset run that has already shown its `preview` still. Each runs to its end and is charged only if it succeeds.

## List your runs

```bash theme={"theme":"css-variables"}
curl "https://api.tryleap.ai/v1/generations?status=queued,running&limit=20" -H "x-api-key: $LEAP_API_KEY"
```

| Query | Meaning |
| - | - |
| `model` | Only runs of this model or preset. A preset ID without a version, such as `leap/headshot`, matches every version. |
| `status` | Only runs in these states, comma-separated, such as `queued,running`. |
| `source` | Only runs started from here, comma-separated: `api`, `mcp`, `studio`. |
| `limit` | Rows per page, 1 to 100. The default is 50. |
| `cursor` | The `next_cursor` of the previous page. |

```json theme={"theme":"css-variables"}
{
  "object": "list",
  "url": "/v1/generations",
  "data": [
    {
      "id": "gen_7Hq2mVx9Lr4Kp8Tn3Wc6Yb1D",
      "object": "generation",
      "status": "running"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJpZCI6Imdlbl83SHEybVZ4OUxyNEtwOFRuM1djNlliMUQifQ.2b4Kx9Qm7Lr1Tn5Wc3Yb8Df6Gh0Jk2Mn4Pq6Rs8Tv0X"
}
```

Rows come newest first. A cursor only works with the filters it was issued for, so keep them the same while you page.

## Batches

`POST /v1/batches` starts up to 50 generations in one request. They're priced and held together, so either all start or none do, and each then runs as its own generation.

```bash theme={"theme":"css-variables"}
curl https://api.tryleap.ai/v1/batches \
  -H "x-api-key: $LEAP_API_KEY" \
  -H "content-type: application/json" \
  -H "idempotency-key: 5c9e2f4a-8b1d-4f6e-a3c7-9d0e1f2a3b4c" \
  -d '{
    "requests": [
      {"model": "black-forest-labs/flux-2-pro", "input": {"prompt": "A fox in the snow, watercolor"}},
      {"model": "black-forest-labs/flux-2-pro", "input": {"prompt": "A fox in the snow, linocut print"}}
    ]
  }'
```

The answer is `201` with `{"id": "bat_...", "object": "batch", "data": [...]}`, holding each generation in the order you sent them. Each one carries the batch's ID in `batch_id`; read it at `GET /v1/generations/{id}` like any other generation.

Read the whole batch with `GET /v1/batches/{id}`. It takes `prefer: wait=N` too, and then answers once every generation in it is final, or after `N` seconds. While any isn't final, the answer carries `Retry-After`. A webhook endpoint also gets one `batch.completed` event when every run in the batch has ended.

```bash theme={"theme":"css-variables"}
curl https://api.tryleap.ai/v1/batches/$BATCH_ID \
  -H "x-api-key: $LEAP_API_KEY" \
  -H "prefer: wait=60"
```


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