# SigWise API reference (v1.0.0)

Generated from the OpenAPI specification: https://sigwise.ai/docs/openapi.json (YAML: https://sigwise.ai/docs/openapi.yaml).

Base URL: `https://api.sigwise.ai`

SigWise reads the events and messages on your platform and answers the
questions you configure about each object (a user, a listing, an order)
as typed **signals**: probabilities (`noul`), positions on a labelled
spectrum (`score`) and one-of-N classifications (`choice`).

You configure signals, send events about objects, and read the latest
answers, or get them pushed to you through webhooks and rules.

## Authentication

Integrations authenticate with an API key, which is a pair:

- a public **key ID**, sent as the `api_key` query parameter
  (or as the JWT header `kid`);
- a **signing secret**, which never leaves your server.

Every request carries `Authorization: Bearer <jwt>`: an HS256 JWT you sign
with the whole secret string. It needs `iat` and `exp` claims at most five
minutes apart (with ±60 s of clock skew), and can bind itself to one
request with `htm` (the HTTP method) and `htu` (the path). The official
SDKs do this for you on every request.

## Errors

Every error uses the same envelope,
`{"error": {"code": "not_found", "message": "signal not found"}}`, with a
stable machine-readable `code` and a human-readable `message`.

## Rate limits

Requests under `/v1` are limited per account (every API key and console
session of an account share one budget; 3,000 requests a minute by
default). Over the limit, the API answers `429` with code `rate_limited`
and a `Retry-After` header in seconds. Batch several events into one
ingest request rather than sending them one by one.

## Objects

An object is anything you want answers about: a user, a listing, an
order. It is identified by your own `object_id` and exists as soon as
you send its first event.

### GET /v1/overview

**Get tenant overview**

Tenant-wide totals and per-signal value distributions, computed with SQL aggregates.

Authentication: API key (signed JWT).

| Status | Description | Body |
|---|---|---|
| 200 | The overview. | `Overview` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### POST /v1/analyze

**Re-analyze every object**

Schedules a re-analysis of every object the tenant has, typically after
changing signal configuration. Jobs run at bulk priority, behind live
traffic. Each analysis is billed.

Authentication: API key (signed JWT).

| Status | Description | Body |
|---|---|---|
| 202 | Scheduled. | `AnalyzeAllScheduled` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### GET /v1/objects

**List objects**

Returns one page of objects with their latest analysis.

