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

# Errors and retries

> The one error shape every endpoint answers with, every error code and what to do about it, which requests are safe to retry, and the rate limits.

## The error object

Every error has the same shape and an HTTP status that matches it:

```json theme={"theme":"css-variables"}
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_parameter",
    "message": "Choose one of: 1:1, 3:2, 2:3, 4:3, 3:4, 16:9, 9:16, 21:9, 9:21.",
    "param": "input.aspect_ratio",
    "doc_url": "https://www.tryleap.ai/docs/errors#invalid_parameter",
    "request_id": "req_8kQ2vX9mR4tL6nB1cW3yZ5aD"
  }
}
```

| Field | Meaning |
| - | - |
| `type` | The kind of error: `invalid_request_error`, `authentication_error`, `permission_error`, `billing_error`, `conflict_error`, `rate_limit_error` or `api_error`. |
| `code` | A stable code for this exact error, listed below. Branch on `type` and `code`, never on `message`. |
| `message` | What went wrong, written for a person. It can change, so show it but don't parse it. |
| `param` | For a `400`, the field at fault, such as `input.aspect_ratio`, when there is one. Otherwise null. |
| `doc_url` | A link to this code's entry on this page, such as `https://www.tryleap.ai/docs/errors#invalid_parameter`. |
| `request_id` | This request's ID, also in the `x-request-id` header of every answer. Include it when you contact support. |

## A failed run isn't an error

