Skip to main content

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.

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 verification helpers on the same install. For browser-side report UI, use the hosted iframe-backed React SDK with a publishable key instead of sending your secret key to the client.

Install

npm install @kansato/whistle-sdk
bun add @kansato/whistle-sdk

Setup

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 (wh_) from Settings → Developer → API Keys. Never ship this to browsers; use whpk_ only with the React SDK.
  • projectId — project UUID from the dashboard.
Optional configuration:
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.
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:
PropertyTypeDescription
statusnumberHTTP status code (0 for network errors / timeouts)
bodyWhistleErrorBodyParsed response body, always includes error?: string
messagestringThe error field from the body, or a default message

Reports

Create a report

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:
// All equivalent:
reason: "harassment"
reason: ["harassment", "spam"]
reason: { names: ["harassment"] }

List reports

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>:
FieldTypeDescription
dataReport[]Array of reports for this page
nextCursorstring | nullPass as cursor to get the next page, or null if no more results

Get a single report

const { report } = await whistle.reports.get("report_abc123");

Resolve and reopen

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

await whistle.reports.dismiss(reportId, {
  reason: "false_positive",
});
Valid dismiss reasons: false_positive, duplicate, out_of_scope, not_actionable, other.

Escalate

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

const { data: identities, nextCursor } = await whistle.identities.list({
  limit: 50,
});
Returns PaginatedResponse<Identity>.

Upsert an identity

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.
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 for payloads and responses.