Sonny Labs API (1.0.0)

Download OpenAPI specification:

Sonny Labs Support: support@sonnylabs.ai URL: https://sonnylabs.ai License: LicenseRef-Proprietary

Real-time AI firewall — inspect prompts and responses for prompt injection, PII, toxicity, and policy violations.

The Sonny Labs API is a content firewall for LLM applications. POST a user prompt before it reaches your model and the API returns a decision your handler can act on. The same surface scans model outputs for observability, though we recommend gating responses on the inbound decision only — see the quickstart for the canonical integration shape.

Conventions

  • Resource-oriented with opaque ULIDs.
  • Single hot-path endpoint: POST /v1/scans, discriminated on kind: content | toolset.
  • Errors follow RFC 9457 application/problem+json. Every problem carries a stable, dot-namespaced code for programmatic handling.
  • Idempotency-Key is accepted (and recommended) on every POST. Retries with the same key are deduplicated server-side.
  • Cursor pagination on every list endpoint.
  • Rate-limit headers (RFC draft RateLimit-*) on every response; Retry-After on 429.
  • X-Request-Id is echoed on every response — include it when contacting support.

Authentication

Every request needs a bearer credential — either a scoped API key (sk_live_... / sk_test_...) minted from the dashboard, or an SSO session JWT for dashboard-driven flows.

Each operation declares the scope it requires via the x-required-scopes extension shown in the operation details below. A credential lacking the scope is rejected with a 403 problem+json whose detail echoes the missing scope.

x-required-scopes: [] means "any authenticated principal may call this; no scope check is performed" — used on identity and discovery endpoints (getMe, listDetectors) where every authenticated caller legitimately needs the answer.

Versioning

The /v1 namespace is stable. Within v1, pin to a dated revision of the contract by sending Sonny-Api-Version: 2026-05-01 (or any later supported date). Omit the header to track the latest stable revision.

Deployment

The same API runs in the Sonny Labs SaaS at https://api.sonnylabs.ai and self-hosted in your own VPC via the Helm chart. Behavior is identical across both — only the base URL changes.

Security posture

  • In-request content only. Scan bodies accept text and image_base64; we do not fetch external URLs on your behalf.
  • Webhook URLs: https:// only; private address ranges (RFC 1918, link-local, cloud metadata IPs) are refused at registration time.
  • Secrets (API key secret, webhook signing secret) are returned exactly once at creation. Subsequent GETs never echo them — rotate or recreate if lost.
  • Tenant isolation. Every tenant-scoped resource carries an org_id and the API strictly isolates credentials across organizations.
  • Payload-size limits are documented on the schemas themselves so a generated client surfaces them ahead of time, not only at the edge.

auth

Caller identity.

Return the resolved principal

Identity endpoint. Every authenticated principal can call this regardless of scope — x-required-scopes: [] declares the unscoped intent explicitly so no SDK or audit tool treats the absence as an oversight.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "type": "api_key",
  • "org_id": "string",
  • "org_display_name": "string",
  • "actor_id": "string",
  • "scopes": [
    ],
  • "deployment_mode": "saas"
}

api-keys

Machine credentials.

List API keys in the caller's org

Authorizations:
bearerAuth
query Parameters
cursor
string

Opaque cursor from a previous page.

limit
integer [ 1 .. 200 ]
Default: 50
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "next_cursor": "string"
}

Create a scoped API key

Plaintext secret is returned exactly once on this response. It cannot be retrieved later — rotate the key if lost.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
name
required
string [ 1 .. 120 ] characters
scopes
required
Array of strings (ApiKeyScope) non-empty
Items Enum: "scans:read" "scans:write" "policies:read" "policies:write" "webhooks:read" "webhooks:write" "api_keys:read" "api_keys:write" "audit:read" "analytics:read" "labeled_events:read" "labeled_events:write" "labeled_events:delete" "redteam:read" "redteam:write" "mcp:invoke" "admin"
environment
string
Default: "live"
Enum: "test" "live"
expires_at
string or null <date-time>

Optional UTC timestamp at which the key should stop authenticating. SOC2-aligned defaults:

  • Omitted or null (and no_expiry is false/absent) → server defaults to now() + 1 year.
  • Provided → must be strictly in the future. Values more than 2 years from now() are clamped down to now() + 2 years. A value in the past is rejected with 400 validation.error.
  • Mutually exclusive with no_expiry: true — sending both is a 400 validation.error.
no_expiry
boolean
Default: false

Explicit opt-out: when true, the key is created with no expiry (the persisted auth.api_keys.expires_at is NULL) and the auth-time enforcer will never reject it on age. This is a deliberate SOC2-discouraged escape hatch for legacy / break-glass integrations; the response body's expires_at comes back as null so dashboards can flag the row in their audit views. Mutually exclusive with expires_at.

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "scopes": [
    ],
  • "environment": "test",
  • "expires_at": "2019-08-24T14:15:22Z",
  • "no_expiry": false
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "prefix": "sk_live",
  • "last_four": "stri",
  • "scopes": [
    ],
  • "environment": "test",
  • "last_used_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z",
  • "expires_at": "2019-08-24T14:15:22Z",
  • "revoked_at": "2019-08-24T14:15:22Z",
  • "secret": "string"
}

Revoke an API key

Authorizations:
bearerAuth
path Parameters
api_key_id
required
string^ak_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/problem+json
{}

Rotate an API key (immediate revoke of the old key)

