# TypeScript SDK

**You will:** Call the Lelemon API from TypeScript or JavaScript with typed errors, safe retries and verified webhooks.

**You need:**
- Node.js 20 or later, or any runtime with Web APIs
- An API key (lel_live_)

The SDK is generated from the v2 contract, so every route, field and enum in the [API reference](/api/v2) has a typed method. It has no runtime dependencies, ships ESM and CommonJS with types for both, and runs on Node.js 20 and later, Cloudflare Workers, Deno and Bun.

## Works in two minutes

```bash
npm install @lelemon-ai/sdk
export LELEMON_API_KEY=lel_live_...
```

Save this as `hello.mjs` and run `node hello.mjs`:

```ts
import { Lelemon } from '@lelemon-ai/sdk';

const lelemon = new Lelemon({ apiKey: process.env.LELEMON_API_KEY });

const me = await lelemon.me.retrieve();
console.log(me.organization.name, me.scopes);

const agent = await lelemon.agents.create({
  name: 'Order bot',
  instructions: 'You answer questions about orders in one short sentence.',
  llmConfig: { model: 'anthropic/claude-sonnet-5.5' },
});
console.log(agent.id, agent.status);
```

Expected: your organization, the scopes of the key, and a new agent in `draft`. The rest of this page builds on that client.

## Method names

Method names are the `operationId` of each route in the contract: `agents.create`, `cases.takeControl`, `webhookEndpoints.create`, `pipelines.stages.update`. Every method is `method(...pathParams, body?, options?)`. A route without a body is `method(...pathParams, options?)`. Types are exported under `Types`:

```ts
import type { Types } from '@lelemon-ai/sdk';

const describe = (agent: Types.Agent) => `${agent.name} v${agent.version}`;
```

## Client options

| Option | Default | Use |
| --- | --- | --- |
| `apiKey` | `LELEMON_API_KEY` | The key. The constructor throws without one |
| `baseUrl` | `https://api.lelemon.cl` | Another API, such as a local one |
| `subaccount` | none | Sends `Lelemon-Subaccount` on every call |
| `maxRetries` | `2` | Retries on `429`, `500`, `502`, `503` and `504`, and on network errors |
| `timeoutMs` | `60000` | Time limit of each attempt |
| `retryDelayMs` | `500` | Base delay between retries. `Retry-After` wins when present |
| `fetch` | global `fetch` | Your own implementation |

## Request options

The last argument of every method is the only options object. It carries `query`, `idempotencyKey`, `subaccount`, `signal` and `headers`.

```ts
const controller = new AbortController();

const contact = await lelemon.contacts.create(
  { name: 'Ana Rojas', externalRef: 'cust-88123' },
  { idempotencyKey: 'create-cust-88123', signal: controller.signal, headers: { 'X-Trace-Id': 'abc-1' } },
);
```

## Idempotency

Every `POST` sends an `Idempotency-Key`. The SDK generates one when you do not pass `idempotencyKey`, and reuses it across its own retries, so a retry after a timeout never does the work twice. Pass your own key when the retry may come from a different process: a job that runs twice must send the same key both times. See [Idempotency](/basics/idempotency).

## Try before you do: `dryRun`

Routes that send a message, cost money or are hard to undo accept `dryRun`. The call runs the same checks as the real one and answers what would happen, with `dryRun: true`; nothing is saved or sent.

```ts
const preview = await lelemon.agents.activate('agt_...', { query: { dryRun: true } });
console.log(preview.dryRun, preview.readiness.missing);
```

## Pagination

Lists return a promise that is also an async iterable: loop over it to read every page, or `await` it to get the first page with `data`, `hasMore` and `nextCursor`.

```ts
for await (const item of lelemon.contacts.list({ query: { limit: 50 } })) {
  console.log(item.id);
}

const firstPage = await lelemon.contacts.list({ query: { limit: 10 } });
console.log(firstPage.data.length, firstPage.hasMore, firstPage.nextCursor);

const firstHundred = await lelemon.contacts.list().toArray({ max: 100 });
```

The cursor is opaque. Pass `cursor` in `query` to continue a page you stored. See [Lists](/basics/lists).

## Errors

Every API error is a typed class. Branch on `code`, never on the text of `detail`.

```ts
import { ConflictError, LelemonApiError, NotFoundError, RateLimitError } from '@lelemon-ai/sdk';

try {
  await lelemon.agents.retrieve('agt_...');
} catch (error) {
  if (error instanceof NotFoundError) {
    console.log(error.status, error.code, error.requestId);
  } else if (error instanceof ConflictError && error.code === 'agent_version_conflict') {
    console.log('read the agent again and retry');
  } else if (error instanceof RateLimitError) {
    console.log('wait', error.retryAfterSeconds);
  } else if (error instanceof LelemonApiError) {
    console.log(error.code, error.param, error.fix);
  } else {
    throw error;
  }
}
```

