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

# TypeScript SDK

> Install and use the @custral/sdk server-side REST client.

**`@custral/sdk`** is the official **server-side** client for the [`/v1` REST API](/dev/api-reference/overview): records, conversations, objects, identity, and Stripe-style webhook verification. Zero runtime dependencies (native `fetch` + `node:crypto`), ESM + CJS, fully typed.

<Warning>
  This is a **secret-key** SDK. A secret key (`sk_…`) must never ship to a browser. For usage tracking with a **publishable** key use [`@custral/js`](/dev/sdks/browser), and for the chat widget use [`@custral/widget`](/dev/sdks/react).
</Warning>

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @custral/sdk
  ```

  ```bash pnpm theme={null}
  pnpm add @custral/sdk
  ```

  ```bash bun theme={null}
  bun add @custral/sdk
  ```
</CodeGroup>

## Quickstart

Create a key in **Settings → Applications** and grant it the scopes each endpoint needs.

```ts theme={null}
import {Custral} from "@custral/sdk";

const custral = new Custral({apiKey: process.env.CUSTRAL_API_KEY!}); // sk_...

// Create a record on any object (requires records:write).
const {id, ignoredFields} = await custral.records.create({
  object: "contacts",
  fields: {email: "jane@acme.com", full_name: "Jane Doe"},
});
```

## Configuration

```ts theme={null}
const custral = new Custral({
  apiKey: process.env.CUSTRAL_API_KEY!, // required, a secret key (sk_...)
  baseUrl: "https://api.custral.com", // default; use http://localhost:8080 locally
  maxRetries: 2, // retry safe failures (rate limits, 5xx on reads, connection errors)
  timeout: 30_000, // per-request timeout in ms
  webhookSecret: process.env.CUSTRAL_WEBHOOK_SECRET, // default secret for webhook verification
});
```

## Records

```ts theme={null}
// Create (records:write). Always adds a new record; there is no upsert.
const {id, ignoredFields} = await custral.records.create({
  object: "contacts",
  fields: {email: "jane@acme.com", full_name: "Jane Doe"},
});

// List (records:read) — filter, sort, search, and offset OR cursor pagination.
const page = await custral.records.list({
  object: "contacts",
  filter: {external_id: "crm_42"}, // look up by any property, {amount: {gte: "100"}} for operators
  sort: "created_at:desc",
  limit: 50,
});
page.data; // CustralRecord[]
page.total; // number | null
page.hasMore; // boolean
page.nextCursor; // pass back as list({cursor}) for the next page

// Full-text search (relevance-ranked; ignores filter/sort/cursor).
const hits = await custral.records.list({object: "deals", q: "globex" });

// Retrieve one (records:read). Inline notes docs → markdown with expand (needs documents:read).
const record = await custral.records.retrieve({object: "issues", id: "rec_123", expand: ["notes"]});
record.fields.notes; // markdown, not a doc_ id

// Or fetch a document directly by the doc_ id a notes field holds (documents:read).
const {markdown} = await custral.documents.retrieve("doc_3Ab9xK2mQ7");

// Update fields on an existing record (records:write). Only the keys you send
// change; unmatched keys come back in `ignoredFields`.
const {ignoredFields} = await custral.records.update({
  object: "contacts",
  id: "rec_123",
  fields: {status: "customer"},
});
```

## Conversations

View, manage, and **ingest** conversations: the loop a custom bridge (e.g. an iMessage or WhatsApp middleware) runs.

```ts theme={null}
// Create a conversation, link a participant, and import an initial message
// in one call (conversations:write).
const convo = await custral.conversations.create({
  title: "Jane Doe",
  sourceType: "imessage", // free-form label; defaults to "api"
  participants: [{name: "Jane Doe", email: "jane@acme.com"}],
  messages: [{text: "Hey, still on for Friday?", direction: "inbound"}],
});

// Append messages as they arrive (conversations:write). Fans out to live agents.
await custral.conversations.sendMessage(convo.id, {
  text: "Yes. See you at 2pm.",
  author: "You",
  direction: "outbound",
});