SOC2-aligned rotation: a fresh plaintext secret is minted, the old key is revoked immediately (no grace period), and the new row carries rotated_from = <old key id> so lineage queries can join keys to their predecessor. The new key inherits the old key's name, scopes, and expires_atexpires_at is not silently extended; rotate again or create a new key if a longer lifetime is required. The plaintext is returned exactly once; subsequent reads only surface the prefix + last_four (the same contract as POST /v1/api-keys). Returns 404 when the supplied id is unknown to the caller's org and 409 when the key has already been revoked (rotation is not idempotent — a revoked key is terminal).

Authorizations:
bearerAuth
path Parameters
api_key_id
required
string^ak_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "name": "string",
  • "prefix": "sk_live",
  • "last_four": "stri",
  • "scopes": [
    ],
  • "environment": "test",
  • "last_used_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z",
  • "expires_at": "2019-08-24T14:15:22Z",
  • "revoked_at": "2019-08-24T14:15:22Z",
  • "secret": "string"
}

invites

Org membership invites.

List pending invites in the caller's org

Returns invites that are still actionable: not accepted, not revoked, not expired. The raw token is never returned — it is delivered exactly once in the accept_url of the original POST /v1/invites response.

Authorizations:
bearerAuth
query Parameters
cursor
string

Opaque cursor from a previous page.

limit
integer [ 1 .. 200 ]
Default: 50
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "next_cursor": "string"
}

Invite a user to the caller's org

Creates a pending invite and returns a one-time accept_url that the inviter shares with the invitee out-of-band (Slack, iMessage, the inviter's own email client). The plaintext token embedded in the URL is generated server-side and stored only as a SHA-256 hash, so the URL cannot be retrieved later via GET /v1/invites — surface it to the user the moment this call returns. Only the invited email address can ultimately accept the invite (enforced at POST /v1/invites/{token}:accept).

Optional email side-channel: when the deployment sets INVITES_SEND_EMAIL=true and a MailSender is configured, the same accept URL is also dispatched in an email to the invitee. The wire response is identical either way — the email is purely a delivery convenience.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
email
required
string <email> <= 320 characters
role
required
string (InviteRole)
Enum: "admin" "member"

Role granted to the invitee on accept.

Responses

Request samples

Content type
application/json
{
  • "email": "user@example.com",
  • "role": "admin"
}

Response samples

Content type
application/json
{
  • "invite": {
    },
  • "accept_url": "http://example.com"
}

Revoke a pending invite

Idempotent: returns 204 whether the invite was pending and is now revoked, or was already revoked / accepted on a prior call. A 404 is returned only when the id is unknown to the caller's org.

Authorizations:
bearerAuth
path Parameters
invite_id
required
string^inv_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/problem+json
{}

Accept an invite using its one-time token

Any authenticated principal can call this — x-required-scopes: [] declares the unscoped intent so a brand-new user with zero provisioned scopes can complete onboarding. The {token} path parameter is the plaintext value the inviter received from POST /v1/invites and shared with the invitee; the server looks it up by SHA-256 hash. Behaviour:

  • Token unknown / revoked → 404 invites.not_found
  • Token expired → 410 invites.expired
  • Caller's verified email does not match the invited email (case-insensitive) → 403 invites.email_mismatch. Forwarded URLs are only usable by the intended recipient.
  • Caller's principal carries no verifiable email (e.g. an API-key bearer or a JWT without an email claim) → 403 invites.principal_email_unavailable
  • Caller's external_subject already a member of the invite's org → idempotent return of the existing membership
  • Caller's external_subject already a member of a DIFFERENT org → 409 invites.already_member
  • Otherwise → user record is created in the invite's org and the invite is marked accepted in a single transaction. On SaaS the WorkOS Organizations API is called in the same request to add the user to the IdP-side membership; an upstream failure surfaces as 502 invites.upstream_failure and no local DB row is written so a retry is safe.
Authorizations:
bearerAuth
path Parameters
token
required
string^[A-Za-z0-9_-]{43}$

Plaintext invite token from the invitation email. 32 random bytes encoded as URL-safe base64 without padding (43 chars). The server stores only sha256(token) and never returns the plaintext from any other endpoint.

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "user": {
    },
  • "organization": {
    },
  • "role": "admin"
}

users

Org membership directory.

List users in the caller's org

Org-scoped membership directory used by the dashboard's Settings/Members surface. Tenant isolation is enforced by V1 RLS on auth.users (the calling principal's app.current_org_id GUC pins the visible row set); the query is also predicate-scoped by org_id as defence in depth. Cursors are opaque base64-JSON of (created_at, user_id) so pagination is stable under concurrent inserts. A tampered cursor returns 400 with code=users.invalid_cursor. Sort order is created_at desc, id desc.

Authorizations:
bearerAuth
query Parameters
cursor
string

Opaque cursor from a previous page.

limit
integer [ 1 .. 200 ]
Default: 50
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "next_cursor": "string"
}

scans

Hot-path detection. One endpoint, discriminated by kind.

Run a scan (content or toolset)

Single hot-path endpoint. Supports SSE streaming via Accept: text/event-stream for long content — findings stream as they complete, terminal decision event closes the stream.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
kind
required
string
surface
required
string (Surface)
Enum: "user_message" "assistant_output" "tool_result" "tool_params" "document" "agent_message" "mcp_resource" "mcp_tool_description"
required
object or object or object (ContentContainer)
object (ScanContext)
object (ScanOptions)

Responses

Request samples

Content type
application/json
Example
{
  • "kind": "content",
  • "surface": "user_message",
  • "content": {
    },
  • "context": {
    },
  • "options": {
    }
}

Response samples

Content type
{
  • "id": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "kind": "content",
  • "surface": "user_message",
  • "context": {
    },
  • "findings": [
    ],
  • "decision": {
    },
  • "content_stored": true,
  • "content": {
    }
}

