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

# SDKs

> Install the Python or TypeScript SDK, create a client, and call any endpoint.

The Pulse SDKs for Python and TypeScript cover every route of the API. They send your API key and the `Pulse-Version: 2` header on every request, and return typed results.

Install `pulse-ai` from PyPI for Python, or `@pulse-software/sdk` from npm for TypeScript:

```bash Python theme={null}
pip install pulse-ai
```

```bash TypeScript theme={null}
npm install @pulse-software/sdk
```

## Create a client

This code creates a client with the API key in the `PULSE_API_KEY` environment variable:

```python Python theme={null}
import os
from pulse import Pulse

client = Pulse(api_key=os.environ["PULSE_API_KEY"])
```

```typescript TypeScript theme={null}
import { PulseClient } from "@pulse-software/sdk";

const client = new PulseClient({ apiKey: process.env.PULSE_API_KEY! });
```

For `asyncio`, Python also has `AsyncPulse`. It takes the same arguments and has the same calls, which you `await`.

## Call an endpoint

Each endpoint is a group on the client, and every group has the same four calls. For Parse:

| Python | TypeScript | HTTP |
| - | - | - |
| `client.parse.run(...)` | `client.parse.run({ ... })` | `POST /parse` |
| `client.parse.get(parse_id)` | `client.parse.get({ parseId })` | `GET /parse/{parse_id}` |
| `client.parse.get_result(parse_id)` | `client.parse.getResult({ parseId })` | `GET /parse/{parse_id}/result` |
| `client.parse.cancel(parse_id)` | `client.parse.cancel({ parseId })` | `DELETE /parse/{parse_id}` |

`classify`, `extract`, `split`, `tables`, `charts`, `pipeline` and `parse.batch` work the same way. Saved configs sit in `client.<endpoint>.configs`, such as `client.extract.configs`. Webhooks sit in `client.webhook`, usage in `client.usage.get()`, and organization settings in `client.settings.org`.

## Names and types

* Python uses the API's field names, with a trailing underscore where a name clashes with Python: `async_`, `from_` on `usage.get`, and `json_` on a table's `content`.
* TypeScript uses camelCase, such as `fileUrl`, and sends the API's names for you.
* A `file` is an open file or bytes in Python, and a stream, `Buffer` or `Blob` in TypeScript.
* To read a response's headers, such as the `Link` header on a page of a batch's manifest, use the raw response. In Python, `client.parse.batch.with_raw_response.get_result(...)` returns `.data` and `.headers`. In TypeScript, `.withRawResponse()` on a call returns `{ data, rawResponse }`.

## Errors

A failed request raises an error that carries the response's status, headers and body. In Python it's `ApiError`, from `pulse.core.api_error`, with `status_code`, `headers` and `body`. In TypeScript it's `PulseError`, with `statusCode`, `rawResponse.headers` and `body`. The body is the error's problem details: branch on its `code`, and quote its `request_id` to support. [Errors and retries](https://docs.runpulse.com/v2/errors.md) explains each status and what to do about it.

## Retries

The SDKs never retry a request on their own, and the `max_retries` (Python) and `maxRetries` (TypeScript) options have no effect. Add retries in your own code.

Reads are safe to retry: reading a job's status or result never starts a job or costs credits. In an organization that deletes each result the first time it's read, a retried result read gets `410` if the first read reached Pulse. This helper retries a read after a network error, a `429` or a `5xx`, and honors `Retry-After`:

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

import httpx
from pulse.core.api_error import ApiError

RETRY_STATUSES = {429, 500, 502, 503, 504}


def with_retries(read, *args, attempts=5):
    """Call a read, such as client.parse.get, again after a network error, a 429 or a 5xx."""
    for attempt in range(attempts):
        try:
            return read(*args)
        except (ApiError, httpx.TransportError) as error:
            status = getattr(error, "status_code", None)
            if attempt == attempts - 1 or (status is not None and status not in RETRY_STATUSES):
                raise
            wait = (getattr(error, "headers", None) or {}).get("retry-after")
            time.sleep(float(wait) if wait else 2**attempt)


status = with_retries(client.parse.get, parse_id)
```

```typescript TypeScript theme={null}
import { PulseError } from "@pulse-software/sdk";

const RETRY_STATUSES = new Set([429, 500, 502, 503, 504]);

async function withRetries<T>(read: () => Promise<T>, attempts = 5): Promise<T> {
  for (let attempt = 0; ; attempt++) {
    try {
      return await read();
    } catch (error) {
      const status = error instanceof PulseError ? error.statusCode : undefined;
      if (attempt === attempts - 1 || (status !== undefined && !RETRY_STATUSES.has(status))) throw error;
      const wait = error instanceof PulseError ? Number(error.rawResponse?.headers.get("retry-after")) : 0;
      await new Promise((resolve) => setTimeout(resolve, (wait || 2 ** attempt) * 1000));
    }
  }
}

const status = await withRetries(() => client.parse.get({ parseId }));
```

`client` is the client from above, and the parse id is one a run returned.

A job's `run` is different. When a run fails with a `5xx` or times out, Pulse may already have started the job:

| The run failed with | Before you send it again |
| - | - |
| An error whose body names a job, such as its `parse_id` | Read that job's status. If it's `queued`, `processing` or `completed`, poll it instead of starting another. If it `failed` or was `canceled`, send the run again, after fixing the request if the error was a `4xx`. |
| `CAPACITY_EXCEEDED` or `RATE_LIMIT_EXCEEDED` | Wait for the seconds in the `Retry-After` header. No job is running, so you can send the run again. |
| `FILE_DOWNLOAD_TIMEOUT` that names no job | Pulse timed out downloading your `file_url` and started nothing. Send the run again, or host the file somewhere that answers faster. |
| A timeout, a dropped connection, or a `5xx` whose body names no job | Pulse may have started the job, and there's no id to check. Don't send the run again automatically. |
| Any other `4xx` | Fix the request. Sent again unchanged, it fails the same way. |

Send long work with `async: true`. The run then returns the job's id within seconds, so a timeout is unlikely.

> [!WARNING]
>
> Never send a run again after a `5xx` or a timeout without first checking whether its job exists. A second run starts a second job, and both are billed when they complete.

## Timeouts

Calls have no timeout by default, so a call without `async` waits until its job ends or the connection closes. To set one, pass `timeout` (Python) or `timeoutInSeconds` (TypeScript), in seconds, when you create the client.


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