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

# TypeScript and Python SDKs

> Official clients generated from the OpenAPI document: install, authentication, errors, pagination, idempotency keys and webhook signature verification.

Two official client libraries wrap the API: `@sempera/sdk` for TypeScript and `sempera-sdk` for Python. Both are generated from the live OpenAPI document (`https://api.sempera.org/openapi.json`), so each has one method for every operation the document lists, plus a small hand-written core for authentication, errors, retries, idempotency keys, paging and webhook verification.

They are **not published to npm or PyPI yet**. Today you install them from the Sempera source tree (`04-delivery/api-sdks/` in the repository) or vendor the folder; [Web SDK](https://docs.sempera.org/web-sdk.md) still applies for the browser. The SDKs call only routes that `openapi.json` lists.

## Install

```bash theme={null}
# TypeScript (Node 22.18 or newer)
cd api-sdks/typescript && npm install && npm run build

# Python (3.9 or newer, no dependencies)
pip install ./api-sdks/python
```

## Authentication

Send your API key as a bearer token ([Authentication and credentials](https://docs.sempera.org/authentication.md)). Pass it to the constructor, or set `SEMPERA_API_KEY` and omit it. Keep the key on your server.

```python Python theme={null}
from sempera import Sempera

client = Sempera("sempera_sk_...")   # or Sempera() with SEMPERA_API_KEY set
me = client.get_me()
print(me)
```

```ts TypeScript theme={null}
import { Sempera } from "@sempera/sdk";

const client = new Sempera({ apiKey: process.env.SEMPERA_API_KEY });
const me = await client.getMe();
```

Methods are named after the operation id in the contract: `createAgent` in TypeScript, `create_agent` in Python. Path and query parameters are named exactly as in `openapi.json`. A request body is passed as a plain object or dict.

## Create and publish an agent

```python Python theme={null}
agent = client.create_agent({"name": "Northwind Barbers"})
published = client.publish_agent(agent["id"], {})
```

```ts TypeScript theme={null}
const agent = await client.createAgent({ name: "Northwind Barbers" });
const published = await client.publishAgent({ agent_id: agent.id }, {});
```

## Place a prompt call

`createCall` / `create_call` places one outbound call. `mode: "prompt"` uses only the prompt. `agent_id` is still the line (number, consent, do-not-contact, calling hours, caps and billing). Send `task`, and optionally `first_sentence`, `voice` and `language`. Do not send `agent_version` or `production` with `mode: "prompt"`. The call still requires `Idempotency-Key` and `consent_basis`. Passing your own key makes a retry safe across process restarts.

```python Python theme={null}
call = client.create_call({
    "agent_id": "northwind-barbers",
    "mode": "prompt",
    "phone_number": "+141****0123",
    "task": "Confirm Friday at 3pm. Ask whether that time still suits.",
    "first_sentence": "Hello, this is a quick call about Friday.",
    "voice": "voice_example",
    "language": "en",
    "consent_basis": "This person asked Northwind Barbers to call them about Friday.",
}, idempotency_key="friday-3pm-ada")
```

```ts TypeScript theme={null}
const call = await client.createCall({
  agent_id: "northwind-barbers",
  mode: "prompt",
  phone_number: "+141****0123",
  task: "Confirm Friday at 3pm. Ask whether that time still suits.",
  first_sentence: "Hello, this is a quick call about Friday.",
  voice: "voice_example",
  language: "en",
  consent_basis: "This person asked Northwind Barbers to call them about Friday.",
}, { idempotencyKey: "friday-3pm-ada" });
```

## Errors

Every refusal is the one error envelope ([Errors and limits](https://docs.sempera.org/errors-and-limits.md)). The SDK raises a typed error that carries the stable `code`, the optional `reason` and `field`, the `request_id` to quote to support and the HTTP `status`. Branch on `code` or `reason`, never on the message.

| Error | Status | Meaning |
| - | - | - |
| `ValidationError` | 400 | A field is missing or wrong (`field` names it) |
| `AuthenticationError` | 401 | No credential, or an unknown, expired or revoked key |
| `PermissionDeniedError` | 403 | The key lacks the scope, or the role cannot do this |
| `NotFoundError` | 404 | No such resource in your scope |
| `ConflictError` | 409 | A rule refused it; `reason` may be finer |
| `IdempotencyKeyReusedError` | 409 | The same idempotency key was sent with a different request |
| `RateLimitError` | 429 | Wait `retry_after` seconds (`retryAfterSeconds` in TypeScript) |
| `ServerError` | 5xx | The API or a provider behind it failed |
| `ConnectionFailure` | none | No answer: DNS, TLS, or a timeout |

```python Python theme={null}
from sempera import NotFoundError, SemperaError

try:
    client.get_agent("agt_missing")
except NotFoundError as err:
    print(err.code, err.request_id)
except SemperaError as err:
    print(err.status, err.code, err.reason)
```

```ts TypeScript theme={null}
import { NotFoundError, SemperaError } from "@sempera/sdk";

try {
  await client.getAgent({ agent_id: "agt_missing" });
} catch (err) {
  if (err instanceof NotFoundError) console.log(err.code, err.requestId);
  else if (err instanceof SemperaError) console.log(err.status, err.code, err.reason);
  else throw err;
}
```

The SDK retries what is safe to retry, two times by default, with exponential backoff and jitter: a `429` (waiting the `Retry-After` it sends), a `502`, a `504`, a `503` with code `service_unavailable`, and a dropped connection. A `503 owner_setup_required` is never retried because waiting does not fix it. A write that has no idempotency key is retried only after a `429`, which means it was not processed. Change the count with `max_retries` / `maxRetries` on the client or on one call.

## Pagination

List routes take `limit` and `cursor` and answer `{"object": "list", "data": [...], "has_more": true, "next_cursor": "..."}` ([Errors and limits](https://docs.sempera.org/errors-and-limits.md)). `paginate` follows `next_cursor` until `has_more` is false and yields every item.

```python Python theme={null}
from sempera import paginate

for agent in paginate(lambda cursor: client.list_agents(limit=100, cursor=cursor)):
    print(agent["id"])
```

```ts TypeScript theme={null}
import { paginate } from "@sempera/sdk";

for await (const agent of paginate((cursor) => client.listAgents({ limit: 100, cursor }))) {
  console.log(agent.id);
}
```

`paginate` follows the native envelope only. A compatibility route keeps its own envelope ([Errors and limits](https://docs.sempera.org/errors-and-limits.md)), so `paginate` raises on one instead of silently yielding nothing; page those by hand with the `cursor` it returns.

## Idempotency keys

Operations that accept an `Idempotency-Key` (listed in [Errors and limits](https://docs.sempera.org/errors-and-limits.md)) get one automatically: the SDK generates a key for the call and sends the same key on every retry, so a retried `POST` cannot act twice. Pass your own to make a retry safe across process restarts, for instance one derived from your order id. A key lives 24 hours; the same key with a different request is a `409 idempotency_key_reused`.

```python Python theme={null}
session = client.create_session({"agent_id": "agt_northwind", "end_user_id": "cust_1042"},
                                idempotency_key="booking-1042")
```

```ts TypeScript theme={null}
const session = await client.createSession(
  { agent_id: "agt_northwind", end_user_id: "cust_1042" },
  { idempotencyKey: "booking-1042" },
);
```

## Webhook signature verification

Verify every delivery against the raw request body before you parse it ([Webhooks](https://docs.sempera.org/webhooks.md)). The helper checks the `X-Sempera-Signature` header (`t=<unix seconds>,v1=<hex HMAC-SHA256 over "<t>." + body>`) in constant time and rejects a timestamp more than 300 seconds from your clock. It raises `WebhookSignatureError`; answer `400` and process nothing. Then drop an event id you have already handled.

```python Python theme={null}
from sempera import WebhookSignatureError, construct_event

def handle(raw_body: bytes, signature_header: str):
    try:
        event = construct_event("whsec_...", raw_body, signature_header)
    except WebhookSignatureError:
        return 400
    print(event["type"], event["id"])
    return 200
```

```ts TypeScript theme={null}
import { constructEvent, WebhookSignatureError } from "@sempera/sdk";

export function handle(rawBody: string, signatureHeader: string): number {
  try {
    const event = constructEvent("whsec_...", rawBody, signatureHeader);
    console.log(event.type, event.id);
    return 200;
  } catch (err) {
    if (err instanceof WebhookSignatureError) return 400;
    throw err;
  }
}
```

The webhooks guide publishes a known answer (secret, timestamp, body and signature). Both SDKs are tested against it, and you can use it to test your own handler.

## Staying in step with the API

The operation layers are regenerated from the contract with one command, `python3 06-tools/build-sdks.py`, and a test in the Sempera repository fails when a route is added, renamed or retyped without regenerating. `GET /v1/me` and the `API_VERSION` constant tell you which contract version your copy was built from.


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