`q` is either a substring of the object ID or display name, or a
signal query such as `is_scammer > 90 and trust_score < 1` (see
[Search and conditions](https://sigwise.ai/docs/guides/queries)).

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `q` | query | string | no | Substring search, or a signal query like `is_scammer >= 90, buyer_intent = ready_to_buy`. |
| `sort` | query | "recent" \| "flagged" \| "trust" | no | `recent` (last seen first, the default), `flagged` (highest yes/no probability first) or `trust` (highest score first). |
| `limit` | query | integer | no |  |
| `offset` | query | integer | no | Rows to skip. Prefer `cursor` for deep pages; offsets get slower the further they go. |
| `cursor` | query | string | no | The `next_cursor` of the previous page. Pages by position rather than offset, so every page is equally fast; `offset` is ignored when set. Keep `q` and `sort` the same across pages. Signal queries page by `offset` only. |

| Status | Description | Body |
|---|---|---|
| 200 | A page of objects. | `ObjectList` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 429 | Too many requests or attempts; retry after the `Retry-After` delay. | `Error` |

### GET /v1/objects/{object_id}

**Get an object's analysis**

Returns the latest answer for each signal computed for the object, plus
the configured signals that have no answer yet (`pending`).

Reads are cached for a few seconds; the `X-Cache` header says whether
this one was a `HIT` or a `MISS`. Ingesting events and completed
analyses invalidate the cache.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `object_id` | path | string | yes | Your identifier for the object, e.g. `user-42`. |

| Status | Description | Body |
|---|---|---|
| 200 | The object's analysis. | `ObjectAnalysis` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### GET /v1/objects/{object_id}/state

**Get an object's compacted history**

Older events are folded into a rolling summary so the analyzer gets a
bounded payload while keeping long-term signal. This returns that
summary and the compaction cursor.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `object_id` | path | string | yes | Your identifier for the object, e.g. `user-42`. |

| Status | Description | Body |
|---|---|---|
| 200 | The compacted state. | `ObjectState` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### POST /v1/objects/{object_id}/analyze

**Re-analyze an object**

Schedules an immediate re-analysis without a new event, for example
right after adding a signal. Runs at live priority; poll
`GET /v1/objects/{object_id}` for the result. Returns `404` when the
object has no events.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `object_id` | path | string | yes | Your identifier for the object, e.g. `user-42`. |

| Status | Description | Body |
|---|---|---|
| 202 | Scheduled. | `AnalyzeScheduled` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

## Events

Events and messages are the evidence an object's answers are computed
from. Ingestion is asynchronous by default and returns `202`; set
`wait: true` to score the object inline instead (direct moderation).

### POST /v1/objects/{object_id}/events

**Ingest events**

Records one or more events or messages about an object.

**Asynchronous (default).** Returns `202` immediately and schedules a
debounced analysis: a burst of events for one object coalesces into a
single analysis once activity settles. Read the answers with
`GET /v1/objects/{object_id}` or receive them by webhook.

**Synchronous (`wait: true`).** Blocks on the analyzer and returns the
verdict inline with `200`, so you can approve, review or block content
before publishing it. Restrict scoring to some signals with `signals`,
and score the request's events in isolation with
`include_history: false`.

Returns `402` when the tenant's balance is empty. A synchronous request
returns `429` when the account already has the maximum number of
synchronous analyses in flight (8 by default), and `503` with
`analyzer_busy` when the analyzer is throttling; the events are
recorded either way, so don't resend them: after `Retry-After`, request
an analysis with `POST /v1/objects/{object_id}/analyze` and read the
answers with `GET /v1/objects/{object_id}` or by webhook.

**Safe retries.** Send an `Idempotency-Key` header (any unique string,
such as a UUID) and retry with the same key after a timeout or a `5xx`:
the events are recorded once. A repeat of an asynchronous request is
answered like the first and carries `Idempotent-Replayed: true`. A repeat
of a synchronous request returns `409` without analyzing (or charging)
again; read the verdict with `GET /v1/objects/{object_id}`. Reusing a key
for a different object returns `422`. Keys are remembered for 24 hours.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `object_id` | path | string | yes | Your identifier for the object, e.g. `user-42`. |
| `Idempotency-Key` | header | string | no | A unique string (such as a UUID) that makes the request safe to retry: repeating it with the same key records nothing again. At most 255 characters; remembered for 24 hours. |

Request body: `IngestRequest`

```json
{
  "object_type": "user",
  "events": [
    {
      "type": "event",
      "name": "profile.updated",
      "metadata": {
        "field": "bio"
      }
    },
    {
      "type": "message",
      "content": "is this still available? can I pay by wire?"
    }
  ]
}
```

| Status | Description | Body |
|---|---|---|
| 200 | `wait: true`: the events were recorded and the object scored. | `ModerationVerdict` |
| 202 | The events were recorded and an analysis scheduled. | `IngestAccepted` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 402 | The tenant's prepaid balance is empty. | `Error` |
| 409 | The request conflicts with the resource's current state. | `Error` |
| 422 | The `Idempotency-Key` was already used for a different object. | `Error` |
| 429 | Too many requests or attempts; retry after the `Retry-After` delay. | `Error` |
| 502 | An upstream service (the analyzer, Stripe, an OAuth provider) failed. | `Error` |
| 503 | The analyzer is throttling requests. The events were recorded; retry the verdict after the `Retry-After` delay. | `Error` |

### GET /v1/objects/{object_id}/events

**List an object's events**

Returns up to the 200 most recent events and messages recorded for the object, newest first.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `object_id` | path | string | yes | Your identifier for the object, e.g. `user-42`. |

| Status | Description | Body |
|---|---|---|
| 200 | The object's events. | `EventList` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### POST /v1/objects/{object_id}/ingest

**Ingest events (alias)** (deprecated)

An alias of `POST /v1/objects/{object_id}/events`. Use that path instead.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `object_id` | path | string | yes | Your identifier for the object, e.g. `user-42`. |

Request body: `IngestRequest`

| Status | Description | Body |
|---|---|---|
| 200 | `wait: true`: the events were recorded and the object scored. | `ModerationVerdict` |
| 202 | The events were recorded and an analysis scheduled. | `IngestAccepted` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 402 | The tenant's prepaid balance is empty. | `Error` |

## Signals

A signal is a question you ask about every object, such as "is this a
scammer?" (`noul`), "how trustworthy is this user?" (`score`) or "what is
their buyer intent?" (`choice`).

### GET /v1/signals

**List signals**

Authentication: API key (signed JWT).

| Status | Description | Body |
|---|---|---|
| 200 | Every configured signal, enabled or not. | `SignalList` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### PUT /v1/signals/{key}

**Create or update a signal**

Creates the signal, or replaces its definition. `instructions` and
`criteria` are passed to the analyzer verbatim; the shape of
`criteria` depends on `type`:

| `type`   | `criteria`                                           |
|----------|------------------------------------------------------|
| `noul`   | object with optional `true` / `false` descriptions   |
| `score`  | ordered array of level descriptions, low to high     |
| `choice` | object mapping each option to a description or null |

Creating a signal does not analyze existing objects unless the tenant
setting `auto_backfill_signals` is on; start a backfill explicitly with
`POST /v1/signals/{key}/backfill`.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `key` | path | string | yes | The signal's key, e.g. `is_scammer`. |

Request body: `SignalInput`

```json
{
  "type": "noul",
  "instructions": "Decide if this user is likely a scammer.",
  "criteria": {
    "true": "clear scam signals",
    "false": "legitimate behaviour"
  }
}
```

| Status | Description | Body |
|---|---|---|
| 200 | The saved signal. | `Signal` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### DELETE /v1/signals/{key}

**Delete a signal**

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `key` | path | string | yes | The signal's key, e.g. `is_scammer`. |

| Status | Description | Body |
|---|---|---|
| 204 | Deleted. |  |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### GET /v1/signals/{key}/backfill

**Get backfill estimate and progress**

Returns how many existing objects have no answer for the signal and
what analyzing them would cost now, plus the progress of the most
recent backfill (`null` if none was started).

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `key` | path | string | yes | The signal's key, e.g. `is_scammer`. |

| Status | Description | Body |
|---|---|---|
| 200 | Estimate and progress. | `BackfillStatus` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### POST /v1/signals/{key}/backfill

**Start a backfill**

Re-analyzes, for this signal only, every existing object that has no
answer for it. Jobs run at bulk priority so they never delay live
ingest. Each analysis is billed. Returns `409` if the signal is
disabled.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `key` | path | string | yes | The signal's key, e.g. `is_scammer`. |

| Status | Description | Body |
|---|---|---|
| 202 | Scheduled. | `SignalBackfill` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |
| 409 | The request conflicts with the resource's current state. | `Error` |

## Webhooks

Webhook endpoints receive a signed `POST` for the event types they
subscribe to: `analysis.completed` each time an analysis completes, and
`rule.triggered` when a rule with a webhook action fires. Deliveries
retry with backoff and land in a dead-letter queue you can replay.

### GET /v1/webhooks

**List webhook endpoints**

Authentication: API key (signed JWT).

| Status | Description | Body |
|---|---|---|
| 200 | The endpoints, without their secrets. | `WebhookEndpointList` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### POST /v1/webhooks

**Register a webhook endpoint**

Registers an endpoint and returns its signing secret. **The secret is
shown only once**; store it to verify deliveries.

Authentication: API key (signed JWT).

Request body: `WebhookEndpointCreate`

| Status | Description | Body |
|---|---|---|
| 201 | Registered. | `WebhookEndpointCreated` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### GET /v1/webhooks/{endpoint_id}

**Get a webhook endpoint**

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `endpoint_id` | path | string (uuid) | yes |  |

| Status | Description | Body |
|---|---|---|
| 200 | The endpoint. | `WebhookEndpoint` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### PATCH /v1/webhooks/{endpoint_id}

**Update a webhook endpoint**

Omitted fields are left unchanged.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `endpoint_id` | path | string (uuid) | yes |  |

Request body: `WebhookEndpointUpdate`

| Status | Description | Body |
|---|---|---|
| 200 | The updated endpoint. | `WebhookEndpoint` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### DELETE /v1/webhooks/{endpoint_id}

**Delete a webhook endpoint**

Removes the endpoint and its deliveries.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `endpoint_id` | path | string (uuid) | yes |  |

| Status | Description | Body |
|---|---|---|
| 204 | Deleted. |  |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### GET /v1/webhook_deliveries

**List webhook deliveries**

Recent deliveries, newest first. Filter with `status=dead` to inspect the dead-letter queue.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `status` | query | `DeliveryStatus` | no |  |
| `endpoint_id` | query | string (uuid) | no |  |
| `limit` | query | integer | no |  |

| Status | Description | Body |
|---|---|---|
| 200 | The deliveries. | `WebhookDeliveryList` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### POST /v1/webhook_deliveries/{delivery_id}/replay

**Replay a delivery**

Re-queues a delivery (for example one in the dead-letter queue) for another attempt.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `delivery_id` | path | string (uuid) | yes |  |

| Status | Description | Body |
|---|---|---|
| 202 | Re-queued. | `WebhookDelivery` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

## Rules

A rule fires an action (email, Slack or webhook) when an object's signal
values cross a line you care about, such as `is_scammer >= 90`.

### GET /v1/rules

**List rules**

Authentication: API key (signed JWT).

| Status | Description | Body |
|---|---|---|
| 200 | The rules. | `RuleList` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### POST /v1/rules

**Create a rule**

The condition is validated against the tenant's signals, and a
`webhook` action must reference a registered endpoint subscribed to
`rule.triggered`.

Authentication: API key (signed JWT).

Request body: `RuleCreate`

```json
{
  "name": "scammer alert",
  "when": "is_scammer >= 90 and trust_score < 1",
  "action_type": "slack",
  "action_config": {
    "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX"
  },
  "cooldown_seconds": 3600
}
```

| Status | Description | Body |
|---|---|---|
| 201 | Created. | `Rule` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### GET /v1/rules/{rule_id}

**Get a rule**

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `rule_id` | path | string (uuid) | yes |  |

| Status | Description | Body |
|---|---|---|
| 200 | The rule. | `Rule` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### PATCH /v1/rules/{rule_id}

**Update a rule**

Omitted fields are left unchanged. The resulting condition and action are re-validated.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `rule_id` | path | string (uuid) | yes |  |

Request body: `RuleUpdate`

| Status | Description | Body |
|---|---|---|
| 200 | The updated rule. | `Rule` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### DELETE /v1/rules/{rule_id}

**Delete a rule**

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `rule_id` | path | string (uuid) | yes |  |

| Status | Description | Body |
|---|---|---|
| 204 | Deleted. |  |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### GET /v1/rule_firings

**List rule firings**

The audit log of rule firings and their action outcomes, newest first.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `rule_id` | query | string (uuid) | no |  |
| `object_id` | query | string | no |  |
| `limit` | query | integer | no |  |

| Status | Description | Body |
|---|---|---|
| 200 | The firings. | `RuleFiringList` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

## API keys

Create, rotate and revoke the keys your integrations sign requests with.

### GET /v1/api_keys

**List API keys**

Revoked keys are not listed. Secrets are never returned.

Authentication: API key (signed JWT).

| Status | Description | Body |
|---|---|---|
| 200 | The keys. | `APIKeyList` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### POST /v1/api_keys

**Create an API key**

Mints a key ID and signing secret. **The secret is shown only once.**

Authentication: API key (signed JWT).

Request body: `APIKeyCreate`

| Status | Description | Body |
|---|---|---|
| 201 | Created. | `APIKeySecret` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### PATCH /v1/api_keys/{key_id}

**Update an API key**

Rename a key, or deactivate and reactivate it. Use `DELETE` to revoke.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `key_id` | path | string (uuid) | yes | The key's `id` (a UUID), not its public `your_key_id` key ID. |

Request body: `APIKeyUpdate`

| Status | Description | Body |
|---|---|---|
| 200 | The updated key. | `APIKey` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### DELETE /v1/api_keys/{key_id}

**Revoke an API key**

The key stops authenticating immediately. Its ID is never reissued.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `key_id` | path | string (uuid) | yes | The key's `id` (a UUID), not its public `your_key_id` key ID. |

| Status | Description | Body |
|---|---|---|
| 204 | Revoked. |  |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

### POST /v1/api_keys/{key_id}/rotate

**Rotate an API key's secret**

Keeps the key ID and replaces the secret. The old secret stops working
immediately; the new one is returned only once.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `key_id` | path | string (uuid) | yes | The key's `id` (a UUID), not its public `your_key_id` key ID. |

| Status | Description | Body |
|---|---|---|
| 200 | Rotated. | `APIKeySecret` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 404 | The resource does not exist. | `Error` |

## Billing

Prepaid balance, the billing ledger, and card top-ups.

### GET /v1/billing

**Get the prepaid balance**

Authentication: API key (signed JWT).

| Status | Description | Body |
|---|---|---|
| 200 | The balance and billing configuration. | `Balance` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### GET /v1/billing/ledger

**List ledger entries**

One page of the billing ledger, newest first. Pass the previous page's
`next_cursor` as `cursor` to continue; it is empty on the last page.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `type` | query | "credit" \| "charge" | no | Only credits (funds added) or only charges (analyses). |
| `limit` | query | integer | no |  |
| `cursor` | query | string | no |  |

| Status | Description | Body |
|---|---|---|
| 200 | A page of entries. | `LedgerPage` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### POST /v1/billing/checkout

**Start a card top-up**

Creates a hosted Stripe Checkout Session for the amount and returns its
URL. The balance is credited only once Stripe confirms the payment.
Returns `503` when card payments are not configured.

Authentication: API key (signed JWT).

Request body: `CheckoutCreate`

| Status | Description | Body |
|---|---|---|
| 201 | The Checkout Session. | `CheckoutSession` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 502 | An upstream service (the analyzer, Stripe, an OAuth provider) failed. | `Error` |
| 503 | The feature is not configured on this deployment. | `Error` |

### GET /v1/billing/checkout/{session_id}

**Get a top-up's status**

Whether the Checkout Session's payment has been credited to the balance yet.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `session_id` | path | string | yes | The Checkout Session ID (`cs_…`). |

| Status | Description | Body |
|---|---|---|
| 200 | The top-up's status. | `CheckoutStatus` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

## Usage

Analysis volume, spend and token usage over a date range.

### GET /v1/usage

**Get usage**

Analyses, spend and tokens over an inclusive UTC date range (at most
366 days), with a zero-filled daily series and breakdowns by model,
source (`async` / `sync`) and signal. Defaults to the last 30 days.

Authentication: API key (signed JWT).

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `from` | query | string (date) | no | First day, `YYYY-MM-DD`. Defaults to 29 days before `to`. |
| `to` | query | string (date) | no | Last day, `YYYY-MM-DD`. Defaults to today. |

| Status | Description | Body |
|---|---|---|
| 200 | Usage for the range. | `Usage` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

## Account

The authenticated principal and tenant settings.

### GET /v1/me

**Get the current principal**

Returns the tenant the credential belongs to. For console tokens it
also returns the signed-in user and whether they finished onboarding.

Authentication: API key (signed JWT).

| Status | Description | Body |
|---|---|---|
| 200 | The authenticated principal. | `Me` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### GET /v1/settings

**Get tenant settings**

Authentication: API key (signed JWT).

| Status | Description | Body |
|---|---|---|
| 200 | The settings. | `Settings` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### PATCH /v1/settings

**Update tenant settings**

Omitted fields are left unchanged.

Authentication: API key (signed JWT).

Request body: `SettingsUpdate`

| Status | Description | Body |
|---|---|---|
| 200 | The updated settings. | `Settings` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

## Console

Endpoints used by the SigWise console for human sign-in and account
management. They authenticate with a console token, not an API key, and
are not part of the SDKs.

### POST /auth/console/register

**Sign up**

Self-service signup: provisions a new tenant and its first admin user,
then signs in. New tenants get the signup credit and the default
signals. Returns `403` when the deployment disables registration.

Authentication: none.

Request body: `RegisterRequest`

| Status | Description | Body |
|---|---|---|
| 201 | Account created and signed in. | `Token` |
| 400 | The request is malformed or fails validation. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |
| 409 | The request conflicts with the resource's current state. | `Error` |

### POST /auth/console/login

**Sign in**

Verifies an email and password and returns a console token.

For an account with [two-factor authentication](#tag/Console/operation/getMfa)
on, a request without `code` gets `401` with the code `mfa_required`;
repeat it with a code from the authenticator app, or an unused recovery
code. Repeated failures lock the account and the caller's address for 15
minutes: the API answers `429` with `Retry-After`.

Authentication: none.

Request body: `LoginRequest`

| Status | Description | Body |
|---|---|---|
| 200 | Signed in. | `Token` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 429 | Too many requests or attempts; retry after the `Retry-After` delay. | `Error` |

### POST /auth/token

**Exchange a legacy key for a token** (deprecated)

**Deprecated.** Exchanges a legacy API key and secret for a short-lived
tenant token. Integrations now sign each request themselves; this path
is kept for one release and logs a warning on every use.

Authentication: none.

Request body: `TokenExchangeRequest`

| Status | Description | Body |
|---|---|---|
| 200 | A tenant token. | `Token` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |

### POST /auth/console/password/forgot

**Request a password reset**

Emails a single-use reset link valid for one hour. It answers `202`
with the same body whether or not the email has an account, and is
throttled per client IP and per email.

Authentication: none.

Request body: `ForgotPasswordRequest`

| Status | Description | Body |
|---|---|---|
| 202 | Accepted. | `Message` |
| 400 | The request is malformed or fails validation. | `Error` |
| 429 | Too many requests or attempts; retry after the `Retry-After` delay. | `Error` |

### POST /auth/console/password/reset

**Reset a password**

Sets a new password with a reset token. Every existing session is signed out.

Authentication: none.

Request body: `ResetPasswordRequest`

| Status | Description | Body |
|---|---|---|
| 204 | Password reset. |  |
| 400 | The request is malformed or fails validation. | `Error` |

### GET /auth/console/oauth/providers

**List social sign-in providers**

Authentication: none.

| Status | Description | Body |
|---|---|---|
| 200 | The providers configured on this deployment. | `OAuthProviders` |

### POST /auth/console/oauth/{provider}/authorize

**Get a provider authorization URL**

Returns the provider's authorization URL for a `state` and a PKCE S256 `code_challenge`.

Authentication: none.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `provider` | path | "google" \| "github" | yes |  |

Request body: `OAuthAuthorizeRequest`

| Status | Description | Body |
|---|---|---|
| 200 | The URL to send the browser to. | `AuthorizationURL` |
| 400 | The request is malformed or fails validation. | `Error` |
| 404 | The resource does not exist. | `Error` |

### POST /auth/console/oauth/{provider}

**Sign in with a provider**

Completes a social sign-in: exchanges the authorization code and PKCE
verifier, then signs in the linked user, links a verified email to an
existing user, or signs up a new tenant (`201`, `new_user: true`).

Authentication: none.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `provider` | path | "google" \| "github" | yes |  |

Request body: `OAuthCodeRequest`

| Status | Description | Body |
|---|---|---|
| 200 | Signed in to an existing account. | `Token` |
| 201 | A new account was created. | `Token` |
| 400 | The request is malformed or fails validation. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |
| 404 | The resource does not exist. | `Error` |
| 409 | The request conflicts with the resource's current state. | `Error` |
| 502 | An upstream service (the analyzer, Stripe, an OAuth provider) failed. | `Error` |

### POST /v1/me/onboarded

**Mark onboarding complete**

Authentication: console token.

| Status | Description | Body |
|---|---|---|
| 204 | Marked onboarded. |  |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |
| 404 | The resource does not exist. | `Error` |

### POST /v1/me/password

**Change password**

Changes the signed-in user's password, signs out every other session, and returns a new console token.

Authentication: console token.

Request body: `ChangePasswordRequest`

| Status | Description | Body |
|---|---|---|
| 200 | Password changed. | `Token` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |
| 429 | Too many requests or attempts; retry after the `Retry-After` delay. | `Error` |

### GET /v1/me/mfa

**Get two-factor status**

Whether two-factor authentication is on for the signed-in user, and how many recovery codes are left.

Authentication: console token.

| Status | Description | Body |
|---|---|---|
| 200 | The two-factor status. | `MfaStatus` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |

### POST /v1/me/mfa/setup

**Start two-factor setup**

Issues a new secret to add to an authenticator app. Two-factor stays off
until `POST /v1/me/mfa/enable` confirms a code from it. Calling this
again before enabling replaces the secret. Returns `409` when
two-factor is already on.

Authentication: console token.

| Status | Description | Body |
|---|---|---|
| 200 | The secret, as text and as an `otpauth://` URI for a QR code. | `MfaSetup` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |
| 409 | The request conflicts with the resource's current state. | `Error` |
| 429 | Too many requests or attempts; retry after the `Retry-After` delay. | `Error` |

### POST /v1/me/mfa/enable

**Turn on two-factor**

Confirms the secret from setup with a current code and turns two-factor
on. Returns eight single-use recovery codes, **once**; each signs in
once if the authenticator is lost.

Authentication: console token.

Request body: `MfaEnableRequest`

| Status | Description | Body |
|---|---|---|
| 200 | Two-factor is on. | `MfaRecoveryCodes` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |
| 409 | The request conflicts with the resource's current state. | `Error` |
| 429 | Too many requests or attempts; retry after the `Retry-After` delay. | `Error` |

### POST /v1/me/mfa/disable

**Turn off two-factor**

Needs the account password (when it has one) and a current code or an
unused recovery code, so a stolen session alone can't remove it.

Authentication: console token.

Request body: `MfaDisableRequest`

| Status | Description | Body |
|---|---|---|
| 204 | Two-factor is off. |  |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |
| 409 | The request conflicts with the resource's current state. | `Error` |
| 429 | Too many requests or attempts; retry after the `Retry-After` delay. | `Error` |

### GET /v1/me/identities

**List sign-in methods**

Authentication: console token.

| Status | Description | Body |
|---|---|---|
| 200 | Password status and linked providers. | `Identities` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |

### POST /v1/me/identities/{provider}

**Link a provider account**

Authentication: console token.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `provider` | path | "google" \| "github" | yes |  |

Request body: `OAuthCodeRequest`

| Status | Description | Body |
|---|---|---|
| 201 | Linked. | `Identity` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |
| 404 | The resource does not exist. | `Error` |
| 409 | The request conflicts with the resource's current state. | `Error` |

### DELETE /v1/me/identities/{provider}

**Unlink a provider account**

Refused with `409` when it is the user's last sign-in method.

Authentication: console token.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `provider` | path | "google" \| "github" | yes |  |

| Status | Description | Body |
|---|---|---|
| 204 | Unlinked. |  |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |
| 404 | The resource does not exist. | `Error` |
| 409 | The request conflicts with the resource's current state. | `Error` |

### GET /v1/me/notifications

**Get email preferences**

Authentication: console token.

| Status | Description | Body |
|---|---|---|
| 200 | Every account email and whether the user receives it. | `NotificationPreferences` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |

### PUT /v1/me/notifications

**Update email preferences**

Maps an email event to whether the user wants it, e.g.
`{"low_balance": false}`. Events left out keep their setting. Critical
emails can't be turned off.

Authentication: console token.

Request body: `NotificationPreferencesUpdate`

| Status | Description | Body |
|---|---|---|
| 200 | The updated preferences. | `NotificationPreferences` |
| 400 | The request is malformed or fails validation. | `Error` |
| 401 | The credential is missing, invalid, expired or revoked. | `Error` |
| 403 | The credential is valid but cannot perform this action. | `Error` |

## System

Health checks and this specification.

### GET /healthz

**Liveness**

Returns `200` as long as the process is serving. It does not check dependencies.

Authentication: none.

| Status | Description | Body |
|---|---|---|
| 200 | The process is up. | `Health` |

### GET /metrics

**Prometheus metrics**

Operational metrics in the Prometheus text format: request rates and
latency by route, analysis outcomes, analyzer latency and throttling,
queue depth and lag, and running jobs per tenant. For the operator of
a deployment, not for integrations.

Requires `Authorization: Bearer <METRICS_TOKEN>` when a token is
configured, and is only served in production when one is. Returns
`404` when metrics are disabled.

Authentication: none.

| Status | Description | Body |
|---|---|---|
| 200 | The current metrics. |  |
| 401 | The metrics token is missing or wrong. |  |
| 404 | The resource does not exist. | `Error` |

### GET /readyz

**Readiness**

Returns `200` only when the database is reachable, so load balancers stop routing traffic during an outage.

Authentication: none.

| Status | Description | Body |
|---|---|---|
| 200 | Ready to serve traffic. | `Health` |
| 503 | The database is unreachable. | `Error` |

### GET /openapi.yaml

**OpenAPI specification (YAML)**

This document, as served by the API you are talking to.

Authentication: none.

| Status | Description | Body |
|---|---|---|
| 200 | The OpenAPI 3.1 document. | string |

### GET /openapi.json

**OpenAPI specification (JSON)**

This document as JSON.

Authentication: none.

| Status | Description | Body |
|---|---|---|
| 200 | The OpenAPI 3.1 document. | object |

### POST /webhooks/stripe

**Stripe events**

Receives Stripe events, verified against the `Stripe-Signature` header.
A paid top-up Checkout Session is credited to the tenant exactly once.
Called by Stripe, not by integrations.

Authentication: none.

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `Stripe-Signature` | header | string | yes |  |

Request body: object

| Status | Description | Body |
|---|---|---|
| 200 | Received. | `StripeWebhookAck` |
| 400 | The request is malformed or fails validation. | `Error` |
| 503 | The feature is not configured on this deployment. | `Error` |

## Webhooks the API sends

### analysis.completed

Sent to every enabled endpoint subscribed to `analysis.completed`
(whose `signal_keys` filter matches) each time an asynchronous analysis
finishes. Verify
`X-Webhook-Signature` before trusting the body.

Body: `AnalysisCompletedEvent`

### rule.triggered

Sent to the endpoint a `webhook` rule action references when the rule
fires. The endpoint must be enabled and subscribed to `rule.triggered`;
otherwise the firing is recorded with status `error`.

Body: `RuleTriggeredEvent`

## Schemas

### Error

| Field | Type | Required | Description |
|---|---|---|---|
| `error` | `ErrorBody` | yes |  |

### ErrorBody

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | string | yes | A stable, machine-readable code: `bad_request`, `unauthorized`, `forbidden`, `not_found`, `conflict`, `payment_required`, `rate_limited`, `payments_not_configured`, `payment_provider_error`, `internal_error`, `not_ready`, or a console-specific code such as `weak_password`. |
| `message` | string | yes | A human-readable explanation. Do not match on it; it may change. |

### Health

| Field | Type | Required | Description |
|---|---|---|---|
| `status` | string | yes |  |

### Message

| Field | Type | Required | Description |
|---|---|---|---|
| `message` | string | yes |  |

### StripeWebhookAck

| Field | Type | Required | Description |
|---|---|---|---|
| `received` | boolean | yes |  |

### Metadata

Arbitrary structured attributes.

Type: object

### EventType

`event` is a discrete action (e.g. `profile.updated`); `message` is free-form text.

Type: "event" | "message"

### EventInput

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | `EventType` | yes |  |
| `name` | string | no | The event name, or a message's subject. |
| `content` | string | no | Free text, such as a message body. |
| `metadata` | `Metadata` | no |  |
| `occurred_at` | string (date-time) | no | When it happened. Defaults to the time the API received it. |

### IngestRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `object_type` | string | no | Optional classification, e.g. `user` or `listing`. |
| `events` | `EventInput`[] | yes |  |
| `wait` | boolean | no | Score the object inline and return the verdict (`200`) instead of scheduling analysis (`202`). |
| `signals` | string[] | no | With `wait`, score only these signal keys. Ignored otherwise. |
| `include_history` | boolean | no | With `wait`, whether to score over the object's prior history (`true`) or only this request's events (`false`). |

### IngestAccepted

| Field | Type | Required | Description |
|---|---|---|---|
| `object_id` | string | yes |  |
| `accepted` | integer | yes | How many events were recorded. |
| `analysis_status` | "scheduled" | yes |  |
| `analysis_delay_ms` | integer | yes | The debounce window before the analysis runs. |

### Answer

One signal's answer. Only the fields for its `type` are set.

| Field | Type | Required | Description |
|---|---|---|---|
| `signal` | string | yes |  |
| `type` | `SignalType` | yes |  |
| `noul` | number | no | `noul`: the probability the answer is yes. |
| `choice` | string | no | `choice`: the most likely option. |
| `score` | number | no | `score`: the position on the spectrum, from 0 (the first level) to the number of levels minus one. |
| `probabilities` | map of number | no | `choice` and `score`: the probability of each option or level. |
| `confidence` | number | no | `choice` and `score`: the analyzer's confidence. |

### ModerationVerdict

| Field | Type | Required | Description |
|---|---|---|---|
| `object_id` | string | yes |  |
| `accepted` | integer | yes | How many events were recorded. |
| `analyzed` | boolean | yes | False when no configured signal matched, so nothing was scored. |
| `history_included` | boolean | yes |  |
| `model` | string | yes | The model that produced the answers, e.g. `model-1`. |
| `latency_ms` | integer | yes |  |
| `answers` | `Answer`[] | yes |  |
| `reason` | string | no | Why nothing was analyzed, when `analyzed` is false. |

### Event

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | `EventType` | yes |  |
| `name` | string | no |  |
| `content` | string | no |  |
| `metadata` | `Metadata` | no |  |
| `occurred_at` | string (date-time) | yes |  |

### EventList

| Field | Type | Required | Description |
|---|---|---|---|
| `events` | `Event`[] | yes |  |

### SignalResult

The latest answer for one signal. Only the fields for its `type` are set.

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | string | yes |  |
| `type` | `SignalType` | yes |  |
| `noul` | number | no |  |
| `choice` | string | no |  |
| `score` | number | no |  |
| `probabilities` | map of number | no |  |
| `confidence` | number | no |  |
| `model` | string | no |  |
| `computed_at` | string (date-time) | yes |  |

### ObjectAnalysis

| Field | Type | Required | Description |
|---|---|---|---|
| `object_id` | string | yes |  |
| `event_count` | integer | yes |  |
| `analysis` | `SignalResult`[] | yes |  |
| `pending` | string[] | yes | Enabled signals with no answer yet. |

### ObjectSummary

| Field | Type | Required | Description |
|---|---|---|---|
| `object_id` | string | yes |  |
| `object_type` | string | yes |  |
| `display_name` | string | no |  |
| `event_count` | integer | yes |  |
| `last_seen` | string (date-time) | yes |  |
| `analysis` | `SignalResult`[] | yes |  |
| `pending` | string[] | yes |  |

### ObjectList

| Field | Type | Required | Description |
|---|---|---|---|
| `objects` | `ObjectSummary`[] | yes |  |
| `limit` | integer | yes |  |
| `offset` | integer | yes |  |
| `next_cursor` | string | no | Pass as `cursor` to fetch the next page. Absent on the last page and for signal queries. |

### CompactedState

A rolling summary of an object's older events.

| Field | Type | Required | Description |
|---|---|---|---|
| `total_events` | integer | yes |  |
| `messages` | integer | yes |  |
| `actions` | integer | yes |  |
| `by_action` | map of integer | no |  |
| `sample_messages` | string[] | no |  |
| `first_seen` | string (date-time) | yes |  |
| `updated_at` | string (date-time) | yes |  |

### ObjectState

| Field | Type | Required | Description |
|---|---|---|---|
| `object_id` | string | yes |  |
| `compacted_events` | integer | yes | How many events have been folded into the summary. |
| `compacted_through` | string (date-time) \| null | no |  |
| `compacted_state` | `CompactedState` \| null | no |  |
| `last_analyzed_at` | string (date-time) \| null | no |  |

### AnalyzeScheduled

| Field | Type | Required | Description |
|---|---|---|---|
| `object_id` | string | yes |  |
| `analysis_status` | "scheduled" | yes |  |

### AnalyzeAllScheduled

| Field | Type | Required | Description |
|---|---|---|---|
| `scheduled` | integer | yes | How many objects were scheduled. |
| `analysis_status` | "scheduled" | yes |  |

### Overview

| Field | Type | Required | Description |
|---|---|---|---|
| `objects` | integer | yes |  |
| `signals` | integer | yes |  |
| `analyzed_pct` | number | yes | Share of objects with at least one answer, 0–1. |
| `events_today` | integer | yes | Events recorded in the last 24 hours. |
| `flagged` | integer | yes |  |
| `signal_summary` | `SignalSummary`[] | yes |  |
| `categories` | `CategoryCount`[] | yes |  |
| `top_risk` | `RiskyObject`[] | yes |  |

### CategoryCount

How many objects have one `object_type`.

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes |  |
| `count` | integer | yes |  |

### RiskyObject

One of the objects with the highest yes/no probabilities.

| Field | Type | Required | Description |
|---|---|---|---|
| `object_id` | string | yes |  |
| `display_name` | string | no |  |
| `risk` | number | yes |  |

### SignalSummary

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | string | yes |  |
| `type` | `SignalType` | yes |  |
| `total` | integer | yes | Objects with an answer for the signal. |
| `avg` | number | no |  |
| `high` | integer | no | Objects answered high (yes/no signals). |
| `high_pct` | number | no |  |
| `choices` | map of number | no | `choice` signals: the share of each option. |

### SignalType

`noul`: yes/no probability. `score`: position on a labelled spectrum. `choice`: one of N options.

Type: "noul" | "choice" | "score"

### SignalInput

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | `SignalType` | yes |  |
| `instructions` | string \| object \| any[] | yes | What the analyzer should decide. Usually a string; objects and arrays are passed through verbatim. |
| `criteria` | object \| any[] | no | Depends on `type`; see the operation description. |
| `enabled` | boolean | no |  |

### Signal

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | string | yes |  |
| `type` | `SignalType` | yes |  |
| `instructions` | string \| object \| any[] | yes |  |
| `criteria` | object \| any[] | no |  |
| `enabled` | boolean | yes |  |
| `created_at` | string (date-time) | yes |  |
| `updated_at` | string (date-time) | yes |  |
| `backfill` | `SignalBackfill` | no | Set only when creating the signal started a backfill (`auto_backfill_signals`). |

### SignalList

| Field | Type | Required | Description |
|---|---|---|---|
| `signals` | `Signal`[] | yes |  |

### SignalBackfill

| Field | Type | Required | Description |
|---|---|---|---|
| `signal_key` | string | yes |  |
| `status` | "running" \| "done" | yes |  |
| `total` | integer | yes | Objects enqueued when the backfill started. |
| `completed` | integer | yes | Objects answered since it started. |
| `pending` | integer | yes | Objects still queued or running. |
| `started_at` | string (date-time) | yes |  |

### BackfillEstimate

| Field | Type | Required | Description |
|---|---|---|---|
| `objects` | integer | yes | Objects with no answer for the signal. |
| `estimated_cost_micros` | integer | yes | Estimated cost in micro-dollars (1e-6 USD), an upper bound. 0 without paid history. |

### BackfillStatus

| Field | Type | Required | Description |
|---|---|---|---|
| `estimate` | `BackfillEstimate` | yes |  |
| `backfill` | `SignalBackfill` \| null | yes |  |

### Settings

| Field | Type | Required | Description |
|---|---|---|---|
| `auto_backfill_signals` | boolean | yes | Creating a signal automatically backfills existing objects for it. Off by default because each analysis is billed. |

### SettingsUpdate

| Field | Type | Required | Description |
|---|---|---|---|
| `auto_backfill_signals` | boolean | no |  |

### WebhookEventType

Type: "analysis.completed" | "rule.triggered"

### WebhookEndpoint

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string (uuid) | yes |  |
| `url` | string (uri) | yes |  |
| `description` | string | yes |  |
| `enabled` | boolean | yes |  |
| `signal_keys` | string[] | yes | Only deliver analyses that answered one of these signals. Empty means every analysis. |
| `events` | `WebhookEventType`[] | yes | The event types this endpoint receives. |
| `created_at` | string (date-time) | yes |  |
| `updated_at` | string (date-time) | yes |  |

### WebhookEndpointCreate

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string (uri) | yes | An `http` or `https` URL. |
| `description` | string | no |  |
| `signal_keys` | string[] | no |  |
| `events` | `WebhookEventType`[] | no | The event types to deliver. Omit to receive every event type. Use `["rule.triggered"]` for an endpoint that only receives rule firings. |
| `enabled` | boolean | no |  |

### WebhookEndpointUpdate

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string (uri) | no |  |
| `description` | string | no |  |
| `signal_keys` | string[] | no |  |
| `events` | `WebhookEventType`[] | no |  |
| `enabled` | boolean | no |  |

### WebhookEndpointCreated

| Field | Type | Required | Description |
|---|---|---|---|
| `endpoint` | `WebhookEndpoint` | yes |  |
| `secret` | string | yes | The signing secret. Shown only once. |
| `note` | string | yes |  |

### WebhookEndpointList

| Field | Type | Required | Description |
|---|---|---|---|
| `webhooks` | `WebhookEndpoint`[] | yes |  |

### DeliveryStatus

`dead` deliveries are in the dead-letter queue: permanently rejected, or out of retries.

Type: "pending" | "delivering" | "delivered" | "dead"

### WebhookDelivery

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string (uuid) | yes |  |
| `endpoint_id` | string (uuid) | yes |  |
| `event_type` | "analysis.completed" \| "rule.triggered" | yes |  |
| `object_id` | string | no |  |
| `status` | `DeliveryStatus` | yes |  |
| `attempts` | integer | yes |  |
| `max_attempts` | integer | yes |  |
| `last_status_code` | integer | no | The endpoint's last HTTP status, if it answered. |
| `last_error` | string | no |  |
| `run_after` | string (date-time) | yes | When the next attempt is due. |
| `delivered_at` | string (date-time) | no |  |
| `created_at` | string (date-time) | yes |  |
| `payload` | object | no | The body that is (or was) delivered. |

### WebhookDeliveryList

| Field | Type | Required | Description |
|---|---|---|---|
| `deliveries` | `WebhookDelivery`[] | yes |  |

### AnalysisCompletedEvent

| Field | Type | Required | Description |
|---|---|---|---|
| `event` | "analysis.completed" | yes |  |
| `object_id` | string | yes |  |
| `tenant_id` | string (uuid) | yes |  |
| `model` | string | yes |  |
| `occurred_at` | string (date-time) | yes |  |
| `answers` | `Answer`[] | yes |  |

### RuleTriggeredEvent

| Field | Type | Required | Description |
|---|---|---|---|
| `event` | "rule.triggered" | yes |  |
| `rule_id` | string (uuid) | yes |  |
| `rule_name` | string | no |  |
| `tenant_id` | string (uuid) | yes |  |
| `object_id` | string | yes |  |
| `matched_when` | string | yes |  |
| `model` | string | no |  |
| `occurred_at` | string (date-time) | yes |  |
| `values` | `Answer`[] | yes |  |

### ActionType

Type: "email" | "slack" | "webhook"

### ActionConfig

Depends on `action_type`:

- `email`: `{"to": "abuse@acme.com", "subject": "…"}` (`subject` optional)
- `slack`: `{"webhook_url": "https://hooks.slack.com/…"}`
- `webhook`: `{"endpoint_id": "<registered endpoint id>"}`

Type: object

### Rule

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string (uuid) | yes |  |
| `name` | string | yes |  |
| `when` | string | yes | The condition, e.g. `is_scammer >= 90 and trust_score < 1`. |
| `action_type` | `ActionType` | yes |  |
| `action_config` | `ActionConfig` | yes |  |
| `enabled` | boolean | yes |  |
| `cooldown_seconds` | integer | yes | 0 fires only when the condition turns true; more lets the rule re-fire after that long while it stays true. |
| `created_at` | string (date-time) | yes |  |
| `updated_at` | string (date-time) | yes |  |

### RuleCreate

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |
| `when` | string | yes |  |
| `action_type` | `ActionType` | yes |  |
| `action_config` | `ActionConfig` | yes |  |
| `enabled` | boolean | no |  |
| `cooldown_seconds` | integer | no |  |

### RuleUpdate

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |
| `when` | string | no |  |
| `action_type` | `ActionType` | no |  |
| `action_config` | `ActionConfig` | no |  |
| `enabled` | boolean | no |  |
| `cooldown_seconds` | integer | no |  |

### RuleList

| Field | Type | Required | Description |
|---|---|---|---|
| `rules` | `Rule`[] | yes |  |

### RuleFiring

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string (uuid) | yes |  |
| `rule_id` | string (uuid) | yes |  |
| `object_id` | string | yes |  |
| `matched_when` | string | yes |  |
| `signal_values` | object | no | The signal values that satisfied the condition. |
| `action_type` | `ActionType` | yes |  |
| `status` | "dispatched" \| "error" | yes |  |
| `detail` | string | no | The error, or a short success note. |
| `fired_at` | string (date-time) | yes |  |

### RuleFiringList

| Field | Type | Required | Description |
|---|---|---|---|
| `firings` | `RuleFiring`[] | yes |  |

### APIKeyStatus

Type: "active" | "inactive" | "revoked"

### APIKey

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string (uuid) | yes |  |
| `key` | string | yes | The public key ID, sent as `api_key`. |
| `name` | string | yes |  |
| `status` | `APIKeyStatus` | yes |  |
| `has_secret` | boolean | yes | Whether the key can sign requests. Legacy keys can't until rotated. |
| `legacy` | boolean | no | Created before request signing; rotate it to get a signing secret. |
| `legacy_preview` | string | no |  |
| `created_at` | string (date-time) | yes |  |
| `last_used_at` | string (date-time) | no |  |
| `legacy_used_at` | string (date-time) | no |  |

### APIKeyCreate

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |

### APIKeyUpdate

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |
| `status` | "active" \| "inactive" | no |  |

### APIKeySecret

| Field | Type | Required | Description |
|---|---|---|---|
| `api_key` | `APIKey` | yes |  |
| `key` | string | yes | The public key ID. |
| `secret` | string | yes | The signing secret. Shown only once. |
| `note` | string | yes |  |

### APIKeyList

| Field | Type | Required | Description |
|---|---|---|---|
| `api_keys` | `APIKey`[] | yes |  |

### Balance

| Field | Type | Required | Description |
|---|---|---|---|
| `balance_cents` | integer | yes |  |
| `currency` | string | yes |  |
| `metering_enabled` | boolean | yes | Whether analyses are billed on this deployment. |
| `enforcement` | string | yes | `block` pauses analyses at an empty balance; `grace` lets it go negative. |
| `self_serve_topup` | boolean | yes | Whether card top-ups are available. |
| `low_balance_threshold_cents` | integer | yes |  |
| `topup_min_cents` | integer | no |  |
| `topup_max_cents` | integer | no |  |

### LedgerEntry

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string (uuid) | yes |  |
| `delta_cents` | integer | yes | Negative for a charge, positive for a credit. Sub-cent charges round to 0; see `delta_micros`. |
| `balance_after_cents` | integer | yes |  |
| `delta_micros` | integer | yes | The exact amount in micro-dollars (1e-6 USD). |
| `balance_after_micros` | integer | yes |  |
| `reason` | string | yes |  |
| `model` | string | no |  |
| `object_id` | string | no |  |
| `cost_usd` | number | no |  |
| `cost_margin` | number | no |  |
| `input_tokens` | integer | no |  |
| `output_tokens` | integer | no |  |
| `payment_ref` | string | no | The Stripe Checkout Session that paid for a top-up. |
| `created_at` | string (date-time) | yes |  |

### LedgerPage

| Field | Type | Required | Description |
|---|---|---|---|
| `entries` | `LedgerEntry`[] | yes |  |
| `next_cursor` | string | yes | Pass as `cursor` for the next page. Empty on the last page. |

### CheckoutCreate

| Field | Type | Required | Description |
|---|---|---|---|
| `amount_cents` | integer | yes | Within the deployment's `topup_min_cents`–`topup_max_cents`. |

### CheckoutSession

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `url` | string (uri) | yes | Send the customer here to pay. |

### CheckoutStatus

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes |  |
| `status` | "pending" \| "credited" | yes |  |
| `amount_cents` | integer | no |  |
| `balance_after_cents` | integer | no |  |

### Usage

| Field | Type | Required | Description |
|---|---|---|---|
| `from` | string (date) | yes |  |
| `to` | string (date) | yes |  |
| `currency` | string | yes |  |
| `metering_enabled` | boolean | yes |  |
| `balance_cents` | integer | yes |  |
| `burn_rate_micros_per_day` | integer | yes | Average spend per elapsed day of the range, in micro-dollars. |
| `runway_days` | number | no | Balance divided by burn rate. Omitted without spend or balance. |
| `totals` | `UsageTotals` | yes |  |
| `series` | `UsageDay`[] | yes |  |
| `by_model` | `UsageBreakdown`[] | yes |  |
| `by_source` | `UsageBreakdown`[] | yes |  |
| `by_signal` | `UsageSignal`[] | yes |  |

### UsageTotals

| Field | Type | Required | Description |
|---|---|---|---|
| `analyses` | integer | yes |  |
| `spend_micros` | integer | yes |  |
| `input_tokens` | integer | yes |  |
| `output_tokens` | integer | yes |  |

### UsageDay

| Field | Type | Required | Description |
|---|---|---|---|
| `date` | string (date) | yes |  |
| `analyses` | integer | yes |  |
| `async` | integer | yes |  |
| `sync` | integer | yes |  |
| `spend_micros` | integer | yes |  |

### UsageBreakdown

| Field | Type | Required | Description |
|---|---|---|---|
| `key` | string | yes |  |
| `analyses` | integer | yes |  |
| `spend_micros` | integer | yes |  |
| `input_tokens` | integer | yes |  |
| `output_tokens` | integer | yes |  |

### UsageSignal

| Field | Type | Required | Description |
|---|---|---|---|
| `signal` | string | yes |  |
| `analyses` | integer | yes |  |

### Me

| Field | Type | Required | Description |
|---|---|---|---|
| `auth_type` | "tenant" \| "console" | yes | `tenant` for API keys, `console` for a signed-in user. |
| `tenant_id` | string (uuid) | yes |  |
| `tenant_name` | string | yes |  |
| `user_id` | string (uuid) | no |  |
| `email` | string (email) | no |  |
| `role` | string | no |  |
| `onboarded` | boolean | no |  |

### Token

| Field | Type | Required | Description |
|---|---|---|---|
| `token` | string | yes |  |
| `expires_at` | string (date-time) | yes |  |
| `token_type` | "console" \| "tenant" | yes |  |
| `new_user` | boolean | no | Social sign-in only; true when it created the account. |

### LoginRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string (email) | yes |  |
| `password` | string (password) | yes |  |
| `code` | string | no | An authenticator code or a recovery code. Required only when two-factor authentication is on. |

### RegisterRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `tenant_name` | string | yes |  |
| `email` | string (email) | yes |  |
| `password` | string (password) | yes |  |

### TokenExchangeRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `api_key` | string | yes |  |
| `secret` | string | yes |  |

### ForgotPasswordRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string (email) | yes |  |

### ResetPasswordRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `token` | string | yes |  |
| `password` | string (password) | yes |  |

### MfaStatus

| Field | Type | Required | Description |
|---|---|---|---|
| `enabled` | boolean | yes |  |
| `recovery_codes_remaining` | integer | yes | Unused recovery codes; 0 while two-factor is off. |

### MfaSetup

| Field | Type | Required | Description |
|---|---|---|---|
| `secret` | string | yes | The shared secret in base32, for typing into an authenticator. |
| `otpauth_uri` | string | yes | The same secret as an `otpauth://` URI, to render as a QR code. |

### MfaEnableRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | string | yes | A current code from the authenticator. |

### MfaRecoveryCodes

| Field | Type | Required | Description |
|---|---|---|---|
| `recovery_codes` | string[] | yes | Shown once. Store them somewhere safe. |

### MfaDisableRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `password` | string (password) | no | Required unless the account has no password (social sign-in only). |
| `code` | string | yes | A current authenticator code or an unused recovery code. |

### ChangePasswordRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `current_password` | string (password) | yes |  |
| `new_password` | string (password) | yes |  |

### OAuthProviders

| Field | Type | Required | Description |
|---|---|---|---|
| `providers` | "google" \| "github"[] | yes |  |
| `allow_registration` | boolean | yes |  |

### OAuthAuthorizeRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `state` | string | yes |  |
| `code_challenge` | string | yes | Base64url SHA-256 of the PKCE verifier (S256). |

### AuthorizationURL

| Field | Type | Required | Description |
|---|---|---|---|
| `url` | string (uri) | yes |  |

### OAuthCodeRequest

| Field | Type | Required | Description |
|---|---|---|---|
| `code` | string | yes |  |
| `code_verifier` | string | yes |  |
| `tenant_name` | string | no | Used only when the sign-in creates a new account. |

### Identity

| Field | Type | Required | Description |
|---|---|---|---|
| `provider` | "google" \| "github" | yes |  |
| `email` | string (email) | yes |  |
| `created_at` | string (date-time) | yes |  |

### Identities

| Field | Type | Required | Description |
|---|---|---|---|
| `has_password` | boolean | yes |  |
| `identities` | `Identity`[] | yes |  |
| `providers` | "google" \| "github"[] | yes |  |

### NotificationPreference

| Field | Type | Required | Description |
|---|---|---|---|
| `event` | string | yes |  |
| `label` | string | yes |  |
| `description` | string | yes |  |
| `enabled` | boolean | yes |  |
| `critical` | boolean | yes | Always sent; can't be turned off. |

### NotificationPreferences

| Field | Type | Required | Description |
|---|---|---|---|
| `preferences` | `NotificationPreference`[] | yes |  |

### NotificationPreferencesUpdate

Type: map of boolean