// Read the full transcript back (conversations:read).
const full = await custral.conversations.retrieve(convo.id);
full.messages; // CustralMessage[]
full.participants; // CustralParticipant[]

// Manage the lifecycle (conversations:write). Statuses: "open" | "closed" | "completed".
await custral.conversations.update(convo.id, {status: "closed", outcome: "won"});

// List, filtered by status / channel (conversations:read) — offset pagination.
const page = await custral.conversations.list({status: "open", limit: 50});

// Discover the valid `outcome` keys (conversations:read).
const dispositions = await custral.conversations.listDispositions();
```

## Objects

```ts theme={null}
const objects = await custral.objects.list(); // objects:read
const object = await custral.objects.retrieve("obj_123");
```

## Identity & MCP

```ts theme={null}
const me = await custral.me(); // "does my key work, and what can it do?"
me.orgId;
me.environment; // "live" | "test" | null
me.scopes; // ApiKeyScope[]

const {tools, count} = await custral.mcp.tools(); // the tools/list an MCP client sees (mcp:read)
```

## Errors

Every failure throws a typed `CustralError` carrying a machine-readable `code`, an HTTP `statusCode`, the `requestId`, and the `rateLimit` budget.

```ts theme={null}
import {Custral, CustralError, CustralRateLimitError} from "@custral/sdk";

try {
  await custral.records.create({object: "deals", fields: {name: "Globex"}});
} catch (err) {
  if (err instanceof CustralRateLimitError) {
    console.warn(`rate limited; retry after ${err.retryAfter}s`);
  } else if (err instanceof CustralError) {
    console.error(`${err.code} (${err.statusCode}) · request ${err.requestId}`);
  }
}
```

The exported error classes are `CustralAuthenticationError` (401), `CustralPermissionError` (403), `CustralNotFoundError` (404), `CustralInvalidRequestError` (400), `CustralRateLimitError` (429), `CustralAPIError` (5xx), and `CustralConnectionError`. `maxRetries` retries only **safe** failures, 429s always, and 5xx / connection errors only for idempotent GETs (a failed `create` is never auto-retried).

## Webhooks

Custral delivers events as signed HTTP POSTs (`X-Custral-Signature: t=<ts>,v1=<hmac>`). Register listeners with `on(...)` and verify + dispatch with `webhooks.express()` (mount with a **raw** body parser so the signed bytes survive):

```ts theme={null}
import express from "express";
import {Custral} from "@custral/sdk";

const custral = new Custral({
  apiKey: process.env.CUSTRAL_API_KEY!,
  webhookSecret: process.env.CUSTRAL_WEBHOOK_SECRET!,
});

custral.on("record.created", (event) => saveLead(event.data));
custral.on("record.*", (event) => audit(event)); // prefix glob

const app = express();
app.post("/webhooks/custral", express.raw({type: "application/json"}), custral.webhooks.express());
```

Or verify a single delivery manually (Stripe-style):

```ts theme={null}
const event = custral.webhooks.constructEvent(req.body, req.headers["x-custral-signature"], secret);
// throws CustralSignatureVerificationError if missing, stale, or wrong.
```

Webhook delivery is at-least-once. Make listeners **idempotent**. See [Webhooks](/dev/webhooks/overview) for the event catalog.

## See also

* [API Reference](/dev/api-reference/overview): every `/v1` endpoint with a live playground.
* [Authentication](/dev/auth/overview): API keys, scopes, and environments.
* [Browser SDK](/dev/sdks/browser): `@custral/js` for usage tracking and errors.
* [React SDK](/dev/sdks/react): `@custral/widget` for the chat widget.


## Related topics

- [SDKs](/dev/sdks/overview.md)
- [Changelog](/product/updates.md)
- [Developer Overview](/dev/overview.md)
- [Quickstart](/dev/webhooks/quickstart.md)
- [Getting Started](/dev/getting-started.md)