List scans with filters

Authorizations:
bearerAuth
query Parameters
agent_id
string <= 255 characters
session_id
string <= 255 characters
end_user_id
string <= 255 characters

Filter to scans for one end user. The server SHA-256s the raw identifier and matches it against the persisted hash — the raw value never lands in a query log or the database. Mutually compatible with end_user_id_hash; when both are supplied the server hashes end_user_id and intersects.

end_user_id_hash
string = 64 characters ^[0-9a-f]{64}$

Filter by the SHA-256 hex of an end-user identifier. Use this form when the caller only has the hash (e.g. dashboard drill-down from a scan-detail row, which never received the raw value). For SDK callers that hold the raw identifier, end_user_id is the more ergonomic choice — the server performs the same hash internally.

action
string (Action)
Enum: "blocked" "flagged" "warned" "allowed"
surface
string (Surface)
Enum: "user_message" "assistant_output" "tool_result" "tool_params" "document" "agent_message" "mcp_resource" "mcp_tool_description"
since
string <date-time>
until
string <date-time>
cursor
string

Opaque cursor from a previous page.

limit
integer [ 1 .. 200 ]
Default: 50
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "next_cursor": "string"
}

Retrieve a scan

Raw content is present in the response only if options.capture was true at scan time. Subject to the configured retention window (default 30 days).

Authorizations:
bearerAuth
path Parameters
scan_id
required
string^(scan_)?[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "kind": "content",
  • "surface": "user_message",
  • "context": {
    },
  • "findings": [
    ],
  • "decision": {
    },
  • "content_stored": true,
  • "content": {
    }
}

policies

Rule sets applied to scans.

List policies

Authorizations:
bearerAuth
query Parameters
cursor
string

Opaque cursor from a previous page.

limit
integer [ 1 .. 200 ]
Default: 50
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "next_cursor": "string"
}

Create a policy

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
name
required
string <= 120 characters
mode
string
Default: "enforce"
Enum: "enforce" "observe"

enforce: decision.action must be honored by clients. observe: detectors run and webhooks fire as normal, but the action is advisory — recommended for pilot rollouts where customers want to see what would happen before committing.

required
Array of objects (PolicyRule) <= 64 items
default_action
required
string (Action)
Enum: "blocked" "flagged" "warned" "allowed"
object
allowed_mcp_servers
Array of strings <= 200 items [ items <= 255 characters ]
blocked_mcp_servers
Array of strings <= 200 items [ items <= 255 characters ]
Array of objects (McpToolRef) <= 500 items
Array of objects (McpToolRef) <= 500 items

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "mode": "enforce",
  • "rules": [
    ],
  • "default_action": "blocked",
  • "detector_config": {
    },
  • "allowed_mcp_servers": [
    ],
  • "blocked_mcp_servers": [
    ],
  • "allowed_mcp_tools": [
    ],
  • "blocked_mcp_tools": [
    ]
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "revision_id": "string",
  • "name": "string",
  • "mode": "enforce",
  • "rules": [
    ],
  • "default_action": "blocked",
  • "detector_config": {
    },
  • "allowed_mcp_servers": [
    ],
  • "blocked_mcp_servers": [
    ],
  • "allowed_mcp_tools": [
    ],
  • "blocked_mcp_tools": [
    ],
  • "enabled": true,
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Get a policy

Authorizations:
bearerAuth
path Parameters
policy_id
required
string^pol_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "revision_id": "string",
  • "name": "string",
  • "mode": "enforce",
  • "rules": [
    ],
  • "default_action": "blocked",
  • "detector_config": {
    },
  • "allowed_mcp_servers": [
    ],
  • "blocked_mcp_servers": [
    ],
  • "allowed_mcp_tools": [
    ],
  • "blocked_mcp_tools": [
    ],
  • "enabled": true,
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Replace a policy in place

Full replace, not patch. Minimal version intentionally has no revision history — mutations are destructive. AuditEvent of kind: management records the change.

Authorizations:
bearerAuth
path Parameters
policy_id
required
string^pol_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
name
required
string <= 120 characters
mode
string
Default: "enforce"
Enum: "enforce" "observe"

enforce: decision.action must be honored by clients. observe: detectors run and webhooks fire as normal, but the action is advisory — recommended for pilot rollouts where customers want to see what would happen before committing.

required
Array of objects (PolicyRule) <= 64 items
default_action
required
string (Action)
Enum: "blocked" "flagged" "warned" "allowed"
object
allowed_mcp_servers
Array of strings <= 200 items [ items <= 255 characters ]
blocked_mcp_servers
Array of strings <= 200 items [ items <= 255 characters ]
Array of objects (McpToolRef) <= 500 items
Array of objects (McpToolRef) <= 500 items
enabled
boolean

Responses

Request samples

