npm package: @openbat/sdk
Description: TypeScript SDK for capturing AI chatbot conversations with OpenBat — sentiment, intent, topics, quality scoring, and behavioral flags
Latest version: 0.1.1

# @openbat/sdk

TypeScript SDK for capturing AI chatbot conversations with [OpenBat](https://openbat.dev). Record messages, enrich with user/org context, and get AI-powered analysis — sentiment, intent detection, topic classification, quality scoring, behavioral flags, and more — with zero latency impact on your app.

## Installation

```bash
npm install @openbat/sdk
```

## Quick Start

```typescript
import { OpenBat } from "@openbat/sdk";

const client = new OpenBat({
  apiKey: process.env.OPENBAT_API_KEY,
});

await client.recordMessages({
  conversationId: "session-abc-123",
  messages: [
    { role: "user", content: "How do I reset my password?" },
    { role: "assistant", content: "Go to Settings > Security > Reset Password." },
  ],
});
```

That's it. Messages are sent asynchronously — errors are logged to console, never thrown.

---

## Configuration

### Constructor Options

```typescript
const client = new OpenBat({
  apiKey: "ob_live_...",       // Required (or set OPENBAT_API_KEY env var)
  baseUrl: "https://...",      // Optional, defaults to https://openbat.dev
  enabled: true,               // Optional, defaults to true
});
```

### Environment Variables

| Variable | Purpose | Default |
|----------|---------|---------|
| `OPENBAT_API_KEY` | API key (used if not passed in constructor) | — |
| `OPENBAT_BASE_URL` | Override the capture endpoint base URL | `https://openbat.dev` |
| `OPENBAT_ENABLED` | Set to `"false"` or `"0"` to disable | `"true"` |

Constructor values take precedence over environment variables.

---

## Integration Guides

### Vercel AI SDK — `withOpenBat` Wrapper

The simplest integration. Wrap your `streamText` response and you're done.

```typescript
// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
import { withOpenBat } from "@openbat/sdk";

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: openai("gpt-4o"),
    messages,
  });

  return withOpenBat(result.toDataStreamResponse(), {
    messages,
    conversationId: "session-abc-123", // optional, auto-generated UUID if omitted
    apiKey: process.env.OPENBAT_API_KEY,
    user: { id: "user-42", email: "jane@acme.com" },
  });
}
```

`withOpenBat` returns the original `Response` unchanged — zero interference with streaming.

### Vercel AI SDK — `onFinish` Pattern

For more control, record messages in the `onFinish` callback. This lets you capture the assistant's final response text and send only new messages (avoiding re-ingesting history on every turn).

```typescript
// app/api/chat/route.ts
import { streamText, UIMessage, convertToModelMessages } from "ai";
import { openai } from "@ai-sdk/openai";
import { OpenBat } from "@openbat/sdk";

const client = new OpenBat(); // reads OPENBAT_API_KEY from env

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  // Forward client headers so OpenBat captures real IP/User-Agent
  const headers = Object.fromEntries(req.headers.entries());

  const result = streamText({
    model: openai("gpt-4o"),
    messages: await convertToModelMessages(messages),
    onFinish: async ({ text }) => {
      // Extract last user message from UIMessage parts
      const lastUserMessage = [...messages]
        .reverse()
        .find((m) => m.role === "user");

      const lastUserContent =
        lastUserMessage?.parts
          ?.filter((p): p is { type: "text"; text: string } => p.type === "text")
          .map((p) => p.text)
          .join("") ?? "";

      // Only send new messages — not the full history
      client.recordMessages({
        conversationId: "session-abc-123",
        user: { id: "user-42", email: "jane@acme.com" },
        headers: {
          "x-original-user-agent": headers["user-agent"] ?? "",
          "x-original-referer": headers["referer"] ?? "",
          "x-forwarded-for": headers["x-forwarded-for"] ?? headers["x-real-ip"] ?? "",
        },
        messages: [
          ...(lastUserContent
            ? [{ role: "user" as const, content: lastUserContent }]
            : []),
          { role: "assistant" as const, content: text },
        ],
      });
    },
  });

  return result.toUIMessageStreamResponse();
}
```

### Direct API — Any Framework

Initialize the client once and call `recordMessages()` after each conversation turn.

```typescript
// lib/openbat.ts
import { OpenBat } from "@openbat/sdk";

export const openbat = new OpenBat({
  apiKey: process.env.OPENBAT_API_KEY,
});
```

```typescript
// wherever you handle chat
import { openbat } from "@/lib/openbat";

openbat.recordMessages({
  conversationId: session.id,
  messages: [
    { role: "user", content: userMessage },
    { role: "assistant", content: assistantResponse },
  ],
  user: { id: user.id, email: user.email },
});
```

### Express / Node.js

```typescript
import express from "express";
import { OpenBat } from "@openbat/sdk";

const app = express();
const openbat = new OpenBat(); // reads from OPENBAT_API_KEY env var

app.post("/api/chat", async (req, res) => {
  const { message, conversationId, userId } = req.body;

  // Your chat logic
  const reply = await getAIResponse(message);

  // Fire-and-forget — does not block the response
  openbat.recordMessages({
    conversationId,
    user: { id: userId },
    messages: [
      { role: "user", content: message },
      { role: "assistant", content: reply },
    ],
  });

  res.json({ reply });
});
```

### Raw HTTP — Any Language

If you're not using TypeScript, call the capture endpoint directly.

```bash
curl -X POST https://openbat.dev/api/v1/capture \
  -H "Content-Type: application/json" \
  -H "x-openbat-key: ob_live_your_api_key_here" \
  -d '{
    "conversationId": "session-abc-123",
    "messages": [
      { "role": "user", "content": "How do I reset my password?" },
      { "role": "assistant", "content": "Go to Settings > Security > Reset Password." }
    ],
    "user": { "id": "user-42", "email": "jane@acme.com" }
  }'
```

**Response:**

```json
{
  "ok": true,
  "conversationId": "uuid-...",
  "messagesStored": 2,
  "analysisQueued": 2
}
```

#### Python

```python
import requests

requests.post(
    "https://openbat.dev/api/v1/capture",
    headers={
        "Content-Type": "application/json",
        "x-openbat-key": "ob_live_your_api_key_here",
    },
    json={
        "conversationId": "session-abc-123",
        "messages": [
            {"role": "user", "content": "How do I reset my password?"},
            {"role": "assistant", "content": "Go to Settings > Security > Reset Password."},
        ],
        "user": {"id": "user-42"},
    },
)
```

---

## API Reference

### `OpenBat` Class

```typescript
import { OpenBat } from "@openbat/sdk";

const client = new OpenBat(config?: OpenBatConfig);
await client.recordMessages(input: RecordMessagesInput): Promise<void>;
```

**Throws** at construction if `enabled` is `true` and no API key is provided.

### `withOpenBat` Function

```typescript
import { withOpenBat } from "@openbat/sdk";

const response = withOpenBat(response: Response, options: WrapperOptions): Response;
```

Wraps a `Response` object, fires `recordMessages()` in the background, and returns the original response unchanged. Filters out `system` messages automatically. Generates a UUID for `conversationId` if not provided.

### Types

```typescript
interface OpenBatConfig {
  apiKey?: string;
  baseUrl?: string;
  enabled?: boolean;
}

interface RecordMessagesInput {
  conversationId: string;
  user?: OpenBatUser;
  organization?: OpenBatOrganization;
  session?: OpenBatSession;
  custom?: Record<string, string | number | boolean>;
  headers?: Record<string, string>;
  messages: Array<{
    role: "user" | "assistant" | "system";
    content: string;
    id?: string;       // External ID for deduplication
    sentAt?: Date;     // Timestamp (serialized to ISO string)
  }>;
}

interface OpenBatUser {
  id?: string;
  name?: string;
  email?: string;
  plan?: string;         // e.g. "pro", "enterprise"
  createdAt?: string;    // ISO datetime
  industry?: string;
  mrr?: number;          // Monthly recurring revenue in cents
}

interface OpenBatOrganization {
  id?: string;
  name?: string;
  plan?: string;
  mrr?: number;
  industry?: string;
}

interface OpenBatSession {
  id?: string;
  pageUrl?: string;
  referrer?: string;
  device?: "mobile" | "desktop" | "tablet";
  country?: string;     // ISO country code
}

interface WrapperOptions {
  apiKey?: string;
  baseUrl?: string;
  messages: Array<{ role: string; content: unknown }>;
  conversationId?: string;
  user?: OpenBatUser;
  organization?: OpenBatOrganization;
  session?: OpenBatSession;
  custom?: Record<string, string | number | boolean>;
}
```

---

## Enriching Context

Attach business context to conversations for richer analytics in the dashboard.

```typescript
client.recordMessages({
  conversationId: "session-abc-123",
  messages: [...],

  // Who is chatting
  user: {
    id: "user-42",
    name: "Jane Smith",
    email: "jane@acme.com",
    plan: "enterprise",
    industry: "fintech",
    mrr: 49900,  // $499.00
  },

  // Their organization
  organization: {
    id: "org-7",
    name: "Acme Corp",
    plan: "enterprise",
    mrr: 249900,
    industry: "fintech",
  },

  // Session context
  session: {
    id: "sess-xyz",
    pageUrl: "https://app.acme.com/support",
    referrer: "https://google.com",
    device: "desktop",
    country: "US",
  },

  // Arbitrary key-value pairs
  custom: {
    feature: "billing",
    chatbotVersion: "2.1",
    isTrialUser: true,
  },
});
```

All context fields are optional. Send what you have — even just `user.id` helps segment conversations in the dashboard.

---

## Message Deduplication

Set `id` on messages to prevent duplicates when re-sending conversation history.

```typescript
client.recordMessages({
  conversationId: "session-abc-123",
  messages: [
    { id: "msg-001", role: "user", content: "Hello" },         // stored on first call
    { id: "msg-002", role: "assistant", content: "Hi there!" }, // stored on first call
    { id: "msg-001", role: "user", content: "Hello" },         // skipped (duplicate)
    { id: "msg-003", role: "user", content: "Help me" },       // stored (new)
  ],
});
```

Messages with `id` are deduplicated per conversation. Messages without `id` are always inserted.

---

## Header Forwarding

When your SDK calls originate from a server-side route (e.g., Next.js API route), the capture endpoint sees your server's IP and User-Agent instead of the real user's. Forward the original headers to get accurate client metadata:

```typescript
// Inside your API route handler
const headers = Object.fromEntries(req.headers.entries());

client.recordMessages({
  conversationId: "session-abc-123",
  messages: [...],
  headers: {
    "x-original-user-agent": headers["user-agent"] ?? "",
    "x-original-referer": headers["referer"] ?? "",
    "x-forwarded-for": headers["x-forwarded-for"] ?? headers["x-real-ip"] ?? "",
  },
});
```

The capture endpoint automatically parses these headers to extract: IP address, browser, OS, device type, language, timezone, country, city, and region.

---

## Error Handling

The SDK is designed to be invisible — it never throws errors or blocks your application.

- `recordMessages()` catches all errors internally
- HTTP errors are logged: `OpenBat Error: HTTP 400 — ...`
- Network failures are logged: `OpenBat Error: Failed to record messages.`
- All errors go to `console.error` — they never propagate to your code

If you need to verify the SDK is working, check your browser/server console for error messages, or visit the OpenBat dashboard to confirm messages are arriving.

---

## Disabling the SDK

Disable the SDK in test or development environments:

```bash
# Environment variable
OPENBAT_ENABLED=false
```

```typescript
// Or in code
const client = new OpenBat({
  apiKey: "ob_live_...",
  enabled: process.env.NODE_ENV === "production",
});
```

When disabled, `recordMessages()` returns immediately without making any HTTP requests.

---

## API Limits

| Constraint | Limit |
|------------|-------|
| Request body size | 512 KB |
| Messages per request | 1–100 |
| Message content length | 1–32,000 characters |
| `conversationId` length | 1–256 characters |
| `message.id` length | max 256 characters |
| `user.email` | Must be a valid email, max 256 characters |
| `user.plan` / `organization.plan` | max 64 characters |
| `user.name` / `organization.name` | max 256 characters |
| `session.pageUrl` / `session.referrer` | max 2,048 characters |
| `custom` field values | `string`, `number`, or `boolean` only |

Requests exceeding these limits receive a `400 Bad Request` response (logged to console, never thrown).

---

## License

MIT