Lelemon
  • Get started
  • Use cases
  • Guides
  • API basics
  • Webhooks
  • SDK, CLI and MCP
  • API reference
  • Changelog
  • v1 (legacy)
TypeScript SDKCommand lineMCP server
powered by Zudoku
SDK, CLI and MCP

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

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

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

Code
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:

Code
import type { Types } from '@lelemon-ai/sdk'; const describe = (agent: Types.Agent) => `${agent.name} v${agent.version}`;

Client options

OptionDefaultUse
apiKeyLELEMON_API_KEYThe key. The constructor throws without one
baseUrlhttps://api.lelemon.clAnother API, such as a local one
subaccountnoneSends Lelemon-Subaccount on every call
maxRetries2Retries on 429, 500, 502, 503 and 504, and on network errors
timeoutMs60000Time limit of each attempt
retryDelayMs500Base delay between retries. Retry-After wins when present
fetchglobal fetchYour own implementation

Request options

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

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

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.

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

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

Errors

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

Code
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), param, detail, docUrl, requestId, fix, budget and retryAfterSeconds. One subclass per family extends it:

ClassStatus
BadRequestError400
AuthenticationError401
PermissionError403
NotFoundError404
ConflictError409
UnprocessableError422
RateLimitError429
ApiServerError5xx

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.

Code
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:

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

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.

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

Helpers for your own endpoints

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

  • verifyRequest checks the signature of an action 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.
  • connector builds and validates the documents of a data connector. See Connect your back office and the CLI.

What's next

  • Set up an account from zero shows every call with the SDK.
  • Command line to check and serve a data connector.
  • MCP server to drive Lelemon from Claude or Cursor.
Command line
On this page
  • Works in two minutes
  • Method names
  • Client options
  • Request options
  • Idempotency
  • Try before you do: dryRun
  • Pagination
  • Errors
  • Timeouts and retries
  • Subaccounts
  • Webhooks
  • Helpers for your own endpoints
  • What's next
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript
TypeScript