Content type
application/json
{
  • "name": "string",
  • "mode": "enforce",
  • "rules": [
    ],
  • "default_action": "blocked",
  • "detector_config": {
    },
  • "allowed_mcp_servers": [
    ],
  • "blocked_mcp_servers": [
    ],
  • "allowed_mcp_tools": [
    ],
  • "blocked_mcp_tools": [
    ],
  • "enabled": true
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "revision_id": "string",
  • "name": "string",
  • "mode": "enforce",
  • "rules": [
    ],
  • "default_action": "blocked",
  • "detector_config": {
    },
  • "allowed_mcp_servers": [
    ],
  • "blocked_mcp_servers": [
    ],
  • "allowed_mcp_tools": [
    ],
  • "blocked_mcp_tools": [
    ],
  • "enabled": true,
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Delete a policy (soft-delete; disables)

Authorizations:
bearerAuth
path Parameters
policy_id
required
string^pol_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/problem+json
{}

Dry-run a policy against a past scan or synthetic findings

Answers "what would this policy do to this scan" without persisting anything or changing state. Intended for the observe → tune → enforce workflow: run in observe mode, collect a week of data, simulate candidate policy edits against captured scans before committing.

Authorizations:
bearerAuth
path Parameters
policy_id
required
string^pol_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
One of
scan_id
required
string

Responses

Request samples

Content type
application/json
{
  • "surface": "user_message",
  • "findings": [
    ]
}

Response samples

Content type
application/json
{
  • "decision": {
    },
  • "matched_rule": {
    },
  • "diff_from_current": {
    }
}

webhooks

HMAC-SHA256 signature verification for outbound event deliveries. The verification helpers ship in the Python and TypeScript SDKs today; outbound delivery is rolling out in a future release.

List webhooks

Authorizations:
bearerAuth
query Parameters
cursor
string

Opaque cursor from a previous page.

limit
integer [ 1 .. 200 ]
Default: 50
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "next_cursor": "string"
}

Register a webhook

Signing secret is returned exactly once on this response. Rotate via :rotate-secret — losing it requires rotation. URL must be https://; RFC1918 / link-local / cloud-metadata addresses are rejected server-side.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
url
required
string <uri> ^https://

Must be https://. RFC1918, link-local, and cloud-metadata addresses are rejected server-side.

description
string <= 500 characters
events
required
Array of strings (WebhookEventType) non-empty
Items Enum: "scan.blocked" "scan.flagged" "scan.warned" "scan.allowed" "webhook.failing"
include_content
boolean
Default: false

Responses

Request samples

Content type
application/json
{
  • "description": "string",
  • "events": [
    ],
  • "include_content": false
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "description": "string",
  • "events": [
    ],
  • "include_content": false,
  • "active": true,
  • "last_failure_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z",
  • "secret": "string"
}

Get a webhook (no secret)

Authorizations:
bearerAuth
path Parameters
webhook_id
required
string^wh_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "description": "string",
  • "events": [
    ],
  • "include_content": false,
  • "active": true,
  • "last_failure_at": "2019-08-24T14:15:22Z",
  • "created_at": "2019-08-24T14:15:22Z"
}

Delete a webhook

Authorizations:
bearerAuth
path Parameters
webhook_id
required
string^wh_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/problem+json
{}

Rotate a webhook's signing secret

Old secret remains valid for grace_seconds (default 24h). New plaintext secret is returned once.

Authorizations:
bearerAuth
path Parameters
webhook_id
required
string^wh_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
optional
grace_seconds
integer [ 0 .. 604800 ]
Default: 86400

Responses

Request samples

Content type
application/json
{
  • "grace_seconds": 86400
}

Response samples

Content type
application/json
{
  • "webhook_id": "string",
  • "secret": "string",
  • "grace_expires_at": "2019-08-24T14:15:22Z"
}

List delivery attempts for a webhook

Enterprise procurement (Indicium, KPMG, Experian) requires SIEM-integration debuggability — "why didn't this event reach my receiver?" Answerable without a support ticket.

Authorizations:
bearerAuth
path Parameters
webhook_id
required
string^wh_[0-9A-HJKMNP-TV-Z]{26}$
query Parameters
status
string (WebhookDeliveryStatus)
Enum: "pending" "succeeded" "failed" "dead_lettered"
since
string <date-time>
cursor
string

Opaque cursor from a previous page.

limit
integer [ 1 .. 200 ]
Default: 50
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "next_cursor": "string"
}

Manually redeliver a failed or dead-lettered event

Authorizations:
bearerAuth
path Parameters
delivery_id
required
string^whd_[0-9A-HJKMNP-TV-Z]{26}$
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Responses

Response samples

Content type
application/json
{
  • "id": "string",
  • "webhook_id": "string",
  • "event_id": "string",
  • "event_type": "scan.blocked",
  • "status": "pending",
  • "attempt": 1,
  • "next_attempt_at": "2019-08-24T14:15:22Z",
  • "response": {
    },
  • "error": "string",
  • "created_at": "2019-08-24T14:15:22Z",
  • "delivered_at": "2019-08-24T14:15:22Z"
}

audit

Unified query log for detection and management events.

Query audit events (detection and management, unified)

Set kind=detection to see traffic scans, kind=management to see administrative mutations (API key revocations, policy edits, webhook changes). Response supports Accept: text/csv up to 10,000 rows per page — async export is deferred.

Authorizations:
bearerAuth
query Parameters
kind
string (AuditEventKind)
Enum: "detection" "management"
actor_id
string <= 255 characters
agent_id
string <= 255 characters
surface
string (Surface)
Enum: "user_message" "assistant_output" "tool_result" "tool_params" "document" "agent_message" "mcp_resource" "mcp_tool_description"
action
string (Action)
Enum: "blocked" "flagged" "warned" "allowed"
detector
string
resource_type
string
Enum: "api_key" "policy" "webhook"
resource_id
string
since
string <date-time>
until
string <date-time>
cursor
string

Opaque cursor from a previous page.

limit
integer [ 1 .. 10000 ]
Default: 50

Page size. Default 50. Cap is raised to 10,000 on this endpoint (vs. the global 200) so text/csv exports can paginate densely when callers pass limit explicitly. The shared Limit parameter component is intentionally not referenced here for that reason.

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
{
  • "data": [
    ],
  • "next_cursor": "string"
}

detectors

Currently-loaded detector metadata.

List detectors currently available from the loaded bundle

Replaces v0's /v1/framework-mappings. Framework mappings are embedded per detector. Today's bundles ship a small, bounded set (tens of detectors), but the endpoint is cursor-paginated so custom / per-org detector catalogs can land later without a wire break.

