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

# Errors and retries

> What an error looks like, what each status means, and how to retry without starting a job twice.

When a request fails, Pulse answers with a `4xx` or `5xx` status and a [problem details](https://www.rfc-editor.org/rfc/rfc9457) body, sent as `application/problem+json`. Its `code` is a stable name for the error, such as `FILE_TOO_LARGE`: branch on it, never on `title` or `detail`, which can change. [Error codes](https://docs.runpulse.com/errors.md) lists every code with what to do about it, and each operation in the [API reference](https://docs.runpulse.com/api-reference/v2/parse/run-parse.md) lists the codes it can answer.

```json theme={null}
{
  "type": "https://docs.runpulse.com/errors/file-too-large",
  "title": "File too large",
  "status": 413,
  "instance": "urn:pulse:request:req-6f1c2a9e-3b7d-4c55-9a8e-2d4f0b1c7e93",
  "code": "FILE_TOO_LARGE",
  "request_id": "req-6f1c2a9e-3b7d-4c55-9a8e-2d4f0b1c7e93",
  "retryable": false,
  "max_size_mb": 100,
  "file_size_mb": 140.5
}
```

| Member | What it holds |
| - | - |
| `code` | The error's stable name. Branch on it. |
| `type` | The code's page, which says what it means and what to do. |
| `title`, `detail` | A summary of the code, and what happened this time. Show them, don't parse them. |
| `request_id` | The request that failed. Every response also sends it as the `X-Request-Id` header. Quote it to [support](https://docs.runpulse.com/v2/support.md). |
| `retryable` | Whether the same request may succeed if you send it again. For a `run`, read the rules below first. |
| `errors` | Each part of the request that was refused, with its `field` and `issue`. |
| `parse_id`, `extract_id`, … | The job the request recorded before it failed, if it recorded one. |
| `retry_after` | The seconds to wait before retrying, as the `Retry-After` header says. |

Some codes add members of their own, such as `max_size_mb` above; the code's page lists them.

A `run` is a call that starts a job, such as `POST /parse` or `client.parse.run`. A job that fails after it starts reports the same problem in the `error` field of its [status](https://docs.runpulse.com/v2/jobs.md), and its result route answers with it.

## What each status means

| Status | What it means | What to do |
| - | - | - |
| `400` | The request breaks a rule: a field is missing, unknown or invalid, a `page_range` doesn't fit the document, a `file_url` isn't allowed, or a `parse_id` can't be read by this endpoint. An option your organization doesn't have answers as an unknown field or value. | Fix the request. `errors` names each field at fault. |
| `401` | The API key is missing, unknown or inactive. | Check the key, or [create a new one](https://docs.runpulse.com/v2/authentication.md). |
| `402` | Your plan or billing doesn't cover the request: the trial ended, a credit or plan limit was reached, or a payment is needed. | Check your plan and billing in the dashboard. |
| `403` | The account is suspended, your email domain isn't allowed, or the key may not do this. | Use a key with access, or contact [support](https://docs.runpulse.com/v2/support.md). |
| `404` | Your organization has no job, batch, config or webhook with that id, or no route answers the path. An endpoint your organization doesn't have, such as Tables before it's turned on, answers as a route that doesn't exist. | Check the id against the one Pulse returned, or ask [support](https://docs.runpulse.com/v2/support.md) to turn the endpoint on. |
| `409` | On a job's routes, the job's state doesn't allow the call: it hasn't finished yet, it has already ended, or it was canceled while a call waited for it. | Read the job's status. Don't start the job again. |
| `410` | The result is gone: it expired, it was deleted when first read, or its job was canceled. | Run the job again. |
| `413` | The file, or a text part of a multipart form, is too large. | Send a large PDF or TIFF by `file_url`. See [Files and limits](https://docs.runpulse.com/v2/files.md). |
| `415` | The file's type isn't supported. | Convert it to a [supported type](https://docs.runpulse.com/v2/files.md). |
| `422` | The file can't be used: it's empty, corrupt or unsafe, the `file_url` couldn't be downloaded, or the document has nothing to process. | Fix the file or the URL. `reason` says why, when there's more than one. |
| `429` | A rate limit was reached, such as on creating webhooks. | Wait the seconds in `Retry-After`, then retry. |
| `500` | Processing failed, or something went wrong on Pulse's side. | Before you resend a `run`, check for its job, as described below. If the error keeps coming back, contact [support](https://docs.runpulse.com/v2/support.md) with its `request_id`. |
| `503` | Pulse is at capacity, or a service the request needs is briefly unavailable. | Wait the seconds in `Retry-After` when it's sent, otherwise back off. Before you resend a `run`, check for its job, as described below. |
| `504` | Pulse timed out downloading your `file_url`, or the request took longer than Pulse waits. | Retry later, or host the file somewhere that answers faster. For a long document, send `async: true`. |
| `502`, or a timeout with no body | No answer came back in time, from Pulse or from a proxy in between. The job may still be running. | Before you resend a `run`, check for its job, as described below. |
| Any other `4xx` | The request can't succeed as sent, such as a method the path doesn't take (`405`). | Fix the request. Sent again unchanged, it fails the same way. |

***

## Retry without starting a job twice

A failed `run` may have started its job. The SDKs never retry on their own (`max_retries` and `maxRetries` have no effect), so retry in your own code by these rules:

* **Reads start nothing.** Send a status or result read again after a network error, or when `retryable` is `true`.
* **Fix a request whose `retryable` is `false` before you resend it.** Sent again unchanged, it fails the same way.
* **If the error names a job, read that job's status.** The problem holds the job's id, such as `parse_id`, when Pulse recorded the job before the request failed. If the job is `queued`, `processing` or `completed`, poll it instead of starting another. If it's `failed` or `canceled`, start a new one.
* **If the error names no job, go by its `code`.** After `CAPACITY_EXCEEDED`, `RATE_LIMIT_EXCEEDED` or `FILE_DOWNLOAD_TIMEOUT`, no job is running, so you can send the `run` again. After any other `5xx`, or a timeout or dropped connection on your side, a job may still be running, so don't.
* **Send long work with `async: true`**, which returns the job's id at once, and **log every job id** so a retry can check what already ran.

> [!WARNING]
>
> Sending a `run` again after a timeout can start a second job, and both are billed when they complete.

This Python helper follows those rules.

```python Python theme={null}
import random
import time

from pulse.core.api_error import ApiError

NOTHING_STARTED = {"CAPACITY_EXCEEDED", "RATE_LIMIT_EXCEEDED", "FILE_DOWNLOAD_TIMEOUT"}


def problem(error):
    """The error's problem details, as a dict."""
    body = error.body.model_dump() if hasattr(error.body, "model_dump") else error.body
    return body if isinstance(body, dict) else {}


def start(endpoint, id_field, **request):
    """Start one job, and retry only when no job is left running.

    `endpoint` is a client group, such as `client.parse`; `id_field` names its id, such as "parse_id".
    """
    for attempt in range(5):
        try:
            return endpoint.run(**request)
        except ApiError as error:
            body = problem(error)
            if not body.get("retryable") or attempt == 4:
                raise
            job_id = body.get(id_field)
            if job_id is None and body.get("code") not in NOTHING_STARTED:
                raise  # a job may still be running
            if job_id is not None and endpoint.get(job_id).status not in ("failed", "canceled"):
                raise  # the job exists: poll it instead of starting another
            wait = body.get("retry_after")
            time.sleep(wait if wait else 2**attempt + random.random())


job = start(client.parse, "parse_id", file_url="https://example.com/a.pdf", config={"model": "ultra-2"}, async_=True)
```

`client` is the one from [SDKs](https://docs.runpulse.com/v2/sdks.md). A timeout or connection error isn't an `ApiError`, so the helper lets it through without a retry. In TypeScript, the same loop catches `PulseError`, which has `statusCode`, `body` and `rawResponse.headers`; look for each member under both spellings, such as `parse_id` and `parseId`.

## Warnings

A completed result can carry `warnings` about problems that didn't stop the job but may affect its result, such as `PAGES_FAILED` when some pages of a long document couldn't be parsed. Each has a stable `code` and a `message`: log them, and match on `code`.

> [card] [Jobs and results](https://docs.runpulse.com/v2/jobs.md)
>
> Statuses, polling and result retention.

> [card] [SDKs](https://docs.runpulse.com/v2/sdks.md)
>
> The clients, their errors and their timeouts.


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