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:
| 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
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>:
| 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
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.