Discovery endpoint. Every authenticated principal can list the catalog regardless of scope — provisioning a detectors:read scope just to enumerate available detectors creates noise without a meaningful privilege boundary (the catalog is not sensitive). x-required-scopes: [] declares the unscoped intent explicitly.

Authorizations:
bearerAuth
query Parameters
cursor
string

Opaque cursor from a previous page.

limit
integer [ 1 .. 200 ]
Default: 50
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "next_cursor": "string"
}

analytics

Aggregate metrics for the dashboard analytics view.

Scan totals + action breakdown for a window

Returns the total scan count and per-action breakdown (allowed / flagged / warned / blocked) for the requested window, plus a delta vs. the immediately-preceding window of equal length (e.g. last 7 days vs. the 7 days before that). Used by the analytics KPI tiles. Counts are org-scoped via RLS.

Authorizations:
bearerAuth
query Parameters
since
required
string <date-time>

Inclusive start of the window (RFC3339).

until
required
string <date-time>

Exclusive end of the window (RFC3339).

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "window": {
    },
  • "total": 0,
  • "allowed": 0,
  • "flagged": 0,
  • "warned": 0,
  • "blocked": 0,
  • "prior_period_delta": {
    }
}

Per-bucket scan action counts over a window

Returns a contiguous time-series of action counts bucketed at the requested granularity. Buckets are aligned to the bucket boundary in UTC (e.g. bucket=1h aligns to top-of-hour) and zero-count buckets are emitted so the chart has no gaps. The backend caps the number of returned points to a fixed maximum per bucket size (validated server-side); requests that would exceed it are rejected with 400 analytics.window_too_wide.

Authorizations:
bearerAuth
query Parameters
since
required
string <date-time>
until
required
string <date-time>
bucket
required
string (AnalyticsBucket)
Enum: "15m" "1h" "1d"

Aggregation bucket width for time-series analytics endpoints. The server caps the maximum window length per bucket value (smaller buckets ⇒ tighter window cap) so any response stays bounded — requests outside the cap are rejected with 400 analytics.window_too_wide.

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "points": [
    ]
}

Scan counts grouped by `surface`

Returns total scan counts grouped by the Surface enum for the requested window. Surfaces with zero traffic in the window are omitted from items (callers render "no data" client-side rather than zero bars).

Authorizations:
bearerAuth
query Parameters
since
required
string <date-time>
until
required
string <date-time>
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Most-triggered detectors over a window

Returns the top-N detectors by trigger count over the requested window, ordered by count desc. Ties break on detector name (ascending) for stable rendering. Each row carries the detector name and its category for grouping in the UI.

Authorizations:
bearerAuth
query Parameters
since
required
string <date-time>
until
required
string <date-time>
limit
integer [ 1 .. 50 ]
Default: 10

Maximum rows to return. Default 10, server-capped at 50.

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "items": [
    ]
}

Hot-path latency percentiles over a window

Returns p50 / p95 / p99 hot-path latency in milliseconds per bucket. Sourced from the same aggregate rollups that back the operator Grafana dashboards; the SaaS dashboard surfaces them to operators so they can correlate detector latency with the scan-volume time-series above. Buckets align to UTC boundaries. Buckets with zero traffic are OMITTED (no row in points) — unlike the scan time-series, where zero-count buckets are emitted as 0. Callers tolerate the gaps when rendering.

Authorizations:
bearerAuth
query Parameters
since
required
string <date-time>
until
required
string <date-time>
bucket
required
string (AnalyticsBucket)
Enum: "15m" "1h" "1d"

Aggregation bucket width for time-series analytics endpoints. The server caps the maximum window length per bucket value (smaller buckets ⇒ tighter window cap) so any response stays bounded — requests outside the cap are rejected with 400 analytics.window_too_wide.

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "points": [
    ]
}

organizations

Self-serve organization creation for first-login SSO users (SaaS only).

Create the calling user's first organization (self-serve onboarding)

