# TypeScript SDK

> Use @ody/sdk from Node.js, Deno, Bun or the browser: setup, pagination, errors, idempotency and webhook verification.

Source: https://dev.ody.co/sdks/typescript

> **Preview:**
`@ody/sdk` isn't on npm yet; see [availability](https://dev.ody.co/sdks/overview). The API it wraps is stable and live.

## Setup

```bash
npm install @ody/sdk   # once published
```

```ts
import { Ody } from "@ody/sdk";

const ody = new Ody(process.env.ODY_API_KEY!);
// Options: new Ody(key, { baseUrl: "https://api.ody.co" })
```

The client is ESM, has no dependencies, and runs anywhere `fetch` exists (Node.js 18+, Deno, Bun, Cloudflare Workers, browsers). **Never ship a live key to a browser.** Keys act as a workspace member, so call Ody from your server.

## Usage

```ts
// Contacts
const contact = await ody.contacts.create({ firstName: "Ada", phone: "+15125550123", tags: ["vip"] });
for await (const c of ody.contacts.listAll({ q: "ada" })) console.log(c.id);

// Conversations
const { conversations } = await ody.conversations.list({ status: "open", limit: 25 });
const { conversation, activities } = await ody.conversations.get(conversations[0].id);
await ody.conversations.setStatus(conversation.id, "done");

// Messages
const sent = await ody.messages.send({ to: "+15125550123", body: "Hi!", idempotencyKey: "welcome-ada" });
const hits = await ody.messages.search("refund", { limit: 10 });

// Calls
const calls = await ody.calls.list({ limit: 20 });
await ody.calls.place({ to: "+15125550123" });

// Numbers, 10DLC, agent
const available = await ody.numbers.searchAvailable({ startsWith: "512" });
await ody.numbers.buy({ phoneNumber: available[0].phone_number, idempotencyKey: "main-line" });
const status = await ody.messaging.status();
await ody.agent.update({ greeting: "Thanks for calling!" });
await ody.agent.publish();
```

## Errors

Every non-`2xx` response throws `OdyError`:

```ts
import { OdyError } from "@ody/sdk";

try {
  await ody.messages.send({ to: "+15125550123", body: "Hi" });
} catch (err) {
  if (err instanceof OdyError) {
    switch (err.code) {
      case "failed_precondition": /* e.g. 10DLC not approved yet: show err.message */ break;
      case "resource_exhausted":  await sleep((err.retryAfter ?? 1) * 1000); break;
      case "unauthenticated":     /* key revoked */ break;
    }
    console.error(err.status, err.code, err.message, err.requestId);
  } else {
    throw err; // network error
  }
}
```

The codes are listed in [Errors](https://dev.ody.co/api/errors).

## Webhook verification

```ts
import { verifyWebhookSignature } from "@ody/sdk";

const ok = verifyWebhookSignature(secret, headers, rawBody); // rawBody: string | Uint8Array
```

It checks the `v1` HMAC-SHA256 signature in constant time and rejects timestamps more than 5 minutes off. It uses `node:crypto`, so it's server-side only. Full examples are in [Verifying signatures](https://dev.ody.co/webhooks/signatures).

## Webhooks

`parseWebhookEvent` verifies and parses in one step, and returns a `WebhookEvent` whose `data` is typed by `event.type`. It throws `WebhookVerificationError` on a bad or stale signature, so a handler is a `try` around one call:

```ts
import express from "express";
import { parseWebhookEvent, WebhookVerificationError } from "@ody/sdk";

app.post("/ody-webhook", express.raw({ type: "application/json" }), (req, res) => {
  let event;
  try {
    event = parseWebhookEvent(process.env.ODY_WEBHOOK_SECRET!, req.headers, req.body);
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.status(401).end();
    throw err;
  }

  switch (event.type) {
    case "call.ringing":
      screenPop(event.data.from, event.data.callerName);       // typed as CallRingingData
      break;
    case "call.answered":
      if (event.data.answeredBy === "member") assign(event.data.memberUid);
      break;
    case "contact.updated":
    case "contact.deleted":
      void syncContact(event.data.contactId);                  // ContactEventData
      break;
  }
  res.status(200).end();
});
```

`WEBHOOK_EVENT_TYPES` is the catalog as a readonly tuple (`WebhookEventType` is its union), and `isKnownWebhookEventType(type)` tells you whether a delivery's `type` is one this SDK version knows, so a subscription to `["*"]` can log new types instead of crashing. Each event's fields are described in [Event types](https://dev.ody.co/webhooks/events).

## Types

Everything is exported: `Contact`, `ContactSummary`, `ContactInput`, `ConversationSummary`, `Activity`, `PhoneNumber`, `MessagingStatus`, `Agent`, `AgentInput`, `Webhook`, `WebhookDelivery`, `OdyErrorCode`, and for webhooks `WebhookEvent`, `WebhookEnvelope`, `WebhookEventDataMap`, `CallRingingData`, `CallAnsweredData`, `CallForwardedData`, `ContactEventData` and more. They mirror the OpenAPI spec and the [event catalog](https://dev.ody.co/webhooks/events).
