Official TypeScript / Node SDK for the Sonny Labs AI firewall — install, first scan, error handling, idempotency, retries, self-hosted, and webhook verification.
The TypeScript SDK targets Node.js 20.19+ (Active LTS). It is
ESM-first with a CJS build for require() callers, ships generated
types from the OpenAPI spec, and supports both Node and modern
edge / runtime environments that implement the standard fetch API.
Looking for the symbol-by-symbol reference? The full
auto-generated API reference (SonnyLabsClient, options bag, error
classes, verifyWebhook) lives at
TypeScript SDK reference. This page is the
conceptual quickstart — install, first scan, error handling,
idempotency, retries, self-hosted, and webhook verification.
Customer-facing quickstart for the official Sonny Labs TypeScript /
Node SDK (@sonnylabs/sdk). Wraps the /v1/* surface defined in
REST API reference.
Coming soon to npm — until publication, install from the source tree
under sdks/typescript/ in this repository.
The SDK authenticates with a scoped API key (sk_live_… or
sk_test_…). For a runtime scanner the smallest viable scope is
scans:write. The full lifecycle, including scope choice, rotation,
and revocation, is documented in
the API key endpoints in the REST reference.
The short version:
Sign in to the dashboard, pick the org you want the key to belong
to, and open API keys.
Click Create key, give it a human-readable name, and select the
scans:write scope.
Copy the plaintext secret from the success modal — it is shown
exactly once. Store it in your secrets manager before closing the
dialog.
Then expose the secret via your environment of choice:
createScan maps to POST /v1/scans (operationId: createScan).
The kind field discriminates between content scans (a single prompt
or response) and toolset scans (an MCP/native tool inventory). See
the
POST /v1/scans reference
for every field.
Every non-2xx response is parsed as an RFC 9457
application/problem+json envelope and thrown as a SonnyLabsError
subclass. Pattern-match on the stable dot-namespaced code:
import { SonnyLabsClient, SonnyLabsError, AuthenticationError, RateLimitError, ValidationError,} from "@sonnylabs/sdk";const client = new SonnyLabsClient({ apiKey: process.env.SONNY_API_KEY! });try { const scan = await client.createScan({ kind: "content", surface: "user_message", content: { type: "text", text: userPrompt }, });} catch (err) { if (err instanceof AuthenticationError) { if (err.code === "auth.api_key.expired") { // Rotate the credential and retry once on the new value. } else if (err.code === "auth.api_key.revoked") { // Operator revoked the key — surface a re-credential UI. } else { throw err; } } else if (err instanceof RateLimitError) { // Honour Retry-After; see "Retry behavior" below. } else if (err instanceof ValidationError) { // err.errors is an array of { path, code, detail } per-field problems. for (const field of err.errors) { console.error(`${field.path}: ${field.code}`); } } else if (err instanceof SonnyLabsError) { // Catch-all for any other Problem-shaped failure. console.error(`${err.status} ${err.code}: ${err.detail}`); } else { throw err; }}
Common code values you should expect to see:
Code
When
auth.api_key.invalid
The bearer secret didn't parse or didn't match any key.
auth.api_key.revoked
The key was deleted via DELETE /v1/api-keys/{id}.
auth.api_key.expired
The key is past its expiry (once expiry lands; see docs/api-keys.md).
auth.scope.missing
The key authenticated but lacks the scope this endpoint demands.
validation.field_invalid
One or more errors[] entries describe the offending field.
idempotency.key_reuse_mismatch
Same Idempotency-Key was reused with a different body.
Every SonnyLabsError carries status, code, title, detail,
requestId, traceId, and a problem property holding the raw
envelope for forward-compatibility.
Every POST accepts an Idempotency-Key header. The server stores the
key plus a body hash for 24 hours; reusing the same key with the same
body replays the original response, and reusing it with a different
body returns 409 idempotency.key_reuse_mismatch.
The TypeScript SDK auto-generates a UUIDv4 idempotency key for every
createScan call so retries are safe by default. Pass an explicit key
when your caller already has a stable identifier (a workflow run ID,
an upstream message ID, etc.):
Disable auto-generation entirely (e.g. when the upstream proxy is
already handling dedup) by passing idempotencyKey: null and
constructing the client with autoIdempotency: false.
The SDK transparently retries 429 Too Many Requests and
503 Service Unavailable responses up to 3 times with exponential
backoff. When the response carries a Retry-After header, the SDK
honors it instead of computing its own delay.
5xx responses other than 503 are not retried — they signal a
permanent server-side error for that request, and silently retrying
risks duplicating side effects.
If you would rather drive retries yourself, set maxRetries: 0 and
catch RateLimitError / ServerError from your own retry loop. The
retryAfterSeconds property on RateLimitError exposes the parsed
Retry-After value for that purpose.
For a self-hosted Sonny Labs deployment in your own VPC, point the
client at your control-plane URL via baseUrl:
const client = new SonnyLabsClient({ apiKey: process.env.SONNY_API_KEY!, baseUrl: "https://sonny.internal.example.com",});
The SDK speaks the same /v1/* surface — the only thing that changes
is where the requests are sent. API keys are issued by the self-hosted
control plane just like in SaaS; the format and scope semantics are
identical.
Outbound webhooks (scan.allowed, scan.flagged, scan.warned,
scan.blocked) are signed with HMAC-SHA256 over the canonical
{t}.{body} payload. The SDK ships a verifyWebhook helper that
validates the signature and the replay window in one call. See
Webhooks for the signing scheme, the full code
sample, and what to do on signature mismatch.