Self-serve org creation for a brand-new WorkOS-authenticated user whose JWT carries no org_id claim — i.e. they have signed in but their WorkOS user is not yet a member of any Organization. Without this endpoint such a user is locked out: every other /v1/* operation requires a tenant-scoped principal, and the only way to obtain one is to already belong to an org.

Flow on success (SaaS):

  1. Backend calls the WorkOS Organizations API to create a new Organization with the supplied display_name.
  2. Backend adds the calling user to the new Organization as a member with role=owner.
  3. Caller signs out + back in (or refreshes their session) so WorkOS issues a fresh JWT with the new org_id claim.
  4. The next authenticated request — ANY tenant-scoped endpoint, not just GET /v1/me — triggers JIT provisioning of the local auth.organizations + auth.users rows in the AuthenticationFilter before the controller runs. There is NO ordering dependency on /v1/me; SDKs can call POST /v1/scans (or any other scoped endpoint) immediately after re-auth and it Just Works. (Pre-fix the local rows were provisioned only on /v1/me, which 75% of new orgs in a 2-day window happened to hit a events.inspection_events_org_id_fkey violation against — see the OrgRowProvisioner contract in the auth module for the corrected handoff.)

Self-hosted deployments do NOT support this endpoint — org provisioning is admin-driven through the customer's IdP. The endpoint returns 501 onboarding.unsupported_in_self_hosted when app.deployment-mode=self_hosted.

Authorisation: x-required-scopes: [] (any authenticated caller). Restricting the endpoint to admin-scoped callers would chicken-and-egg the bootstrap case it exists to solve.

Idempotency: WorkOS Organization names are NOT unique, so an unintended retry would silently create a duplicate org. The backend treats (external_subject, request_body) within a 24h window as the dedup key when an Idempotency-Key is supplied; without one, callers MUST avoid retrying on transport errors.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
display_name
required
string [ 1 .. 160 ] characters

Responses

Request samples

Content type
application/json
{
  • "display_name": "string"
}

Response samples

Content type
application/json
{
  • "id": "string",
  • "slug": "string",
  • "display_name": "string",
  • "created_at": "2019-08-24T14:15:22Z"
}

Disband the calling user's organization (owner-only, sole-member)

Permanently deletes the calling user's organization, all data scoped to it (scans, events, policies, API keys, invites, audit events, etc.), and the corresponding WorkOS Organization. Used by the dashboard's Settings → Danger Zone "Disband organization" flow when the operator wants to wipe a personal org and start over with a fresh one.

Authorisation is intentionally narrower than the standard x-required-scopes machinery can express: the caller must be the org's owner (auth.users.role = 'owner') AND the org's sole member. Both conditions are enforced service-side after scope admission. Any other shape — non-owner caller, another member present, pending invite outstanding — returns 409 Conflict with a problem code naming the specific guard that tripped, so the dashboard can surface actionable guidance instead of a generic "forbidden". The sole-member constraint exists because v1 has no member-transfer or pre-disband handoff UX; relaxing it would let an owner orphan their teammates.

Self-hosted deployments do NOT support this endpoint — org teardown there is administrator-driven through the customer's IdP and infra teardown plan. Returns 501 organizations.delete.unsupported_in_self_hosted when app.deployment-mode=self_hosted.

After a successful delete the caller's WorkOS session still holds the (now-gone) org_id claim. The next authenticated request falls into the orgless first-login flow and the dashboard prompts them to create a fresh org via POST /v1/organizations (the usual self-serve onboarding surface). Forcing a re-auth on the client side is optional but recommended — the stale claim does not authorise anything.

Idempotency: the operation deletes the calling user's row last, so a retry that arrives before the WorkOS session has refreshed will 401 (auth.unauthenticated) on the auth filter, not 404 on this endpoint. The caller should treat the first 204 as authoritative and stop retrying.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/problem+json
{}

labeled-events

Labeled events that feed model training and evaluation — a unified shape for red-team findings, sampled production traffic, customer feedback, and reviewer-curated examples. Sensitive content is redacted before storage and gated on consent.

Batch-append labeled events

Append a batch of up to 100 labeled events. The server applies the org's redaction policy (/v1/org/redaction-policy) to every event's input, output, and comparator payload BEFORE insertion; the wire payload carries raw values and the stored row records the active redaction_version. Idempotency-Key replay semantics are the same as every other state-changing POST in v1.

Used by every source (redteam, production_sample, feedback, reviewed); the source discriminator on each event determines the source-specific extension fields the server expects to find in source_data.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
required
Array of objects (LabeledEventInput) [ 1 .. 100 ] items

Responses

Request samples

Content type
application/json
{
  • "events": [
    ]
}

Response samples

Content type
application/json
{
  • "accepted": 0,
  • "event_ids": [
    ]
}

List labeled events in the caller's org

Cursor-paginated read of the substrate. Filterable by source, severity, training_consent, and observation-time bounds. Sort order is observed_at desc, event_id desc.

Authorizations:
bearerAuth
query Parameters
cursor
string

Opaque cursor from a previous page.

limit
integer [ 1 .. 200 ]
Default: 50
source
string (LabeledEventSource)
Enum: "redteam" "production_sample" "feedback" "reviewed"

Discriminator for the source that wrote the row. Each value pairs with a source_data schema variant (see LabeledEventSourceData).

severity
string (Severity)
Enum: "critical" "high" "medium" "low" "info"
training_consent
boolean
observed_after
string <date-time>
observed_before
string <date-time>
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "next_cursor": "string"
}

Fetch one labeled event with full source_data

Returns the full row, including the source-specific source_data payload (which list responses omit for size). Use this to drill into a single event from the dashboard's list view.

Authorizations:
bearerAuth
path Parameters
event_id
required
string^lev_[0-9A-HJKMNP-TV-Z]{26}$

Labeled-event identifier. Prefix lev_ followed by a 26-character Crockford base32 ULID/CUID.

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "event_id": "string",
  • "source": "redteam",
  • "observed_at": "2019-08-24T14:15:22Z",
  • "input_redacted": "string",
  • "output_redacted": "string",
  • "verdict": "allowed",
  • "verdict_confidence": 1,
  • "verdict_reason": "string",
  • "severity": "critical",
  • "framework_refs": [
    ],
  • "comparator": {
    },
  • "source_data": {
    },
  • "training_consent": true,
  • "redaction_version": "redactor-v1.0.0",
  • "redaction_policy_id": "string",
  • "evidence_blob_keys": [
    ],
  • "created_at": "2019-08-24T14:15:22Z"
}

Delete a single labeled event (operator right-to-delete)

Hard-deletes the row. Designed for one-off operator removal (mistakenly-ingested run, customer right-to-delete request). Bulk delete by date range / source is tracked separately — see docs/design/redteam-ingest.md §8.

Authorizations:
bearerAuth
path Parameters
event_id
required
string^lev_[0-9A-HJKMNP-TV-Z]{26}$

Labeled-event identifier. Prefix lev_ followed by a 26-character Crockford base32 ULID/CUID.

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/problem+json
{}

Request a presigned S3 upload URL for an evidence blob

Operator uploads a screenshot / HAR / log bundle via the returned presigned URL, then references the resulting s3_key in a subsequent appendLabeledEvents call via evidence_blob_keys[].s3_key. The upload URL is time-bounded (default 15 minutes) and pinned to the supplied SHA-256 + size.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
kind
required
string <= 64 characters

Operator-visible category for the artefact (screenshot, har, log_bundle, ...).

sha256
required
string^[a-f0-9]{64}$

Lowercase hex SHA-256 of the artefact bytes. The server pins the upload URL to this digest so the upload is content- addressable.

size_bytes
required
integer [ 1 .. 104857600 ]

Artefact size in bytes. Hard-cap 100 MB; revisit when a customer hits the limit.

Responses

Request samples

Content type
application/json
{
  • "kind": "string",
  • "sha256": "string",
  • "size_bytes": 1
}

Response samples

Content type
application/json
{
  • "upload_url": "http://example.com",
  • "s3_key": "string",
  • "expires_at": "2019-08-24T14:15:22Z"
}

redteam

Redteam-specific helpers — run-level scope (start/finalize) and deduplicated findings — that sit on top of the labeled-events substrate. Operator-driven; not a customer-facing product.

Create a redteam run header

Starts a run record. The redteam CLI calls this once at run start and uses the returned run_id as the source_data.run_id on every subsequent per-event appendLabeledEvents call. The run is in state running until finalizeRedteamRun is called.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
scope_hash
required
string^[a-f0-9]{64}$
object
upload_enabled
required
boolean
training_consent
required
boolean

Per-run training consent. Propagated onto every event row written under this run_id. true requires upload_enabled: true (no consent without storage); the CLI also enforces this client-side.

written_authorization
required
boolean

The operator confirmed in writing that the target's owner authorised the run. The CLI refuses to start without it; the backend rejects creation with 400 redteam.written_authorization_required if false.

attached_authenticated
required
boolean
started_at
required
string <date-time>
required
object
required
object
redteam_version
required
string
operator_id
string or null

Responses

Request samples

Content type
application/json
{
  • "scope_hash": "string",
  • "scope_redacted": { },
  • "upload_enabled": true,
  • "training_consent": true,
  • "written_authorization": true,
  • "attached_authenticated": true,
  • "started_at": "2019-08-24T14:15:22Z",
  • "model_versions": {
    },
  • "corpus_hashes": {
    },
  • "redteam_version": "string",
  • "operator_id": "string"
}

Response samples

Content type
application/json
{
  • "run_id": "string",
  • "state": "running",
  • "scope_hash": "string",
  • "scope_redacted": { },
  • "upload_enabled": true,
  • "training_consent": true,
  • "written_authorization": true,
  • "attached_authenticated": true,
  • "started_at": "2019-08-24T14:15:22Z",
  • "finished_at": "2019-08-24T14:15:22Z",
  • "verdict": "critical",
  • "verdict_reason": "string",
  • "model_versions": {
    },
  • "corpus_hashes": {
    },
  • "redteam_version": "string",
  • "operator_id": "string",
  • "created_at": "2019-08-24T14:15:22Z"
}

Fetch a redteam run header

Returns the run record including its lifecycle state, the pinned model versions and corpus hashes, and (once finalized) the operator's terminal verdict. Per-event probes live in events.labeled_events and are queried separately via listLabeledEvents filtered by source_data.run_id.

Authorizations:
bearerAuth
path Parameters
run_id
required
string^rtr_[0-9A-HJKMNP-TV-Z]{26}$

Redteam-run identifier. Prefix rtr_ followed by a 26-character Crockford base32 ULID/CUID.

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "run_id": "string",
  • "state": "running",
  • "scope_hash": "string",
  • "scope_redacted": { },
  • "upload_enabled": true,
  • "training_consent": true,
  • "written_authorization": true,
  • "attached_authenticated": true,
  • "started_at": "2019-08-24T14:15:22Z",
  • "finished_at": "2019-08-24T14:15:22Z",
  • "verdict": "critical",
  • "verdict_reason": "string",
  • "model_versions": {
    },
  • "corpus_hashes": {
    },
  • "redteam_version": "string",
  • "operator_id": "string",
  • "created_at": "2019-08-24T14:15:22Z"
}

Finalize a redteam run (terminal state, records the verdict)

Transitions the run to state finished. The verdict is the operator's overall judgement of the run; the comparator's per-event verdict lives on each labeled_events row. A finalized run is terminal — subsequent calls return 409 redteam.run_already_finalized.

Authorizations:
bearerAuth
path Parameters
run_id
required
string^rtr_[0-9A-HJKMNP-TV-Z]{26}$

Redteam-run identifier. Prefix rtr_ followed by a 26-character Crockford base32 ULID/CUID.

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
finished_at
required
string <date-time>
verdict
required
string (Severity)
Enum: "critical" "high" "medium" "low" "info"
verdict_reason
string or null <= 4096 characters

Responses

Request samples

Content type
application/json
{
  • "finished_at": "2019-08-24T14:15:22Z",
  • "verdict": "critical",
  • "verdict_reason": "string"
}

Response samples

Content type
application/json
{
  • "run_id": "string",
  • "state": "running",
  • "scope_hash": "string",
  • "scope_redacted": { },
  • "upload_enabled": true,
  • "training_consent": true,
  • "written_authorization": true,
  • "attached_authenticated": true,
  • "started_at": "2019-08-24T14:15:22Z",
  • "finished_at": "2019-08-24T14:15:22Z",
  • "verdict": "critical",
  • "verdict_reason": "string",
  • "model_versions": {
    },
  • "corpus_hashes": {
    },
  • "redteam_version": "string",
  • "operator_id": "string",
  • "created_at": "2019-08-24T14:15:22Z"
}

Append deduped findings to a redteam run

Findings are the operator's rollup across per-iteration probe events. Each finding cites a representative_event_id pointing back at the canonical labeled_events row that exhibits the attack class. Calling this after finalizeRedteamRun returns 409 redteam.run_already_finalized.

Authorizations:
bearerAuth
path Parameters
run_id
required
string^rtr_[0-9A-HJKMNP-TV-Z]{26}$

Redteam-run identifier. Prefix rtr_ followed by a 26-character Crockford base32 ULID/CUID.

header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
required
Array of objects (RedteamFindingInput) [ 1 .. 100 ] items

Responses

Request samples

Content type
application/json
{
  • "findings": [
    ]
}

Response samples

Content type
application/json
{
  • "accepted": 0,
  • "finding_ids": [
    ]
}

org-admin

Org-level configuration surfaces (redaction policy, training- consent defaults, ...). Admin-scoped writes; reads are unscoped so any authenticated principal in the org can see the active policy that applies to their data.

Fetch the active redaction policy for the caller's org

Every authenticated principal in the org can read the active policy — knowing what redaction applies to your own data is not a privileged disclosure. x-required-scopes: [] declares the unscoped intent explicitly.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Responses

Response samples

Content type
application/json
{
  • "policy_id": "string",
  • "field_rules": {
    },
  • "default_obfuscation": "redact",
  • "override_allowed_for_reviewers": true,
  • "redaction_version": "redactor-v1.0.0",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

Update the org's redaction policy

Replaces the active policy. The new row records its own redaction_version so previously-captured labeled_events rows retain provenance to the policy they were redacted under. Future re-redact passes (when a stricter pipeline ships) can re-redact only rows captured under earlier versions.

Authorizations:
bearerAuth
header Parameters
Sonny-Api-Version
string^\d{4}-\d{2}-\d{2}$

Pin a date version (e.g. 2026-06-01). Unpinned clients get latest stable. Retirement: 6-month deprecation via Sunset + Deprecation response headers.

Idempotency-Key
string <= 255 characters

Client-generated key. Server stores key + body-hash for 24h and replays the original response on reuse. Mismatched body with the same key returns 409 idempotency.key_reuse_mismatch.

Request Body schema: application/json
required
required
object
default_obfuscation
required
string (RedactionObfuscation)
Enum: "redact" "hash" "pass_through"

How the redaction pipeline treats a field. redact rewrites the value to a fixed mask; hash replaces it with a salted hex hash (so equal inputs collapse to equal outputs for deduplication); pass_through keeps the raw value. Per-field rules override the org default.

override_allowed_for_reviewers
required
boolean
redaction_version
required
string (RedactionVersion) ^redactor-v\d+\.\d+\.\d+$

Version identifier of the redaction policy that was applied to the row at insert time. Pattern redactor-vMAJOR.MINOR.PATCH so a future re-redact pass can target rows captured under earlier policies.

Responses

Request samples

Content type
application/json
{
  • "field_rules": {
    },
  • "default_obfuscation": "redact",
  • "override_allowed_for_reviewers": true,
  • "redaction_version": "redactor-v1.0.0"
}

Response samples

Content type
application/json
{
  • "policy_id": "string",
  • "field_rules": {
    },
  • "default_obfuscation": "redact",
  • "override_allowed_for_reviewers": true,
  • "redaction_version": "redactor-v1.0.0",
  • "created_at": "2019-08-24T14:15:22Z",
  • "updated_at": "2019-08-24T14:15:22Z"
}

health

Liveness and readiness.

Legacy liveness probe

Pre-v1 liveness path, kept as a real endpoint for any caller still pointing at /health. Functionally identical to /healthz. New integrations should use /healthz (k8s-style naming) — this path is documented here so the spec reflects every real endpoint the server serves, not deprecated.

Responses

Response samples

Content type
application/json
{
  • "status": "ok"
}

Liveness probe

Responses

Response samples

Content type
application/json
{
  • "status": "ok"
}

Readiness probe (bundle loaded + DB reachable)

Responses

Response samples

Content type
application/json
{
  • "status": "ok"
}

Outbound scan event (blocked / flagged / warned / allowed) Webhook

Signed HMAC-SHA256: Sonny-Signature: t=<unix>,v1=<hex> over {t}.{body}. Replay window: 5 minutes. Delivery schedule: one initial attempt at scan time, then up to 6 retries at 30s, 2m, 10m, 1h, 6h, 24h after the initial (7 attempts total). Failure on attempt 7 dead-letters the delivery.

include_content is off by default — events carry a compact scan summary without raw content. Customers fetch full scans via GET /v1/scans/{id} if they need more.

Authentication: receivers authenticate deliveries by verifying Sonny-Signature — SonnyLabs does not present a Bearer token. Explicitly cleared with security: [] so generated receivers don't demand one.

header Parameters
Sonny-Signature
required
string
Example: t=1712345678,v1=abc123...
Sonny-Event-Id
required
string
Sonny-Api-Version
required
string
Example: 2026-06-01
Request Body schema: application/json
required
type
required
string (WebhookEventType)
Enum: "scan.blocked" "scan.flagged" "scan.warned" "scan.allowed" "webhook.failing"
id
required
string
created
required
string <date-time>
api_version
required
string^\d{4}-\d{2}-\d{2}$
data
required
object

Shape depends on type. scan.* carries a compact scan summary without raw content (unless include_content is on).

Responses

Request samples

Content type
application/json
{
  • "type": "scan.blocked",
  • "id": "string",
  • "created": "2019-08-24T14:15:22Z",
  • "api_version": "string",
  • "data": { }
}

Response samples

Content type
application/json
null