A generation that fails, because the model refused the prompt or the provider had an error, isn't an HTTP error. Starting it succeeded, and reading it returns `200` with `status: "failed"` and an `error.message`. It isn't charged. Check `status` before you read `output`. See [Generations](/docs/generations#when-a-run-fails).

## Error codes

| Code | Status | Type | Retry? |
| - | - | - | - |
| [`invalid_parameter`](#invalid_parameter) | 400 | `invalid_request_error` | No. Fix the field `param` names. |
| [`invalid_request`](#invalid_request) | Other 4xx | `invalid_request_error` | No. Fix the request. |
| [`conflicting_credentials`](#conflicting_credentials) | 400 | `invalid_request_error` | No. Send one key. |
| [`placeholder_in_path`](#placeholder_in_path) | 400 | `invalid_request_error` | No. Put the real ID in the path. |
| [`missing_api_key`](#missing_api_key) | 401 | `authentication_error` | No. Send your key. |
| [`invalid_api_key`](#invalid_api_key) | 401 | `authentication_error` | No. Send a valid key. |
| [`authentication_required`](#authentication_required) | 401 | `authentication_error` | No. Send a valid key. |
| [`insufficient_credit`](#insufficient_credit) | 402 | `billing_error` | After you add credit. |
| [`permission_denied`](#permission_denied) | 403 | `permission_error` | No. |
| [`not_found`](#not_found) | 404 | `invalid_request_error` | No. Check the method and path. |
| [`resource_not_found`](#resource_not_found) | 404 | `invalid_request_error` | No. |
| [`method_not_allowed`](#method_not_allowed) | 405 | `invalid_request_error` | No. |
| [`conflict`](#conflict) | 409 | `conflict_error` | No. |
| [`gone`](#gone) | 410 | `invalid_request_error` | No. Move to the replacement. |
| [`payload_too_large`](#payload_too_large) | 413 | `invalid_request_error` | No. Send a smaller body. |
| [`unsupported_media_type`](#unsupported_media_type) | 415 | `invalid_request_error` | No. |
| [`rate_limit_exceeded`](#rate_limit_exceeded) | 429 | `rate_limit_error` | Yes, after `Retry-After`. |
| [`ip_rate_limit_exceeded`](#ip_rate_limit_exceeded) | 429 | `rate_limit_error` | Yes, after `Retry-After`. |
| [`internal_error`](#internal_error) | 500 | `api_error` | Yes, with backoff. |
| [`service_unavailable`](#service_unavailable) | 503 | `api_error` | Yes, after `Retry-After` or with backoff. |

### invalid\_parameter

`400`. A field in the body or the query, or the `idempotency-key` header, isn't valid: missing, the wrong type, not one of the allowed values, or not an input of this model at all. `param` names the field. For a field of a model's `input`, `message` also says what's allowed. A body that isn't valid JSON gets this code too, with `param` null and the message `Invalid request parameters.` Fix the request; retrying it unchanged fails again.

### invalid\_request

A `4xx` that has no code of its own on this page, with the status that fits it. You'll rarely see one. Fix the request before you send it again.

### conflicting\_credentials

`400`. The request carried two different API keys, one in `x-api-key` and one as `Authorization: Bearer`. Send one of them. The same key in both headers is fine.

### placeholder\_in\_path

`400`. The path still holds a placeholder from an example, such as `{id}`, `<id>`, `:id` or `undefined`, so the request never reached an endpoint. Put the real ID there.

### missing\_api\_key

`401`. The request carried no API key. Send it in the `x-api-key` header or as `Authorization: Bearer`. The message also names a common slip, such as an empty header or a shell variable sent as its name (`$LEAP_API_KEY` that never expanded). See [Authentication](/docs/authentication).

### invalid\_api\_key

`401`. The key isn't valid: mistyped, cut short, revoked or expired. The message says which mistake its shape shows, such as `Bearer ` inside `x-api-key`, or a key that doesn't start with `leap_`. Create a new key at [app.tryleap.ai/go/api](https://app.tryleap.ai/go/api) if yours is gone.

### authentication\_required

`401`. Authentication is required. On `/v1`, a missing or invalid key gets `missing_api_key` or `invalid_api_key` instead, so you'll rarely see this one.

### insufficient\_credit

`402`, type `billing_error`. Your available balance doesn't cover the run's hold, or an upload of a video or a sound needs credit. Nothing started. Add credits at [app.tryleap.ai/go/settings/credits](https://app.tryleap.ai/go/settings/credits), or [get a quote](/docs/pricing#get-a-quote) to see what a run needs.

### permission\_denied

`403`. The key doesn't have the scope this endpoint needs. The message names the scope.

### resource\_not\_found

`404`. Nothing with that ID exists in your workspace. IDs from another workspace are a `404` too.

### not\_found

`404`. No endpoint matches the path. The message points the way: a path without `/v1` gets the `/v1` one, a habit from another API gets the nearest Leap endpoint, and an OpenAI path such as `/v1/chat/completions` is told that Leap isn't OpenAI-compatible and that runs start at `POST /v1/generations`.

### method\_not\_allowed

`405`. The endpoint exists but doesn't take this HTTP method. The `Allow` header lists the methods it does take.

### conflict

`409`, type `conflict_error`. Either the `idempotency-key` was already used with a different body (use a new key for a new request), or the generation can't be canceled anymore (see [Cancel](/docs/generations#cancel)).

### gone

`410`. The model or preset was retired at its sunset date and doesn't run anymore. Its own catalog entry, `GET /v1/models/{creator}/{name}`, answers with this `410` too, so you can't look up the replacement there. The message names it, when there is one: `{id} was retired on {sunset}. Use {replacement} instead.`

To move before that happens, watch for deprecation. While a model is deprecated, its catalog entry's `deprecation.replaced_by` names the replacement, and every answer about it carries the [deprecation headers](/docs/models#lifecycle).

### payload\_too\_large

`413`. The body is over its limit, such as an image over 4 MB. Send a video or a sound through a [direct upload](/docs/files#upload-a-video-or-a-sound).

### unsupported\_media\_type

`415`. The body's content type isn't one this endpoint takes.

### rate\_limit\_exceeded

`429`, type `rate_limit_error`. Your key sent too many requests, or a workspace limit was reached, such as the daily upload allowance. The answer says how many seconds to wait in `Retry-After`; wait that long, or a minute if it's missing, then retry with the same `idempotency-key`.

### ip\_rate\_limit\_exceeded

`429`, type `rate_limit_error`. Too many requests came from your IP address. Wait for `Retry-After` seconds, then retry.

### internal\_error

`500`, type `api_error`. Something failed on our side. Retry with backoff and the same `idempotency-key`. If it keeps failing, contact support with the `request_id`.

### service\_unavailable

`503`, type `api_error`. The API or this model is out of service for a while. Wait for `Retry-After` seconds when it's set, otherwise back off, then retry.

## Retries

| Answer | Retry? |
| - | - |
| Network error or timeout | Yes, with the same `idempotency-key`, so a request that did arrive doesn't start a second run. |
| `429` | Yes, after `Retry-After` seconds, or a minute when it isn't set. |
| `500`, `502`, `503`, `504` | Yes, with exponential backoff (1 s, 2 s, 4 s, and so on, up to a minute) and the same `idempotency-key`. |
| Any other `4xx` | No. Fix the request first. |

### Which requests are safe to repeat

`GET` requests and quotes change nothing, so they're always safe to retry.

Only `POST /v1/generations` and `POST /v1/batches` read the `idempotency-key` header. Send a new key with each new request, and the same key when you retry it: the retry gets back what the first request started, and nothing runs or is charged twice. A key is 1 to 255 printable ASCII characters; anything else gets a `400` with `param` set to `Idempotency-Key`. See [Retry safely](/docs/generations#retry-safely).

Other `POST` requests ignore the header:

* Retrying `POST /v1/files` stores a second copy of the image, with its own ID.
* Retrying `POST /v1/uploads` opens a second upload, which counts toward the 10 uploads in flight and the 5 GB a day.
* Completing an upload, canceling a generation and keeping an output as a file are safe to repeat: doing one twice has the same effect as doing it once.

### A retry helper

This wrapper retries network errors, `429` and `5xx` up to five times. It waits as long as `Retry-After` asks, or backs off exponentially with jitter, and gives up at once when the API asks for a wait longer than a minute, such as a daily allowance that resets at midnight UTC. A proxy in front of the API can answer a `502` with an HTML page, so it reads the body as text before it parses it.

<CodeGroup>
  ```ts TypeScript theme={"theme":"css-variables"}
  import { randomUUID } from "node:crypto";

  const RETRYABLE = new Set([429, 500, 502, 503, 504]);
  const MAX_WAIT_MS = 60_000;

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

  async function leapRequest(path: string, init: RequestInit = {}, attempts = 5) {
    for (let attempt = 1; ; attempt++) {
      // Exponential backoff with full jitter: up to 1 s, 2 s, 4 s and so on.
      let waitMs = Math.random() * Math.min(MAX_WAIT_MS, 1_000 * 2 ** (attempt - 1));
      let response: Response | undefined;

      try {
        response = await fetch(`https://api.tryleap.ai${path}`, {
          ...init,
          headers: { "x-api-key": process.env.LEAP_API_KEY ?? "", ...init.headers },
          signal: AbortSignal.timeout(90_000),
        });
      } catch (error) {
        // The connection dropped or the request timed out.
        if (attempt >= attempts) throw error;
      }

      if (response) {
        const retryAfter = Number(response.headers.get("retry-after"));
        if (retryAfter > 0) waitMs = retryAfter * 1_000;

        if (!RETRYABLE.has(response.status) || attempt >= attempts || waitMs > MAX_WAIT_MS) {
          const text = await response.text();
          let body;
          try {
            body = JSON.parse(text);
          } catch {
            // Not JSON, such as a proxy's HTML error page.
          }

          if (response.ok) return body;
          throw new Error(body?.error ? `${body.error.code}: ${body.error.message}` : `HTTP ${response.status}`);
        }
      }

      await sleep(waitMs);
    }
  }

  // One key per new run, sent again with every retry of it.
  const generation = await leapRequest("/v1/generations", {
    method: "POST",
    headers: { "content-type": "application/json", "idempotency-key": randomUUID() },
    body: JSON.stringify({
      model: "black-forest-labs/flux-2-pro",
      input: { prompt: "A fox in the snow, watercolor" },
    }),
  });
  ```

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

  import requests

  RETRYABLE = {429, 500, 502, 503, 504}
  MAX_WAIT = 60


  def leap_request(method, path, attempts=5, **kwargs):
      headers = {"x-api-key": os.environ["LEAP_API_KEY"], **kwargs.pop("headers", {})}
      kwargs.setdefault("timeout", 90)

      for attempt in range(1, attempts + 1):
          # Exponential backoff with full jitter: up to 1 s, 2 s, 4 s and so on.
          wait = random.uniform(0, min(MAX_WAIT, 2 ** (attempt - 1)))
          try:
              response = requests.request(
                  method, f"https://api.tryleap.ai{path}", headers=headers, **kwargs
              )
          except requests.RequestException:
              # The connection dropped or the request timed out.
              if attempt == attempts:
                  raise
              time.sleep(wait)
              continue

          retry_after = response.headers.get("retry-after", "")
          if retry_after.isdigit():
              wait = int(retry_after)

          if response.status_code in RETRYABLE and attempt < attempts and wait <= MAX_WAIT:
              time.sleep(wait)
              continue

          try:
              body = response.json()
          except ValueError:
              # Not JSON, such as a proxy's HTML error page.
              body = None

          if response.ok:
              return body
          if isinstance(body, dict) and "error" in body:
              raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
          raise RuntimeError(f"HTTP {response.status_code}")


  # One key per new run, sent again with every retry of it.
  generation = leap_request(
      "POST",
      "/v1/generations",
      headers={"idempotency-key": str(uuid.uuid4())},
      json={
          "model": "black-forest-labs/flux-2-pro",
          "input": {"prompt": "A fox in the snow, watercolor"},
      },
  )
  ```
</CodeGroup>

## Rate limits

| Limit | Budget |
| - | - |
| Requests from one IP address | 300 a minute |
| Requests with one API key | 600 a minute |
| Video and sound uploads per workspace | 5 GB a day, and 10 in flight at once |

Every answer carries your IP budget in the `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` headers. `RateLimit-Reset` is the number of seconds until the window resets. To stay under the limits, wait in the request with `prefer: wait` instead of polling fast, poll each run every few seconds at most, and start many runs with one [batch](/docs/generations#batches).


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