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

# Node SDK

> Use the Whistle Node SDK to integrate cases and identities from any JavaScript/TypeScript backend.

The `@kansato/whistle-sdk` package is the typed server-side client for the Whistle SDK HTTP API (`/sdk/whistle/v1`). Pair it with [Outbound webhooks](/webhooks) verification helpers on the same install.

For browser-side case reporting UI, use the hosted iframe-backed [React SDK](/react-sdk) with a publishable key instead of sending your secret key to the client.

## Install

```bash theme={null}
npm install @kansato/whistle-sdk
```

```bash theme={null}
bun add @kansato/whistle-sdk
```

## Setup

```typescript theme={null}
import { Whistle } from "@kansato/whistle-sdk";

const whistle = new Whistle({
  apiKey: process.env.WHISTLE_API_KEY,
  projectId: process.env.WHISTLE_PROJECT_ID,
});
```

* `apiKey` — secret key (`sk_live_`) from **Settings → Developer → API Keys**. Never ship this to browsers; use `pk_live_` only with the [React SDK](/react-sdk).
* `projectId` — project UUID from the dashboard.

Optional configuration:

```typescript theme={null}
const whistle = new Whistle({
  apiKey: process.env.WHISTLE_API_KEY,
  projectId: process.env.WHISTLE_PROJECT_ID,
  timeout: 10_000,          // request timeout in ms (default: 30,000)
  baseUrl: "https://api.kansato.com/sdk/whistle/v1",  // default; omit for production
  fetch: customFetch,       // optional custom fetch implementation
});
```

## Error handling

All methods throw `WhistleError` on non-2xx responses and network failures.

```typescript theme={null}
import { Whistle, WhistleError, isWhistleError } from "@kansato/whistle-sdk";

const whistle = new Whistle({ apiKey, projectId });

try {
  await whistle.reports.create({ ... });
} catch (e) {
  if (isWhistleError(e)) {
    console.error(`HTTP ${e.status}: ${e.body.error}`);

    if (e.status === 401) {
      // Invalid or revoked API key
    }
    if (e.status === 429) {
      // Rate limited -- retry after delay
    }
  }
}
```

`WhistleError` properties:

| Property  | Type               | Description                                            |
| --------- | ------------------ | ------------------------------------------------------ |
| `status`  | `number`           | HTTP status code (0 for network errors / timeouts)     |
| `body`    | `WhistleErrorBody` | Parsed response body, always includes `error?: string` |
| `message` | `string`           | The `error` field from the body, or a default message  |

## Reports

### Create a report

```typescript theme={null}
const { report } = await whistle.reports.create({
  subject: {
    type: "user",
    externalId: "user_789",
    display: {
      name: "Jane Doe",
      username: "janedoe",
      avatarUrl: "https://example.com/avatar.jpg",
    },
  },
  reporter: {
    type: "user",
    externalId: "user_123",
  },
  target: {
    contentExternalId: "post_456",
    contentType: "post",
  },
  reason: "harassment",
  description: "This post contains targeted harassment.",
});
```

`subject` is the person who authored the content being reported. `reporter` is optional and identifies who filed the report.

The `reason` field accepts a string, an array of strings, or an explicit `{ names: string[] }` object:

```typescript theme={null}
// All equivalent:
reason: "harassment"
reason: ["harassment", "spam"]
reason: { names: ["harassment"] }
```

### List reports

```typescript theme={null}
const { data: reports, nextCursor } = await whistle.reports.list({
  limit: 25,
});

// Fetch next page
if (nextCursor) {
  const { data: more } = await whistle.reports.list({
    limit: 25,
    cursor: nextCursor,
  });
}
```

Returns `PaginatedResponse<Report>`:

| Field        | Type             | Description                                                         |
| ------------ | ---------------- | ------------------------------------------------------------------- |
| `data`       | `Report[]`       | Array of reports for this page                                      |
| `nextCursor` | `string \| null` | Pass as `cursor` to get the next page, or `null` if no more results |

### Get a single report

```typescript theme={null}
const { report } = await whistle.reports.get("report_abc123");
```

### Resolve and reopen

```typescript theme={null}
await whistle.reports.resolve(reportId);
await whistle.reports.unresolve(reportId);
```

Both return `void` on success. They throw `WhistleError` if the report is not found.

### Dismiss

```typescript theme={null}
await whistle.reports.dismiss(reportId, {
  reason: "false_positive",
});
```

Valid dismiss reasons: `false_positive`, `duplicate`, `out_of_scope`, `not_actionable`, `other`.

### Escalate

```typescript theme={null}
const escalation = await whistle.reports.escalate(reportId, {
  target: "safety",
});
// escalation.ok === true; escalation.reportId is the same report id
```

Valid escalation targets: `safety`, `legal`, `senior_moderator`.

## Identities

### List identities

```typescript theme={null}
const { data: identities, nextCursor } = await whistle.identities.list({
  limit: 50,
});
```

Returns `PaginatedResponse<Identity>`.

### Upsert an identity

```typescript theme={null}
const identity = await whistle.identities.upsert(
  "user",       // type
  "user_789",   // externalId
  {
    display: {
      name: "Jane Doe",
      username: "janedoe",
    },
    metadata: {
      platform: "web",
      registeredAt: "2025-01-01",
    },
  },
);
```

Returns the full `Identity` object. Creates the identity if it does not exist, or updates display fields and metadata if it does.

## Report reasons

### List report reasons

Returns the project's configured report reasons. Accessible with both secret and publishable keys.

```typescript theme={null}
const { reasons } = await whistle.reportReasons.list();

// reasons: Array<{ id: string; value: string; label: string; severityHint?: string }>
```

## Content flag detection

Kuro AI content scoring uses organization REST routes (`/api/v1/organizations/.../content`), not the SDK class surface. Call `fetch` or your HTTP client from the same backend that holds the secret key. See [Kuro AI](/kuro-ai) for payloads and responses.