`LelemonApiError` carries `status`, `code` (typed as the codes of the [error catalog](/errors)), `param`, `detail`, `docUrl`, `requestId`, `fix`, `budget` and `retryAfterSeconds`. One subclass per family extends it:

| Class | Status |
| --- | --- |
| `BadRequestError` | 400 |
| `AuthenticationError` | 401 |
| `PermissionError` | 403 |
| `NotFoundError` | 404 |
| `ConflictError` | 409 |
| `UnprocessableError` | 422 |
| `RateLimitError` | 429 |
| `ApiServerError` | 5xx |

Outside the API: `LelemonConnectionError` covers network failures, `LelemonTimeoutError` an attempt that took longer than `timeoutMs`, `LelemonAbortError` a call you aborted with `signal` (which also stops the wait before a retry), and `LelemonSignatureVerificationError` a webhook that failed verification. All extend `LelemonError`. Send the `requestId` to support when you need help with a call.

## Timeouts and retries

An attempt that takes longer than `timeoutMs` throws `LelemonTimeoutError`. Before that, the SDK retries `429`, `500`, `502`, `503`, `504` and network errors up to `maxRetries` times, waiting `Retry-After` when the API sent it and an exponential delay with jitter otherwise. A `4xx` other than `429` is never retried. Because every `POST` carries an idempotency key, retrying is safe.

```ts
const patient = new Lelemon({
  apiKey: process.env.LELEMON_API_KEY,
  maxRetries: 4,
  timeoutMs: 20_000,
  retryDelayMs: 1_000,
});
```

## Subaccounts

A platform acts on a customer's subaccount with its own key. Set it for one call, or scope a whole client:

```ts
const customerAgents = await lelemon.agents.list({ subaccount: 'sub_...' });
await lelemon.contacts.create({ name: 'Ana Rojas' }, { subaccount: 'sub_...' });

const customer = lelemon.withSubaccount('sub_...');
const agents = await customer.agents.list();
```

`withSubaccount` returns a new client with the same options and `Lelemon-Subaccount` set on every call. See [Run Lelemon for your customers](/guides/platforms).

## Webhooks

`Lelemon.webhooks.constructEvent(rawBody, signatureHeader, secret)` verifies the signature and returns the event, typed by its `type`. It is asynchronous, takes the raw body (a string, a `Buffer` or a `Uint8Array`) and throws `LelemonSignatureVerificationError` when the signature, the timestamp or the secret do not match. `verifyHeader` only verifies. Both accept `{ toleranceSeconds }`, which defaults to 300.

```ts
import { createServer } from 'node:http';
import { Lelemon, LelemonSignatureVerificationError } from '@lelemon-ai/sdk';

const receiver = createServer(async (request, response) => {
  const chunks: Buffer[] = [];
  for await (const chunk of request) chunks.push(chunk as Buffer);
  try {
    const event = await Lelemon.webhooks.constructEvent(
      Buffer.concat(chunks),
      String(request.headers['lelemon-signature'] ?? ''),
      process.env.LELEMON_WEBHOOK_SECRET!,
    );
    response.writeHead(200).end();
    if (event.type === 'message.created') console.log(event.data.text);
  } catch (error) {
    if (error instanceof LelemonSignatureVerificationError) response.writeHead(400).end();
    else response.writeHead(500).end();
  }
});
receiver.listen(3000);
```

Always verify against the raw body: a body that was parsed and serialized again will not match. More in [Verify signatures](/webhooks/verify).

## Helpers for your own endpoints

The SDK also verifies the calls Lelemon makes to **your** systems:

- `verifyRequest` checks the signature of an [action](/guides/actions-and-mcp#verify-the-signature) call and `deriveToolSecret` derives a per-tool secret.
- `verifyMcpAssertion(token, secrets, { audience, expectedTool, expectedArgs })` checks the JWT that Lelemon sends to your MCP server when you turn on its run identity. See [What an MCP server of yours receives](/guides/actions-and-mcp#what-an-mcp-server-of-yours-receives).
- `connector` builds and validates the documents of a data connector. See [Connect your back office](/guides/data-connectors#build-it-with-the-sdk) and the [CLI](/tools/cli).

## What's next

- [Set up an account from zero](/get-started/setup-from-zero) shows every call with the SDK.
- [Command line](/tools/cli) to check and serve a data connector.
- [MCP server](/tools/mcp) to drive Lelemon from Claude or Cursor.
