# SigWise (full) > SigWise reads the events and messages on your platform and answers the questions you care about as typed signals. Guides, API reference and SDKs. --- Source: https://sigwise.ai/docs/guide/introduction.md # Introduction 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**. ``` POST events / messages GET answers your app ───────────────────────► SigWise ◄────────────────── your app │ ▲ schedule │ │ store answers ▼ │ analysis queue ──► analyzer (LLM) │ └──► webhooks, rules (Slack, email, webhook) ``` 1. **Configure signals.** Say what you want to know: *is this a scammer?*, *how trustworthy is this user?*, *what is their buyer intent?* A signal is an API call away, and every new account starts with those three. 2. **Send events.** Post what happens on your platform: profile edits, logins, messages between users. Ingestion returns `202` right away. 3. **Read the answers.** Analysis runs in the background, debounced so a burst of events produces one analysis. Read the latest answers, search objects by them, or have them pushed to you. ## What you get back Each signal is one of three types, and its answer is typed accordingly: | Type | Answer | Example | |----------|---------------------------------------------|----------------------| | `noul` | a probability between 0 and 1 | *is this a scammer?* | | `score` | a position on a labelled spectrum | *trust score* | | `choice` | one option out of N, with probabilities | *buyer intent* | ```json { "object_id": "user-42", "event_count": 7, "analysis": [ { "key": "is_scammer", "type": "noul", "noul": 0.91, "model": "model-1", "computed_at": "2026-09-27T10:00:05Z" }, { "key": "trust_score", "type": "score", "score": 0.4, "confidence": 0.8, "model": "model-1", "computed_at": "2026-09-27T10:00:05Z" } ], "pending": [] } ``` ## Ways to use it - **Background scoring.** Send events as they happen and read answers when you need them, such as on a moderation dashboard or before a payout. - **Direct moderation.** Send `"wait": true` to score a message inline and block it before other users see it. See [Direct moderation](https://sigwise.ai/docs/guides/moderation.md). - **Automation.** Get a signed [webhook](https://sigwise.ai/docs/guides/webhooks.md) for every completed analysis, or define [rules](https://sigwise.ai/docs/guides/rules.md) that alert Slack or email when, say, `is_scammer >= 90`. ## Next steps - [Quickstart](https://sigwise.ai/docs/guide/quickstart.md): your first signal, event and answer in five minutes. - [Authentication](https://sigwise.ai/docs/guide/authentication.md): how requests are signed. - [API reference](https://sigwise.ai/docs/reference.md): every endpoint and schema. --- Source: https://sigwise.ai/docs/guide/quickstart.md # Quickstart From an API key to your first answer in five minutes. ## 1. Get an API key Sign up in the [console](https://sigwise.ai/register), open **API keys** and create a key. You get two values: - a **key ID** like `7Hx2Qp9LmZ`, which identifies the key; - a **signing secret** like `your_secret`, **shown only once**. Keep it on your server. ```bash export ANALYZE_API_KEY=your_key_id export ANALYZE_SECRET=your_secret ``` Every account starts with free credit and three signals (`is_scammer`, `trust_score` and `buyer_intent`), so you can send events right away. ## 2. Set up a client Requests are authenticated with a short-lived token signed with your secret (see [Authentication](https://sigwise.ai/docs/guide/authentication.md)). The SDKs sign each request for you. With plain HTTP, a small shell function does it: cURL: ```bash # Signs a JWT with $ANALYZE_SECRET that is valid for five minutes. b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; } sign() { local now=$(date +%s) h p h=$(printf '{"alg":"HS256","typ":"JWT"}' | b64url) p=$(printf '{"iat":%d,"exp":%d}' "$now" "$((now + 300))" | b64url) printf '%s.%s.%s' "$h" "$p" \ "$(printf '%s.%s' "$h" "$p" | openssl dgst -sha256 -hmac "$ANALYZE_SECRET" -binary | b64url)" } API=https://api.sigwise.ai ``` Node.js: ```ts import { SigWise } from "@sigwise/sdk"; // Reads ANALYZE_API_KEY and ANALYZE_SECRET. const sigwise = new SigWise(); ``` Python: ```python from sigwise import SigWise # Reads ANALYZE_API_KEY and ANALYZE_SECRET. sigwise = SigWise() ``` Go: ```go import sigwise "github.com/sigwise/sigwise-go" // Empty values fall back to ANALYZE_API_KEY and ANALYZE_SECRET. client := sigwise.New("", "") ``` PHP: ```php // Null arguments fall back to ANALYZE_API_KEY and ANALYZE_SECRET. $sigwise = new SigWise\Client(); ``` > **SDKs** > > The SDKs are coming soon to npm, PyPI, the Go module proxy and Packagist. Until > then, use the cURL tab: the same requests work from any HTTP client. ## 3. Send events Send what happens to an object. Here, a message from `user-42`: cURL: ```bash curl -X POST "$API/v1/objects/user-42/events?api_key=$ANALYZE_API_KEY" \ -H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \ -d '{"object_type":"user","events":[ {"type":"event","name":"profile.updated","metadata":{"field":"bio"}}, {"type":"message","content":"is this still available? can I pay by wire?"} ]}' ``` Node.js: ```ts await sigwise.events.ingest("user-42", { object_type: "user", events: [ { type: "event", name: "profile.updated", metadata: { field: "bio" } }, { type: "message", content: "is this still available? can I pay by wire?" }, ], }); ``` Python: ```python sigwise.events.ingest( "user-42", object_type="user", events=[ {"type": "event", "name": "profile.updated", "metadata": {"field": "bio"}}, {"type": "message", "content": "is this still available? can I pay by wire?"}, ], ) ``` Go: ```go _, err := client.Events.Ingest(ctx, "user-42", &sigwise.IngestRequest{ ObjectType: sigwise.String("user"), Events: []sigwise.EventInput{ {Type: sigwise.EventTypeEvent, Name: sigwise.String("profile.updated"), Metadata: sigwise.Metadata{"field": "bio"}}, {Type: sigwise.EventTypeMessage, Content: sigwise.String("is this still available? can I pay by wire?")}, }, }) ``` PHP: ```php $sigwise->events->ingest('user-42', [ 'object_type' => 'user', 'events' => [ ['type' => 'event', 'name' => 'profile.updated', 'metadata' => ['field' => 'bio']], ['type' => 'message', 'content' => 'is this still available? can I pay by wire?'], ], ]); ``` The API answers `202 Accepted` and schedules an analysis a few seconds later: ```json { "object_id": "user-42", "accepted": 2, "analysis_status": "scheduled", "analysis_delay_ms": 5000 } ``` ## 4. Read the answers cURL: ```bash curl "$API/v1/objects/user-42?api_key=$ANALYZE_API_KEY" -H "Authorization: Bearer $(sign)" ``` Node.js: ```ts const object = await sigwise.objects.get("user-42"); console.log(object.analysis, object.pending); ``` Python: ```python obj = sigwise.objects.get("user-42") print(obj["analysis"], obj["pending"]) ``` Go: ```go obj, err := client.Objects.Get(ctx, "user-42") ``` PHP: ```php $object = $sigwise->objects->get('user-42'); ``` `analysis` holds the latest answer for each signal; `pending` lists the signals that have no answer yet. Right after ingesting, everything is pending until the analysis runs. ## 5. Ask your own question Add a signal with `PUT /v1/signals/{key}`. It is used from the next analysis on: cURL: ```bash curl -X PUT "$API/v1/signals/wants_refund?api_key=$ANALYZE_API_KEY" \ -H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \ -d '{"type":"noul","instructions":"Is this customer asking for a refund?", "criteria":{"true":"asks for money back","false":"anything else"}}' ``` Node.js: ```ts await sigwise.signals.upsert("wants_refund", { type: "noul", instructions: "Is this customer asking for a refund?", criteria: { true: "asks for money back", false: "anything else" }, }); ``` Python: ```python sigwise.signals.upsert( "wants_refund", type="noul", instructions="Is this customer asking for a refund?", criteria={"true": "asks for money back", "false": "anything else"}, ) ``` Go: ```go _, err := client.Signals.Upsert(ctx, "wants_refund", &sigwise.SignalInput{ Type: sigwise.SignalTypeNoul, Instructions: "Is this customer asking for a refund?", Criteria: map[string]string{"true": "asks for money back", "false": "anything else"}, }) ``` PHP: ```php $sigwise->signals->upsert('wants_refund', [ 'type' => 'noul', 'instructions' => 'Is this customer asking for a refund?', 'criteria' => ['true' => 'asks for money back', 'false' => 'anything else'], ]); ``` Existing objects are not re-analyzed automatically, since each analysis is billed. To answer the new signal for them, start a [backfill](https://sigwise.ai/docs/guides/backfills.md). ## Next - [Signals](https://sigwise.ai/docs/guide/signals.md): the three types and how to write criteria. - [Direct moderation](https://sigwise.ai/docs/guides/moderation.md): score content before publishing it. - [Webhooks](https://sigwise.ai/docs/guides/webhooks.md): stop polling and get answers pushed to you. --- Source: https://sigwise.ai/docs/guide/authentication.md # Authentication Every `/v1` request is signed. You never send your secret: you send a short-lived token signed with it. ## API keys An API key is a pair, created in the console under **API keys** or with [`POST /v1/api_keys`](https://sigwise.ai/docs/reference.md#tag/api-keys): | Part | Looks like | Where it goes | |------|------------|---------------| | Key ID | `7Hx2Qp9LmZ` | The `api_key` query parameter of every request. Public: it identifies the key and grants nothing on its own. | | Signing secret | `your_secret` | Only on your server. It signs a token for each request and is shown once, at creation or rotation. | ## Signing a request Each request carries `Authorization: Bearer `, where the JWT is signed with **HS256** using the whole secret string as the key. ``` GET /v1/objects/user-42?api_key=7Hx2Qp9LmZ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFrXzdIeDJRcDlMbVoifQ.eyJpYXQiOjE3OTAwMDAwMDAsImV4cCI6MTc5MDAwMDA2MCwiaHRtIjoiR0VUIiwiaHR1IjoiL3YxL29iamVjdHMvdXNlci00MiJ9.… ``` The header and claims: ```json { "alg": "HS256", "typ": "JWT", "kid": "7Hx2Qp9LmZ" } ``` ```json { "iat": 1790000000, "exp": 1790000060, "jti": "5f0c8e8e2b7d4c1e9a3d6b2f1c0e4a7b", "htm": "GET", "htu": "/v1/objects/user-42" } ``` | Claim | Required | Meaning | |-------|----------|---------| | `iat` | yes | Issued at, in Unix seconds. | | `exp` | yes | Expiry. At most **300 seconds** after `iat`. | | `jti` | no | A unique ID for the token. | | `htm` | no | Binds the token to one HTTP method. | | `htu` | no | Binds the token to one path (or a full URL, of which only the path is compared). | - `kid` in the JWT header can replace the `api_key` query parameter. If you send both, they must match. - The server allows **±60 seconds** of clock skew on `iat` and `exp`. - Binding with `htm` and `htu` is optional but recommended: a leaked token then only works for the one request it was made for. The SDKs always bind, and sign a new 60-second token per request. - Tokens in the query string (`access_token`, `token`, `jwt`) are rejected, because URLs end up in logs. Every failure (unknown, inactive or revoked key, bad signature, expired or future-dated token, wrong method or path) is the same generic `401`: ```json { "error": { "code": "unauthorized", "message": "unauthorized" } } ``` ## Examples Shell: ```bash b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; } sign() { # sign METHOD PATH local now=$(date +%s) h p h=$(printf '{"alg":"HS256","typ":"JWT","kid":"%s"}' "$ANALYZE_API_KEY" | b64url) p=$(printf '{"iat":%d,"exp":%d,"htm":"%s","htu":"%s"}' "$now" "$((now + 60))" "$1" "$2" | b64url) printf '%s.%s.%s' "$h" "$p" \ "$(printf '%s.%s' "$h" "$p" | openssl dgst -sha256 -hmac "$ANALYZE_SECRET" -binary | b64url)" } curl "https://api.sigwise.ai/v1/me?api_key=$ANALYZE_API_KEY" \ -H "Authorization: Bearer $(sign GET /v1/me)" ``` Node.js: ```ts import { createHmac, randomUUID } from "node:crypto"; function sign(apiKey: string, secret: string, method: string, path: string): string { const b64 = (v: object) => Buffer.from(JSON.stringify(v)).toString("base64url"); const now = Math.floor(Date.now() / 1000); const head = b64({ alg: "HS256", typ: "JWT", kid: apiKey }); const body = b64({ iat: now, exp: now + 60, jti: randomUUID(), htm: method, htu: path }); const sig = createHmac("sha256", secret).update(`${head}.${body}`).digest("base64url"); return `${head}.${body}.${sig}`; } ``` Python: ```python import base64, hashlib, hmac, json, time, uuid def sign(api_key: str, secret: str, method: str, path: str) -> str: b64 = lambda v: base64.urlsafe_b64encode(json.dumps(v).encode()).rstrip(b"=").decode() now = int(time.time()) head = b64({"alg": "HS256", "typ": "JWT", "kid": api_key}) body = b64({"iat": now, "exp": now + 60, "jti": uuid.uuid4().hex, "htm": method, "htu": path}) sig = hmac.new(secret.encode(), f"{head}.{body}".encode(), hashlib.sha256).digest() return f"{head}.{body}." + base64.urlsafe_b64encode(sig).rstrip(b"=").decode() ``` Go: ```go func sign(apiKey, secret, method, path string) string { enc := base64.RawURLEncoding now := time.Now().Unix() head, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT", "kid": apiKey}) body, _ := json.Marshal(map[string]any{"iat": now, "exp": now + 60, "htm": method, "htu": path}) msg := enc.EncodeToString(head) + "." + enc.EncodeToString(body) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(msg)) return msg + "." + enc.EncodeToString(mac.Sum(nil)) } ``` PHP: ```php function sign(string $apiKey, string $secret, string $method, string $path): string { $b64 = fn (string $v) => rtrim(strtr(base64_encode($v), '+/', '-_'), '='); $now = time(); $head = $b64(json_encode(['alg' => 'HS256', 'typ' => 'JWT', 'kid' => $apiKey])); $body = $b64(json_encode(['iat' => $now, 'exp' => $now + 60, 'htm' => $method, 'htu' => $path], JSON_UNESCAPED_SLASHES)); return "$head.$body." . $b64(hash_hmac('sha256', "$head.$body", $secret, true)); } ``` > **Sign the decoded path** > > `htu` is compared with the request's **decoded** path. For an object ID with > characters that need escaping, like `user 42`, request > `/v1/objects/user%2042` but sign `/v1/objects/user 42`. ## Rotating and revoking keys - **Rotate** (`POST /v1/api_keys/{key_id}/rotate`) keeps the key ID and issues a new secret. The old secret stops working immediately. - **Deactivate** (`PATCH` with `{"status": "inactive"}`) pauses a key; set it back to `active` to resume. - **Revoke** (`DELETE`) retires a key for good. Its ID is never reissued. Every console user of the account gets an email when a key is created, rotated or revoked. See [API keys](https://sigwise.ai/docs/guides/api-keys.md). ## Console tokens The SigWise console authenticates people, not integrations: it signs in with an email and password (or Google or GitHub) and gets a **console token** from the `/auth/console/*` endpoints. Those endpoints are documented under *Console* in the [API reference](https://sigwise.ai/docs/reference.md), but integrations should always use API keys. ### Two-factor authentication and lockout Console users can turn on two-factor authentication with an authenticator app from the account menu. Once it is on, signing in with a password also needs a 6-digit code, or one of the eight single-use recovery codes shown when you turned it on. Signing in with Google or GitHub uses that provider's own security. Repeated failed sign-ins lock the account, and the address they came from, out for 15 minutes. The API answers `429` with a `Retry-After` header. ## Deprecated: legacy keys Keys created before request signing have no signing secret. Until rotated, they still authenticate by sending the old long key as `Authorization: Bearer your_key_id` or `X-API-Key`, or by exchanging it at `POST /auth/token`. Both paths log a warning and will be removed. Rotate a legacy key to get a signing secret. --- Source: https://sigwise.ai/docs/guide/concepts.md # Core concepts ## Accounts Everything belongs to an **account** (a tenant): signals, objects, webhooks, rules, API keys and the prepaid balance. API keys and console users each belong to exactly one account, and no request can reach another account's data. ## Objects An **object** is anything you want answers about: a user, a listing, an order, a conversation. You identify it with your own `object_id` (any string, such as `user-42`), and it exists as soon as you send its first event. There is no call to create one. `object_type` (for example `user` or `listing`) is optional. It groups objects in the console's overview. ## Events **Events** are the evidence answers are computed from. Each has a `type`: - `event`: a discrete action, named with `name`, e.g. `profile.updated`, `payout.requested`, `login.failed`; - `message`: free-form text in `content`, such as a chat message or a review. Either can carry `metadata` (any JSON object) and `occurred_at` (defaults to when the API received it). Events accumulate: every analysis looks at the object's whole history, not only the latest batch. ### History and compaction To keep analysis fast and bounded on long-lived objects, older events are folded into a rolling **compacted summary** (counts by action, sample messages, first seen) while the most recent events are kept verbatim. The analyzer gets both. You can inspect the summary with `GET /v1/objects/{object_id}/state`. ## Signals A **signal** is a question asked about every object: *is this a scammer?* It has a `key` (`is_scammer`), a `type` (`noul`, `score` or `choice`), `instructions` for the analyzer and type-specific `criteria`. Signals can be added, changed, disabled or removed at any time. See [Signals](https://sigwise.ai/docs/guide/signals.md). ## Analysis An **analysis** runs every enabled signal over one object's history and stores the latest answer for each. - **Debounced.** Ingesting events schedules an analysis a few seconds later (the `analysis_delay_ms` in the response). Events that arrive in the meantime join the same analysis, so a burst of activity costs one analysis, not one per event. - **Queued by priority.** Live traffic (ingestion, re-analyzing one object) is served ahead of bulk work (re-analyzing everything, backfills), so a large backfill never delays real events. - **Latest answer wins.** Each signal keeps its most recent answer, with the `model` that produced it and `computed_at`. - **Billed per analysis.** Each analysis draws on the account's prepaid balance. See [Billing and usage](https://sigwise.ai/docs/guides/billing.md). You can also force an analysis without new events: one object with `POST /v1/objects/{object_id}/analyze`, or every object with `POST /v1/analyze`. ## Results `GET /v1/objects/{object_id}` returns the latest answer per signal in `analysis`, and the signals that have no answer yet in `pending` (for example one added after the last analysis). Answers can also be pushed to you by [webhooks](https://sigwise.ai/docs/guides/webhooks.md) and acted on by [rules](https://sigwise.ai/docs/guides/rules.md). ## Idempotency and ordering Ingestion is not idempotent: sending the same event twice records it twice. The SDKs therefore never retry `POST` requests automatically. The analyzer sees an object's events in `occurred_at` order, so set `occurred_at` when you send events some time after they happened. --- Source: https://sigwise.ai/docs/guide/signals.md # Signals A signal is a question SigWise answers about every object. You define it with `PUT /v1/signals/{key}`; the same call updates it later. ```json { "type": "noul", "instructions": "Decide if this user is likely a scammer.", "criteria": { "true": "clear scam signals", "false": "legitimate behaviour" }, "enabled": true } ``` - `key` (in the path) is the stable name you read answers by. Use letters, digits and underscores, such as `is_scammer`. - `instructions` tell the analyzer what to decide. Usually a sentence; objects and arrays are passed through verbatim for structured prompts. - `criteria` describe the possible answers. Their shape depends on `type`. - `enabled` (default `true`) switches the signal off without deleting it or its answers. ## Types ### `noul`: yes or no The answer is `noul`, the probability (0 to 1) that the answer is **yes**. ```json { "type": "noul", "instructions": "Will this user churn in the next 30 days?", "criteria": { "true": "disengaging, complaining, cancelling", "false": "active and satisfied" } } ``` ```json { "key": "churn_risk", "type": "noul", "noul": 0.27 } ``` `criteria` is an object with optional `true` and `false` descriptions. ### `score`: a labelled spectrum The answer is `score`, a position on an ordered list of levels: `0` is the first level and `levels − 1` the last, with fractions in between. It also carries `probabilities` and a `confidence`. ```json { "type": "score", "instructions": "Rate overall trustworthiness.", "criteria": ["high risk", "neutral", "trusted"] } ``` ```json { "key": "trust_score", "type": "score", "score": 1.63, "confidence": 0.8 } ``` `criteria` is an array of level descriptions, from lowest to highest. ### `choice`: one of N The answer is `choice`, the most likely option, with `probabilities` for every option and a `confidence`. ```json { "type": "choice", "instructions": "Classify the buyer's intent.", "criteria": { "browsing": null, "comparing": "asks about alternatives", "ready_to_buy": "asks how to pay" } } ``` ```json { "key": "buyer_intent", "type": "choice", "choice": "ready_to_buy", "probabilities": { "browsing": 0.05, "comparing": 0.15, "ready_to_buy": 0.8 }, "confidence": 0.8 } ``` `criteria` maps each option to a description, or to `null` when the name says it all. ## Default signals Every new account starts with three signals, so the first events already get answers: `is_scammer` (noul), `trust_score` (score) and `buyer_intent` (choice). Change or delete them like any other signal. ## Writing good signals - **Ask one thing.** "Is this a scammer?" works better than "Is this a scammer or a spammer?" Make two signals instead. - **Describe the evidence.** Criteria like "asks to pay outside the platform" steer the analyzer more than "suspicious". - **Pick the type by what you will do with it.** Thresholds and alerts suit `noul`; ranking suits `score`; routing suits `choice`. - **Name keys for conditions.** Keys appear in [search and rule conditions](https://sigwise.ai/docs/guides/queries.md), such as `is_scammer >= 90`. ## Changing signals - A new or changed signal is used from the **next** analysis of each object. Objects not analyzed since show it in `pending`. - To answer a new signal for existing objects, run a [backfill](https://sigwise.ai/docs/guides/backfills.md), or turn on `auto_backfill_signals` in [settings](https://sigwise.ai/docs/reference.md#tag/account) to backfill every new signal automatically. - Deleting a signal removes it from future analyses. ## Endpoints | | | |---|---| | GET `/v1/signals` | List signals | | PUT `/v1/signals/{key}` | Create or update a signal | | DELETE `/v1/signals/{key}` | Delete a signal | | GET `/v1/signals/{key}/backfill` | Backfill estimate and progress | | POST `/v1/signals/{key}/backfill` | Start a backfill | --- Source: https://sigwise.ai/docs/guides/ingesting-events.md # Ingesting events `POST /v1/objects/{object_id}/events` records one or more events about an object and schedules its analysis. ```json { "object_type": "user", "events": [ { "type": "event", "name": "profile.updated", "metadata": { "field": "bio" } }, { "type": "event", "name": "payout.requested", "metadata": { "amount": 950, "currency": "usd" } }, { "type": "message", "content": "is this still available? can I pay by wire?", "occurred_at": "2026-09-27T09:58:00Z" } ] } ``` | Field | | | |-------|---|---| | `events` | required | One or more events, below. | | `object_type` | optional | A classification such as `user` or `listing`. | | `wait` | optional | `true` scores the object inline instead. See [Direct moderation](https://sigwise.ai/docs/guides/moderation.md). | | `signals` | optional | With `wait`, the signals to score. | | `include_history` | optional | With `wait`, whether to score over the object's history (default) or only these events. | Each event: | Field | | | |-------|---|---| | `type` | required | `event` (an action) or `message` (text). | | `name` | optional | The action's name, like `login.failed`, or a message's subject. | | `content` | optional | Free text, such as a message body. | | `metadata` | optional | Any JSON object. | | `occurred_at` | optional | RFC 3339 time it happened. Defaults to when the API received it. | The response is `202 Accepted`: ```json { "object_id": "user-42", "accepted": 3, "analysis_status": "scheduled", "analysis_delay_ms": 5000 } ``` ## Debouncing The analysis runs `analysis_delay_ms` after the latest event. Events that arrive in the meantime push it back and join it, so a burst of activity costs one analysis. Send events as they happen; there is no need to batch them yourself, though batching several in one request saves round trips. ## What to send The analyzer reasons over whatever you send, so send the evidence a human moderator would want to see: - **Messages** between users, reviews, listing descriptions, support tickets. - **Actions** that matter for your signals: sign-ups, profile and payout changes, failed logins, reports by other users, refunds. - **Context** in `metadata`: amounts, counts, account age, country. Keep it small and relevant; avoid secrets and data you don't need analyzed. ## Limits and validation - A request body is at most **1 MiB**. - Bodies are decoded strictly: an unknown field is a `400`, so a typo like `"evnts"` fails loudly instead of being ignored. - `type` must be `event` or `message`, and `metadata` must be valid JSON. - When the prepaid balance is empty, ingestion answers `402` and records nothing. See [Billing and usage](https://sigwise.ai/docs/guides/billing.md). ## Retrying safely A timeout or a `5xx` doesn't tell you whether your events were recorded. Send an `Idempotency-Key` header, any unique string such as a UUID, and retry with the same key: ```bash curl -X POST "$API/v1/objects/user-42/events" \ -H "Idempotency-Key: 6f1c0e3e-5b8a-4c7e-9f2d-1a2b3c4d5e6f" \ -H "Content-Type: application/json" \ -d '{"object_type":"user","events":[{"type":"message","content":"hi"}]}' ``` - The events are recorded once, however many times the request is sent. - A repeat of an asynchronous request is answered like the first and carries `Idempotent-Replayed: true`. - A repeat of a `wait: true` request returns `409` and does not analyze (or charge) again. Read the verdict with `GET /v1/objects/{object_id}`. - Using a key for a different object returns `422`. - Keys are remembered for 24 hours, per account. ## Re-analyzing without new events - `POST /v1/objects/{object_id}/analyze` schedules an immediate analysis of one object, for example after changing a signal. It answers `404` for an object with no events. - `POST /v1/analyze` schedules every object at bulk priority, behind live traffic. Each analysis is billed, so prefer a [backfill](https://sigwise.ai/docs/guides/backfills.md) when you only added a signal. ## Reading what was recorded `GET /v1/objects/{object_id}/events` returns the 200 most recent events, newest first, and `GET /v1/objects/{object_id}/state` returns the compacted summary of older ones. --- Source: https://sigwise.ai/docs/guides/moderation.md # Direct moderation Asynchronous ingestion is right for scoring users over time. To decide about a piece of content **before** anyone sees it (a message, a listing, a review), send it with `"wait": true`: the API records the events, scores the object inline and returns the verdict. cURL: ```bash curl -X POST "$API/v1/objects/user-42/events?api_key=$ANALYZE_API_KEY" \ -H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \ -d '{"wait":true,"signals":["is_scammer"], "events":[{"type":"message","content":"pay me by wire and I double it"}]}' ``` Node.js: ```ts const verdict = await analyze.events.ingest("user-42", { wait: true, signals: ["is_scammer"], events: [{ type: "message", content: "pay me by wire and I double it" }], }); if ("answers" in verdict && (verdict.answers[0]?.noul ?? 0) > 0.9) { return reject(); } ``` Python: ```python verdict = analyze.events.ingest( "user-42", wait=True, signals=["is_scammer"], events=[{"type": "message", "content": "pay me by wire and I double it"}], ) if verdict["answers"] and verdict["answers"][0].get("noul", 0) > 0.9: reject() ``` Go: ```go res, err := client.Events.Ingest(ctx, "user-42", &analyze.IngestRequest{ Wait: analyze.Bool(true), Signals: []string{"is_scammer"}, Events: []analyze.EventInput{{Type: analyze.EventTypeMessage, Content: analyze.String("pay me by wire and I double it")}}, }) if v := res.ModerationVerdict; v != nil && len(v.Answers) > 0 && *v.Answers[0].Noul > 0.9 { reject() } ``` PHP: ```php $verdict = $analyze->events->ingest('user-42', [ 'wait' => true, 'signals' => ['is_scammer'], 'events' => [['type' => 'message', 'content' => 'pay me by wire and I double it']], ]); if (($verdict['answers'][0]['noul'] ?? 0) > 0.9) { reject(); } ``` The response is `200 OK`: ```json { "object_id": "user-42", "accepted": 1, "analyzed": true, "history_included": true, "model": "model-1", "latency_ms": 840, "answers": [ { "signal": "is_scammer", "type": "noul", "noul": 0.97 } ] } ``` ## Options - **`signals`** scores only the listed signals, which is faster and cheaper than scoring all of them. Omit it to score every enabled signal. - **`include_history`** (default `true`) scores over the object's prior events too. Set it to `false` to judge this content on its own, for example a listing description regardless of who wrote it. When no configured signal matches, nothing is scored: `analyzed` is `false` and `reason` says why. The events are still recorded. ## Deciding A common pattern is three bands on a `noul` signal: | `noul` | Action | |--------|--------| | `>= 0.9` | Block | | `0.6 – 0.9` | Hold for human review | | `< 0.6` | Publish | Tune the thresholds on your own traffic: read a sample of answers from the console before you enforce them. ## Failure handling The request blocks on the analyzer, so give it a timeout suited to your latency budget and decide what happens when it fails: - `402` means the balance is empty; the events were recorded but not scored. - `502` means the analyzer failed; the events were recorded. - A timeout or network error on your side means you don't know. Failing open (publish, then review asynchronously) or closed (hold) is a product decision. Synchronous scoring is billed like any analysis and does not schedule another asynchronous one. --- Source: https://sigwise.ai/docs/guides/reading-results.md # Reading results ## One object `GET /v1/objects/{object_id}` returns the latest answer for every signal that has one, and the enabled signals that don't yet: ```json { "object_id": "user-42", "event_count": 7, "analysis": [ { "key": "is_scammer", "type": "noul", "noul": 0.91, "model": "model-1", "computed_at": "2026-09-27T10:00:05Z" }, { "key": "buyer_intent", "type": "choice", "choice": "ready_to_buy", "probabilities": { "browsing": 0.05, "comparing": 0.15, "ready_to_buy": 0.8 }, "confidence": 0.8, "model": "model-1", "computed_at": "2026-09-27T10:00:05Z" } ], "pending": ["trust_score"] } ``` Only the fields for a signal's type are set: `noul` for noul signals, `score` for score signals, `choice` for choice signals, plus `probabilities` and `confidence` for the latter two. An object with no events and no answers is a `404`. ### Caching Reads are cached for a few seconds. The `X-Cache` header says whether a response was a `HIT` or a `MISS`. Ingesting events and completed analyses invalidate the object's cache entry, so new answers show up as soon as they are stored. ## Waiting for an analysis After ingesting, answers arrive once the debounced analysis has run. To wait for them: 1. **Webhooks (recommended).** Register an endpoint and get `analysis.completed` with the answers. See [Webhooks](https://sigwise.ai/docs/guides/webhooks.md). 2. **Polling.** Read the object after `analysis_delay_ms` plus a margin, and check `computed_at`. Poll gently; there is no benefit to polling faster than once a second. 3. **Inline.** For a decision you need now, ingest with `"wait": true`. See [Direct moderation](https://sigwise.ai/docs/guides/moderation.md). ## Many objects `GET /v1/objects` returns a page of objects with their answers: | Parameter | | |-----------|---| | `q` | A substring of the object ID or display name, or a signal query such as `is_scammer >= 90` (see [Search and conditions](https://sigwise.ai/docs/guides/queries.md)). | | `sort` | `recent` (default), `flagged` (highest yes/no probability first) or `trust` (highest score first). | | `limit` | 1–200, default 100. | | `offset` | For the next page. | ```bash curl "$API/v1/objects?q=is_scammer%20%3E%3D%2090&sort=flagged&limit=50&api_key=$ANALYZE_API_KEY" \ -H "Authorization: Bearer $(sign)" ``` `GET /v1/overview` returns account-wide totals: how many objects exist and are analyzed, events in the last 24 hours, flagged objects, and each signal's distribution of answers. --- Source: https://sigwise.ai/docs/guides/queries.md # Search and conditions Object search (`GET /v1/objects?q=`) and [rule](https://sigwise.ai/docs/guides/rules.md) conditions share one small language for comparing signal values. ``` is_scammer >= 90 and trust_score < 1 buyer_intent = ready_to_buy, is_scammer < 20 ``` ## Grammar A query is one or more **comparisons** joined by `and` (any case) or commas. All of them must hold. ``` comparison := signal_key operator value operator := >= | <= | > | < | = | != ``` `signal_key` is the key of one of your signals. ## Values by signal type | Type | Value | Operators | Example | |------|-------|-----------|---------| | `noul` | A percentage. Values above 1 are read as percent, so `90` and `0.9` both mean 0.9. | all | `is_scammer >= 90` | | `score` | The raw score, from 0 (first level) up. | all | `trust_score < 1` | | `choice` | An option name, optionally quoted. | `=`, `!=` | `buyer_intent = ready_to_buy` | Values are compared with the object's latest answer. An object with no answer for a signal matches no comparison on it. ## Search versus rules - **Search** is lenient. A `q` without any comparison operator is a substring search on object IDs and display names, and comparisons that don't parse or name an unknown signal are skipped. - **Rules** are strict. A condition must parse completely, name existing signals and use operators their types support; otherwise creating the rule fails with a `400` that says what is wrong: ```json { "error": { "code": "bad_request", "message": "choice signal \"buyer_intent\" supports only = and !=" } } ``` --- Source: https://sigwise.ai/docs/guides/webhooks.md # Webhooks Instead of polling, register an endpoint and receive a signed `POST` each time an asynchronous analysis completes. ## Register an endpoint cURL: ```bash curl -X POST "$API/v1/webhooks?api_key=$ANALYZE_API_KEY" \ -H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \ -d '{"url":"https://example.com/hooks/analyze","signal_keys":["is_scammer"]}' ``` Node.js: ```ts const { endpoint, secret } = await sigwise.webhooks.create({ url: "https://example.com/hooks/analyze", signal_keys: ["is_scammer"], }); ``` Python: ```python created = analyze.webhooks.create(url="https://example.com/hooks/analyze", signal_keys=["is_scammer"]) secret = created["secret"] ``` The response includes the endpoint's signing **secret, shown only once**. Store it with your application's secrets. `signal_keys` is optional: with it, the endpoint only receives analyses that answered one of those signals. Update an endpoint with `PATCH /v1/webhooks/{endpoint_id}` (for example `{"enabled": false}` to pause it) and remove it with `DELETE`. Deliveries already queued for a paused endpoint wait and resume when you enable it again. ## Choosing events `events` picks which event types an endpoint receives. Omit it to receive all of them. | Event | Sent when | |-------|-----------| | `analysis.completed` | Any analysis of an object finishes. | | `rule.triggered` | A [rule](https://sigwise.ai/docs/guides/rules.md) whose webhook action points at this endpoint fires. | To be notified only when a rule matches, for example `is_scammer > 50`, register an endpoint that receives rule firings and nothing else, then point the rule's webhook action at it: ```bash curl -X POST "$API/v1/webhooks?api_key=$ANALYZE_API_KEY" \ -H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \ -d '{"url":"https://example.com/hooks/scammers","events":["rule.triggered"]}' ``` ## Payload ```http POST /hooks/analyze HTTP/1.1 Content-Type: application/json User-Agent: analyzeapi-webhooks/1 X-Webhook-Event: analysis.completed X-Webhook-Delivery: 3f0c1c8e-7a5e-4e0b-9f5d-0a4d7f6c2b11 X-Webhook-Timestamp: 1790000000 X-Webhook-Signature: sha256=5d41402abc4b2a76b9719d911017c592… ``` ```json { "event": "analysis.completed", "object_id": "user-42", "tenant_id": "0c6f…", "model": "model-1", "occurred_at": "2026-09-27T10:00:05Z", "answers": [ { "signal": "is_scammer", "type": "noul", "noul": 0.91 } ] } ``` `answers` has the same shape as a [direct moderation](https://sigwise.ai/docs/guides/moderation.md) verdict. [Rules](https://sigwise.ai/docs/guides/rules.md) with a webhook action send `rule.triggered` events through the same machinery; both payloads are in the [API reference](https://sigwise.ai/docs/reference.md). ## Verify every delivery `X-Webhook-Signature` is `sha256=` followed by the hex HMAC-SHA256 of `"."`, keyed with the endpoint secret. Verify it against the **raw** body (before any JSON parsing), compare in constant time, and reject timestamps more than a few minutes old to stop replays. Node.js: ```ts import { constructWebhookEvent } from "@sigwise/sdk"; app.post("/hooks/analyze", express.raw({ type: "application/json" }), (req, res) => { try { const event = constructWebhookEvent( req.body, // Buffer req.header("x-webhook-signature"), req.header("x-webhook-timestamp"), process.env.ANALYZE_WEBHOOK_SECRET!, ); handle(event); res.sendStatus(204); } catch { res.sendStatus(400); } }); ``` Python: ```python from sigwise import WebhookVerificationError, construct_webhook_event @app.post("/hooks/analyze") def hook(): try: event = construct_webhook_event( request.get_data(), request.headers.get("X-Webhook-Signature"), request.headers.get("X-Webhook-Timestamp"), os.environ["ANALYZE_WEBHOOK_SECRET"], ) except WebhookVerificationError: return "", 400 handle(event) return "", 204 ``` Go: ```go func hook(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) ev, err := sigwise.ParseWebhookEvent(body, r.Header.Get("X-Webhook-Signature"), r.Header.Get("X-Webhook-Timestamp"), os.Getenv("ANALYZE_WEBHOOK_SECRET"), 0) if err != nil { http.Error(w, "invalid signature", http.StatusBadRequest) return } handle(ev) w.WriteHeader(http.StatusNoContent) } ``` PHP: ```php use SigWise\Webhook; use SigWise\WebhookVerificationException; try { $event = Webhook::constructEvent( file_get_contents('php://input'), $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? null, $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? null, getenv('ANALYZE_WEBHOOK_SECRET'), ); } catch (WebhookVerificationException $e) { http_response_code(400); exit; } ``` Go (no SDK): ```go func verify(secret, timestamp, signature string, body []byte) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(timestamp + ".")) mac.Write(body) want := "sha256=" + hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(want), []byte(signature)) } ``` ## Retries and the dead-letter queue Respond with any `2xx` quickly (within 10 seconds) and do slow work afterwards. - Network errors, timeouts, `429` and `5xx` responses are **retried** with exponential backoff and jitter (starting at about 30 seconds and doubling), up to 6 attempts. - Other `4xx` responses are **permanent** and not retried. - A delivery that fails permanently or runs out of attempts moves to the **dead-letter queue** (`status: dead`), keeping its payload and last response. Deliveries can arrive more than once and out of order. Deduplicate on `X-Webhook-Delivery` (retries of one delivery share it) and use `occurred_at` to ignore stale updates. Inspect deliveries with `GET /v1/webhook_deliveries` (`?status=dead`, `?endpoint_id=`) and re-send one with `POST /v1/webhook_deliveries/{delivery_id}/replay` once your endpoint is fixed. --- Source: https://sigwise.ai/docs/guides/rules.md # Rules Webhooks push every completed analysis. **Rules** push only when signal values cross a line you care about, and take an action: an email, a Slack message or a webhook. ```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 } ``` cURL: ```bash curl -X POST "$API/v1/rules?api_key=$ANALYZE_API_KEY" \ -H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \ -d '{"name":"scammer alert","when":"is_scammer >= 90", "action_type":"email","action_config":{"to":"trust@acme.com"}}' ``` Node.js: ```ts await analyze.rules.create({ name: "scammer alert", when: "is_scammer >= 90", action_type: "email", action_config: { to: "trust@acme.com" }, }); ``` Python: ```python analyze.rules.create( name="scammer alert", when="is_scammer >= 90", action_type="email", action_config={"to": "trust@acme.com"}, ) ``` ## Conditions `when` uses the [search and conditions](https://sigwise.ai/docs/guides/queries.md) language: noul values are percentages (`>= 90`), scores are raw (`trust_score < 1`), and choices compare with `=` or `!=` (`buyer_intent = ready_to_buy`). Conditions are validated against your signals when you create or update the rule. ## Actions | `action_type` | `action_config` | Does | |---------------|-----------------|------| | `email` | `{"to": "trust@acme.com", "subject": "…"}` (`subject` optional) | Sends an alert email. | | `slack` | `{"webhook_url": "https://hooks.slack.com/…"}` | Posts to a Slack incoming webhook. | | `webhook` | `{"endpoint_id": ""}` | Sends `rule.triggered` to one of your [webhook endpoints](https://sigwise.ai/docs/guides/webhooks.md), with the same signing, retries and dead-letter queue. The endpoint must be subscribed to `rule.triggered`; subscribe it to only that event to receive rule firings without every analysis. | ## When a rule fires Rules are evaluated after every analysis, asynchronous or [direct](https://sigwise.ai/docs/guides/moderation.md), against the new answers. - A rule fires when its condition **turns true** for an object, and does not fire again while it stays true. - With `cooldown_seconds` above 0, it may fire again once that long has passed since it last fired, if the condition is still true. - It fires again after the condition has been false in between. This is tracked per rule and object. ## Audit Every firing is recorded with the condition, the signal values that satisfied it and the action's outcome (`dispatched` or `error`): ```bash curl "$API/v1/rule_firings?rule_id=$RULE_ID&api_key=$ANALYZE_API_KEY" -H "Authorization: Bearer $(sign)" ``` Pause a rule with `PATCH /v1/rules/{rule_id}` and `{"enabled": false}`, and remove it with `DELETE`. --- Source: https://sigwise.ai/docs/guides/backfills.md # Backfills A new signal is answered from each object's next analysis. To answer it for the objects you already have, run a **backfill**: a re-analysis of every object that has no answer for the signal, for that signal only. ## Estimate first Each analysis is billed, so check what a backfill would cost: ```bash curl "$API/v1/signals/wants_refund/backfill?api_key=$ANALYZE_API_KEY" -H "Authorization: Bearer $(sign)" ``` ```json { "estimate": { "objects": 12840, "estimated_cost_micros": 25680000 }, "backfill": null } ``` `estimated_cost_micros` is in micro-dollars (here $25.68), an upper bound based on your recent average cost per analysis. `backfill` is the progress of the last backfill, or `null`. ## Start it ```bash curl -X POST "$API/v1/signals/wants_refund/backfill?api_key=$ANALYZE_API_KEY" -H "Authorization: Bearer $(sign)" ``` ```json { "signal_key": "wants_refund", "status": "running", "total": 12840, "completed": 0, "pending": 12840, "started_at": "2026-09-27T10:00:00Z" } ``` Backfill jobs run at **bulk priority**: live traffic is always served first, so a large backfill never delays the analysis of new events. Poll the `GET` above to follow `completed` and `pending`; `status` becomes `done` when nothing is left. A disabled signal can't be backfilled (`409`). Enable it first. ## Automatic backfills Set `auto_backfill_signals` to backfill every new signal as soon as it is created. It is off by default because of the cost. ```bash curl -X PATCH "$API/v1/settings?api_key=$ANALYZE_API_KEY" \ -H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \ -d '{"auto_backfill_signals":true}' ``` With it on, the signal returned by `PUT /v1/signals/{key}` includes the started `backfill`. --- Source: https://sigwise.ai/docs/guides/api-keys.md # API keys Manage keys in the console under **API keys**, or with the API. | | | |---|---| | GET `/v1/api_keys` | List keys (never their secrets) | | POST `/v1/api_keys` | Create a key; returns its secret once | | PATCH `/v1/api_keys/{key_id}` | Rename, deactivate or reactivate | | POST `/v1/api_keys/{key_id}/rotate` | Replace the secret; returns the new one once | | DELETE `/v1/api_keys/{key_id}` | Revoke for good | `{key_id}` is the key's `id` (a UUID) from the list, not its public `your_key_id` key ID. ```json { "api_key": { "id": "9b2e…", "key": "7Hx2Qp9LmZ", "name": "Production", "status": "active", "has_secret": true, "created_at": "2026-09-27T10:00:00Z" }, "key": "7Hx2Qp9LmZ", "secret": "your_secret", "note": "Save this secret now — it is not shown again." } ``` ## Practices - **One key per integration or environment**, named so you can tell them apart. `last_used_at` shows which are in use. - **Keep secrets server-side.** Never ship a secret in a browser or mobile app; anyone who has it can call the API as you. - **Rotate on a schedule and on suspicion.** Rotation keeps the key ID, so only the secret changes in your configuration. The old secret stops working immediately, so deploy the new one first. - **Deactivate before deleting** if you are unsure a key is unused: a deactivated key fails with `401` and can be reactivated. Every console user of the account gets an email when a key is created, rotated or revoked. ## Legacy keys Keys created before request signing are marked `legacy` and have no signing secret (`has_secret: false`). Rotate them to get one; see [Authentication](https://sigwise.ai/docs/guide/authentication.md#deprecated-legacy-keys). --- Source: https://sigwise.ai/docs/guides/billing.md # Billing and usage SigWise is prepaid: you add funds, and each analysis draws down the balance. There is no subscription and every account starts with free credit. ## Balance ```bash curl "$API/v1/billing?api_key=$ANALYZE_API_KEY" -H "Authorization: Bearer $(sign)" ``` ```json { "balance_cents": 1250, "currency": "usd", "metering_enabled": true, "enforcement": "block", "self_serve_topup": true, "low_balance_threshold_cents": 100, "topup_min_cents": 500, "topup_max_cents": 100000 } ``` When the balance reaches zero under `block` enforcement, analyses pause: ingestion answers `402 payment_required` and records nothing, and [direct moderation](https://sigwise.ai/docs/guides/moderation.md) records the events but doesn't score them. Console users are emailed when the balance runs low and when it runs out. ## Adding funds Add funds in the console with a card, or through the API: ```bash curl -X POST "$API/v1/billing/checkout?api_key=$ANALYZE_API_KEY" \ -H "Authorization: Bearer $(sign)" -H 'Content-Type: application/json' \ -d '{"amount_cents":2500}' ``` This returns a Stripe Checkout `url` to send the payer to. The balance is credited once Stripe confirms the payment; check with `GET /v1/billing/checkout/{session_id}` (`pending` until then, `credited` after). ## Ledger Every change to the balance is an entry in the ledger: a negative `delta_cents` for each analysis, a positive one for each credit. ```bash curl "$API/v1/billing/ledger?type=charge&limit=50&api_key=$ANALYZE_API_KEY" -H "Authorization: Bearer $(sign)" ``` A single analysis usually costs a fraction of a cent, which rounds to `0` in `delta_cents`; `delta_micros` has the exact amount in micro-dollars (1e-6 USD). Pages are newest first: pass `next_cursor` as `cursor` for the next page, until it comes back empty. ## Usage `GET /v1/usage?from=2026-09-01&to=2026-09-30` summarizes a date range (UTC, inclusive, at most 366 days, the last 30 by default): analyses, spend and tokens in total and per day, broken down by model, by source (`async` or `sync`) and by signal, with your average daily spend and how many days the balance would last at that rate (`runway_days`). --- Source: https://sigwise.ai/docs/guides/errors.md # Errors and retries ## The error envelope Every error has the same shape: ```json { "error": { "code": "not_found", "message": "no data for this object" } } ``` Match on `code`, which is stable. `message` is for humans and may change. | Status | `code` | Meaning | |--------|--------|---------| | `400` | `bad_request` | Malformed JSON, an unknown field, or a failed validation. `message` says which. | | `401` | `unauthorized` | Missing, invalid, expired or revoked credentials. See [Authentication](https://sigwise.ai/docs/guide/authentication.md). | | `402` | `payment_required` | The prepaid balance is empty. See [Billing](https://sigwise.ai/docs/guides/billing.md). | | `403` | `forbidden` | The credential can't do this, e.g. a console-only action with an API key. | | `404` | `not_found` | The object, signal, rule, endpoint or key doesn't exist. | | `409` | `conflict` | The request conflicts with the current state, e.g. backfilling a disabled signal. | | `422` | `idempotency_key_reused` | An `Idempotency-Key` was already used for a different object. | | `429` | `rate_limited` | Too many requests. `/v1` requests are limited per account (3,000 a minute by default), and so are concurrent `wait: true` analyses. Wait for `Retry-After` seconds. | | `502` | `internal_error`, `payment_provider_error` | An upstream service (the analyzer, Stripe) failed. | | `503` | `analyzer_busy` | The analyzer is throttling a `wait: true` request. The events were recorded; don't resend them. After `Retry-After`, request an analysis or read the answers later. | | `503` | `payments_not_configured`, `not_ready` | A feature is unavailable on this deployment, or the service is starting. | | `500` | `internal_error` | Something went wrong on our side. | ## Retrying - **Safe to retry:** `GET`, `PUT` and `DELETE` requests, after a network error, a timeout, a `429` or a `5xx`. Back off exponentially with jitter. - **Safe to retry with an idempotency key:** event ingestion. Send an `Idempotency-Key` header and retry with the same key; the events are recorded once. See [Ingesting events](https://sigwise.ai/docs/guides/ingesting-events.md#retrying-safely). - **Not safe to retry blindly:** other `POST` requests, and ingestion without an `Idempotency-Key`. A timeout doesn't tell you whether the events were recorded, and retrying may record them twice. - **Never retry** other `4xx` errors unchanged; fix the request. The SDKs follow these rules: they retry idempotent requests twice by default, honor `Retry-After`, and never retry a `POST`. ## Timeouts Most requests answer in milliseconds. [Direct moderation](https://sigwise.ai/docs/guides/moderation.md) waits for the analyzer and can take seconds; give it a longer timeout. The server itself gives up on any request after 30 seconds. --- Source: https://sigwise.ai/docs/use-cases/marketplace-scam-detection.md # Marketplace scam detection Marketplace scams rarely show up in a single message. A buyer asks a normal question, then asks to move to WhatsApp, then offers to pay by wire "plus shipping", then sends a link to a fake payment page. Keyword filters catch the last step, if that. SigWise scores the **user**, over everything they have said and done, so the pattern is visible as it forms. ## What to send Send each user's activity as events on an object named after your user ID: - **Messages** between buyers and sellers, with the listing in `metadata`. - **Actions** that matter for fraud: sign-up, profile and payout changes, failed logins, listings created, reports by other users. - **Context** in `metadata`: account age, country, price, amount. ```json POST /v1/objects/user-42/events { "object_type": "user", "events": [ { "type": "event", "name": "account.created", "metadata": { "country": "NG" } }, { "type": "message", "content": "is this still available? I'm abroad, can I pay by wire and my courier picks up?", "metadata": { "listing": "bike-221", "price": 450 } }, { "type": "message", "content": "I sent $950 by mistake, please refund the difference to my cousin" } ] } ``` Ingestion returns `202` right away. Events that arrive within a few seconds of each other are analyzed together. See [Ingesting events](https://sigwise.ai/docs/guides/ingesting-events.md). ## The signals Every account starts with `is_scammer`. Make its criteria concrete for your marketplace. Describing the evidence steers the analyzer much more than words like "suspicious" do: ```json PUT /v1/signals/is_scammer { "type": "noul", "instructions": "Decide whether this user is trying to defraud other users of the marketplace.", "criteria": { "true": "asks to pay or talk off-platform, overpays and asks for a refund, sends payment or courier links, pressures for urgency, story does not add up", "false": "ordinary questions about the item, price negotiation, arranging pickup on the platform" } } ``` Add narrower signals when you want to act on them differently: ```json PUT /v1/signals/scam_type { "type": "choice", "instructions": "If this user is running a scam, which kind?", "criteria": { "none": null, "off_platform_payment": "moves payment outside the marketplace", "overpayment": "pays too much and asks for the difference back", "phishing": "sends links to fake payment or login pages", "account_takeover": "behaviour changes abruptly after a login from a new place" } } ``` See [Signals](https://sigwise.ai/docs/guide/signals.md) for the three types. ## Acting on the answers - **Block a message before it is delivered.** Send it with `"wait": true` and `"signals": ["is_scammer"]`, and hold it if the probability is high. See [Direct moderation](https://sigwise.ai/docs/guides/moderation.md). - **Review queue.** List the riskiest users first: `GET /v1/objects?q=is_scammer >= 80&sort=flagged`. See [Search and conditions](https://sigwise.ai/docs/guides/queries.md). - **Alert your team.** A [rule](https://sigwise.ai/docs/guides/rules.md) posts to Slack when a user crosses the line, once, not on every message: ```json POST /v1/rules { "name": "likely scammer", "when": "is_scammer >= 90", "action_type": "slack", "action_config": { "webhook_url": "https://hooks.slack.com/services/T000/B000/XXXX" } } ``` - **Automate.** Subscribe a [webhook](https://sigwise.ai/docs/guides/webhooks.md) to `analysis.completed` and freeze payouts or hide listings in your own code. ## Cost Each analysis is billed at its model cost, typically a fraction of a cent. A short marketplace conversation costs about $0.0001. Bursts of messages are folded into one analysis, so you pay per conversation turn, not per event. See [Billing and usage](https://sigwise.ai/docs/guides/billing.md). ## Next steps - [Quickstart](https://sigwise.ai/docs/guide/quickstart.md): your first signal, event and answer in five minutes. - [User trust scores](https://sigwise.ai/docs/use-cases/user-trust-scores.md): rank users by trustworthiness, not only flag the worst. --- Source: https://sigwise.ai/docs/use-cases/user-trust-scores.md # User trust scores A yes/no fraud flag tells you who to stop. A **trust score** tells you how far to let everyone else go: who can list without review, whose payouts clear instantly, who needs to verify first. SigWise keeps a score for every user, updated as they act, on a scale whose levels you describe in words. ## Define the score A `score` signal places each user on an ordered list of levels. The answer is a number from `0` (the first level) to `levels − 1` (the last), with fractions in between, plus a `confidence`. ```json PUT /v1/signals/trust_score { "type": "score", "instructions": "How much can other users and the platform trust this user?", "criteria": [ "high risk: deceptive, abusive or clearly fraudulent", "unproven: new or thin history, nothing wrong yet", "neutral: normal activity, minor issues", "trusted: long, consistent, positive history" ] } ``` Every new account already has a three-level `trust_score`; replace its levels with ones that match the decisions you make. ## Feed it evidence Trust is built from behaviour over time, so send the events a human reviewer would weigh: ```json POST /v1/objects/user-42/events { "object_type": "user", "events": [ { "type": "event", "name": "order.completed", "metadata": { "amount": 120, "rating": 5 } }, { "type": "event", "name": "report.received", "metadata": { "reason": "no-show", "reporter_trust": "trusted" } }, { "type": "message", "content": "Sorry, I was sick, can we reschedule for tomorrow?" } ] } ``` Each analysis sees the user's recent events plus a rolling summary of everything before, so one bad day does not erase a long good record, and a long record does not hide a sudden change. ## Read and use it ```json GET /v1/objects/user-42 { "object_id": "user-42", "analysis": [ { "key": "trust_score", "type": "score", "score": 2.4, "confidence": 0.8 } ] } ``` - **Gate actions in your code.** Instant payouts above `2.5`, manual review below `1`. - **Rank users.** `GET /v1/objects?sort=trust` lists the most trusted first. - **Find who to review.** `GET /v1/objects?q=trust_score < 1`. See [Search and conditions](https://sigwise.ai/docs/guides/queries.md). - **Get told when trust drops.** A [rule](https://sigwise.ai/docs/guides/rules.md) with `"when": "trust_score < 1"` fires once when a user falls below the line. ## Combine with other signals Conditions can combine signals, such as `is_scammer >= 60 and trust_score < 1`, so an alert only fires when a suspicious message comes from someone without a track record. ## Next steps - [Marketplace scam detection](https://sigwise.ai/docs/use-cases/marketplace-scam-detection.md) - [Signals](https://sigwise.ai/docs/guide/signals.md): the three signal types and how to write criteria. --- Source: https://sigwise.ai/docs/use-cases/buyer-intent.md # Buyer intent and lead scoring Lead scores built from page views and form fills miss what people actually say. "Do you ship to Norway?" and "can I pay by invoice for 40 seats?" are different leads. SigWise reads the messages and actions of each user or account and tells you where they are in a purchase, as one of the stages you name. ## Define the stages A `choice` signal picks one option and gives a probability for each: ```json PUT /v1/signals/buyer_intent { "type": "choice", "instructions": "Where is this user in a purchase?", "criteria": { "browsing": "looking around, no specific item or need", "comparing": "asks about specific items, alternatives or competitors", "ready_to_buy": "asks about payment, delivery, contracts or availability" } } ``` Every new account starts with this signal. For B2B, name the stages your sales team uses, and add a `noul` for the question you most want answered: ```json PUT /v1/signals/enterprise_fit { "type": "noul", "instructions": "Is this lead a fit for the enterprise plan?", "criteria": { "true": "many seats, SSO, security review, procurement, invoices", "false": "individual or small team" } } ``` ## Send the conversation Chat messages, support tickets, product events and CRM notes all count: ```json POST /v1/objects/lead-981/events { "object_type": "lead", "events": [ { "type": "event", "name": "pricing.viewed" }, { "type": "message", "content": "We're about 40 people, do you support SAML and can we pay by invoice?" } ] } ``` ## Route the hot leads ```json { "key": "buyer_intent", "type": "choice", "choice": "ready_to_buy", "probabilities": { "browsing": 0.05, "comparing": 0.15, "ready_to_buy": 0.8 }, "confidence": 0.8 } ``` - **Alert sales.** A [rule](https://sigwise.ai/docs/guides/rules.md) fires once when a lead becomes ready: `"when": "buyer_intent = ready_to_buy and enterprise_fit >= 70"`, to Slack or email. - **Sync to your CRM.** A [webhook](https://sigwise.ai/docs/guides/webhooks.md) on `analysis.completed` carries every answer; update the lead's stage from it. - **Build a list.** `GET /v1/objects?q=buyer_intent = ready_to_buy` returns every lead at that stage. ## Next steps - [Quickstart](https://sigwise.ai/docs/guide/quickstart.md) - [Reading results](https://sigwise.ai/docs/guides/reading-results.md) --- Source: https://sigwise.ai/docs/use-cases/chat-moderation.md # Chat and content moderation Most moderation APIs score a message in isolation, against a fixed list of harm categories. Your policy is usually more specific than that ("no sharing phone numbers before a booking", "no reviews from people who never ordered"), and the same words can be fine from one user and a red flag from another. With SigWise you write the policy as signals, and every message is judged together with its author's history. ## Write your policy as signals ```json PUT /v1/signals/policy_violation { "type": "choice", "instructions": "Does this user's latest content break the community rules?", "criteria": { "none": null, "contact_sharing": "shares phone numbers, emails or social handles to move off-platform", "harassment": "insults, threats or targeted abuse of another user", "spam": "repeated promotion, links or copy-pasted messages", "prohibited_item": "offers weapons, drugs or counterfeit goods" } } ``` One signal per decision keeps each answer sharp. See [Writing good signals](https://sigwise.ai/docs/guide/signals.md#writing-good-signals). ## Check before publishing Send the message with `"wait": true` and only the signals the decision needs. The API records it, scores the author inline, and returns the verdict: ```json POST /v1/objects/user-42/events { "wait": true, "signals": ["policy_violation"], "events": [{ "type": "message", "content": "text me on 555 0100, cheaper outside the app" }] } ``` ```json { "object_id": "user-42", "analyzed": true, "answers": [ { "signal": "policy_violation", "type": "choice", "choice": "contact_sharing", "probabilities": { "none": 0.03, "contact_sharing": 0.94, "harassment": 0.01, "spam": 0.01, "prohibited_item": 0.01 } } ] } ``` Publish, hold for review or reject based on the answer. Set `"include_history": false` to judge only the new content. See [Direct moderation](https://sigwise.ai/docs/guides/moderation.md#failure-handling) for what to do when a check fails. ## Moderate in the background too Not everything needs a blocking check. Send the rest of the activity asynchronously, and let [rules](https://sigwise.ai/docs/guides/rules.md) alert your team when a user's behaviour crosses a line over time. Rules fire after inline and background analyses alike. ## Next steps - [Direct moderation](https://sigwise.ai/docs/guides/moderation.md) - [SigWise vs. moderation APIs](https://sigwise.ai/docs/compare/moderation-apis.md) --- Source: https://sigwise.ai/docs/compare/moderation-apis.md # SigWise vs. moderation APIs Content moderation APIs, such as OpenAI's Moderation endpoint, take one piece of content and return scores for a fixed set of harm categories: hate, harassment, self-harm, sexual content, violence and the like. They are fast, cheap and a good baseline for keeping obviously harmful content off a platform. SigWise answers a different question. Instead of *is this text harmful?*, it answers the questions **you** define, about a **user, listing or order**, over **everything they have done**. ## At a glance | | Moderation APIs | SigWise | |---|---|---| | Unit judged | One piece of content | An object (user, listing, order) and its history | | Questions | A fixed list of harm categories | Any question you write, in plain language | | Answer | A score per category | Typed: a yes/no probability, a labelled score, or one of your categories | | Context | The content you send | Recent events and messages plus a rolling summary of the rest | | Inputs | Text (some also accept images) | Messages and structured events (logins, payouts, reports) with metadata | | Timing | Synchronous | Background by default, or inline with `"wait": true` | | Follow-up | Your code | Search by answers, signed webhooks, and rules that alert Slack, email or a webhook | | Changing the policy | Not possible; categories are fixed | Edit a signal with one API call | ## Where SigWise is the better fit - **Fraud and scams.** "Can I pay by wire?" is harmless as text. It matters when it comes from a new account that just asked to move to WhatsApp. See [Marketplace scam detection](https://sigwise.ai/docs/use-cases/marketplace-scam-detection.md). - **Platform-specific rules.** "No phone numbers before a booking" or "no reselling tickets above face value" aren't harm categories. Write them as a signal. See [Chat and content moderation](https://sigwise.ai/docs/use-cases/chat-moderation.md). - **Questions that aren't about harm at all**, such as trust, churn risk or buying intent. See [User trust scores](https://sigwise.ai/docs/use-cases/user-trust-scores.md) and [Buyer intent](https://sigwise.ai/docs/use-cases/buyer-intent.md). ## Where a moderation API is enough If you only need to screen single messages for generic harmful content, a moderation API is simpler and often free. The two also combine well: screen every message with a moderation API, and send the conversation to SigWise to judge the people behind it. ## Try it Every new account starts with $5 of free credit and three signals (`is_scammer`, `trust_score`, `buyer_intent`). The [Quickstart](https://sigwise.ai/docs/guide/quickstart.md) takes about five minutes. --- Source: https://sigwise.ai/docs/compare/build-it-yourself.md # SigWise vs. building on an LLM API Calling an LLM with "is this user a scammer? here are their messages" works in a notebook. In production, the prompt is the easy part. This page lists what else you would build, so you can decide whether it is worth it. ## What you would build | Piece | Doing it yourself | In SigWise | |---|---|---| | **Event storage** | A table of events per user, with retention and limits | `POST /v1/objects/{id}/events`. See [Ingesting events](https://sigwise.ai/docs/guides/ingesting-events.md). | | **History that fits a context window** | Truncation or summarization of long histories, kept up to date | Recent events plus a rolling summary of everything before | | **Debouncing** | Coalesce a burst of messages into one call, or pay per message | Built in: events a few seconds apart share one analysis | | **Typed, reliable outputs** | JSON schemas, parsing, retries on malformed output, calibrated probabilities | Three typed [signal](https://sigwise.ai/docs/guide/signals.md) shapes with probabilities and confidence | | **Changing questions** | Prompt changes, deploys, and re-running old users | Edit a signal with one call; [backfill](https://sigwise.ai/docs/guides/backfills.md) existing objects | | **Queues and priorities** | Workers, rate limits, and keeping bulk re-runs from delaying live traffic | Live traffic is served ahead of bulk work | | **Inline checks** | A synchronous path with its own timeouts | `"wait": true`. See [Direct moderation](https://sigwise.ai/docs/guides/moderation.md). | | **Querying answers** | Store answers, index them, write a query layer | `GET /v1/objects?q=is_scammer >= 90`. See [Search](https://sigwise.ai/docs/guides/queries.md). | | **Notifications** | Webhooks with signing, retries and a dead-letter queue; Slack and email alerts that don't repeat | [Webhooks](https://sigwise.ai/docs/guides/webhooks.md) and [rules](https://sigwise.ai/docs/guides/rules.md) | | **Cost tracking** | Token accounting per feature and per customer | Usage by day, model, signal and source. See [Billing](https://sigwise.ai/docs/guides/billing.md). | | **Console** | An internal tool to inspect users and answers | Included | ## When building it yourself makes sense - You need a model or deployment SigWise doesn't offer, such as a model you host on your own hardware. - Scoring users is your core product and you want to own every part of it. - Your volume is large enough that a dedicated team is cheaper than any per-analysis price. ## When SigWise makes sense - You want answers this week, not a quarter from now. - Trust and safety, fraud or lead scoring supports your product rather than being the product. - You want non-engineers to change the questions without a deploy. Analyses are billed at their model cost, with no subscription or minimum, so the comparison is mostly about engineering time. See [Billing and usage](https://sigwise.ai/docs/guides/billing.md). ## Try it Every new account starts with $5 of free credit. The [Quickstart](https://sigwise.ai/docs/guide/quickstart.md) takes about five minutes. --- Source: https://sigwise.ai/docs/reference.md # 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 `: 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 ` 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": ""}` 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 --- Source: https://sigwise.ai/docs/sdks.md # SDKs Official clients are generated from the [OpenAPI specification](https://sigwise.ai/docs/guide/openapi.md), so every endpoint and schema in the [API reference](https://sigwise.ai/docs/reference.md) is available, typed, in each language. | Language | Package | Requires | |----------|---------|----------| | Node.js / TypeScript | `@sigwise/sdk` | Node 18+, no dependencies | | Python | `sigwise-sdk` | Python 3.8+, no dependencies | | Go | `github.com/sigwise/sigwise-go` | Go 1.21+, standard library only | | PHP | `sigwise/sdk` | PHP 8.1+ with `ext-curl` | > **Coming soon** > > The SDKs are on their way to npm, PyPI, the Go module proxy and Packagist. > Until then, call the API directly: see [Authentication](https://sigwise.ai/docs/guide/authentication.md) > for how to sign requests. ## What every SDK does - **Signs every request** with your key's secret: a fresh 60-second HS256 token bound to the request's method and path. The secret is never sent. - **Reads configuration from the environment:** `ANALYZE_API_KEY`, `ANALYZE_SECRET` and `ANALYZE_BASE_URL`. - **Retries safely:** idempotent requests (`GET`, `PUT`, `DELETE`) are retried after network errors, `429` and `5xx`, with exponential backoff; `POST` requests never are. - **Raises typed errors** with the HTTP status, the error `code` and message. - **Verifies webhooks:** a helper checks `X-Webhook-Signature` and the timestamp and parses the payload. ## Same API, idiomatic in each language Resources and methods follow the API: `objects.get`, `events.ingest`, `signals.upsert`, `webhooks.create` and so on. Node.js: ```ts import { SigWise } from "@sigwise/sdk"; const sigwise = new SigWise({ apiKey: "your_key_id", secret: "your_secret" }); const object = await sigwise.objects.get("user-42"); const page = await sigwise.objects.list({ q: "is_scammer >= 90", sort: "flagged" }); ``` Python: ```python from sigwise import SigWise sigwise = SigWise(api_key="your_key_id", secret="your_secret") obj = sigwise.objects.get("user-42") page = sigwise.objects.list(q="is_scammer >= 90", sort="flagged") ``` Go: ```go client := sigwise.New("your_key_id", "your_secret") obj, err := client.Objects.Get(ctx, "user-42") page, err := client.Objects.List(ctx, &sigwise.ListObjectsParams{Q: sigwise.String("is_scammer >= 90")}) ``` PHP: ```php $sigwise = new SigWise\Client('your_key_id', 'your_secret'); $object = $sigwise->objects->get('user-42'); $page = $sigwise->objects->list(['q' => 'is_scammer >= 90', 'sort' => 'flagged']); ``` Each generated package has a README with installation, configuration, error handling, webhook verification and a reference of every method. ## Other languages Anything that can compute an HMAC-SHA256 can call the API. See [Authentication](https://sigwise.ai/docs/guide/authentication.md) for signing examples, and point any OpenAPI tool at the [specification](https://sigwise.ai/docs/guide/openapi.md). --- Source: https://sigwise.ai/docs/guide/openapi.md # OpenAPI specification The SigWise API is described by an [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) document. It is the single source of truth for the API contract: - the [API reference](https://sigwise.ai/docs/reference.md) on this site is rendered from it; - the [SDKs](https://sigwise.ai/docs/sdks.md) are generated from it; - the server's test suite fails when a route is added without being described in it, so it can't fall behind. ## Download | Where | | |-------|---| | This site | [`/openapi.yaml`](https://sigwise.ai/docs/openapi.yaml) · [`/openapi.json`](https://sigwise.ai/docs/openapi.json) | | The API itself | `https://api.sigwise.ai/openapi.yaml` · `https://api.sigwise.ai/openapi.json` | | Source | `api/openapi.yaml` in the repository | The API serves the specification of the version it runs, so a local server at `http://localhost:8080/openapi.yaml` always matches your checkout. ## Use it with your tools - **API clients.** Import the URL into Postman, Insomnia, Bruno or Hoppscotch to get every request pre-filled. Requests need a signed token; see [Authentication](https://sigwise.ai/docs/guide/authentication.md). - **Mock servers.** `npx @stoplight/prism-cli mock https://api.sigwise.ai/openapi.yaml` serves example responses for offline development. - **Type generation.** `npx openapi-typescript https://api.sigwise.ai/openapi.yaml -o analyze.d.ts` gives you request and response types in any TypeScript project. ## Webhooks The document also lists the webhooks the API sends (`analysis.completed` and `rule.triggered`) under `webhooks`, with their payload schemas. --- Source: https://sigwise.ai/docs/guide/agents.md # For AI agents These docs are built to be read by coding agents and LLM tools as well as people. ## Markdown for every page Add `.md` to any page's URL to get it as plain Markdown, with code examples and links to other pages' Markdown: ``` https://sigwise.ai/docs/guide/quickstart → HTML https://sigwise.ai/docs/guide/quickstart.md → Markdown ``` Every HTML page also advertises its Markdown in its head: ```html ``` Each page has buttons to copy its Markdown or open it in Claude or ChatGPT. The API reference has a Markdown version too, /docs/reference.md, generated from the OpenAPI specification. ## llms.txt | File | Contains | |------|----------| | [`/llms.txt`](https://sigwise.ai/docs/llms.txt) | An index of every page, with a one-line description and its Markdown URL ([llmstxt.org](https://llmstxt.org)). Also at the domain root, `https://sigwise.ai/llms.txt`. | | [`/llms-full.txt`](https://sigwise.ai/docs/llms-full.txt) | Every page, including the API reference, in one file. Give it to an agent as context. | `llms.txt` starts with a short summary of the product and its pricing. The landing page has a Markdown version too, at https://sigwise.ai/index.md. ## The API contract For code generation and tool calling, use the OpenAPI 3.1 specification rather than prose: [`/docs/openapi.json`](https://sigwise.ai/docs/openapi.json) or [`/docs/openapi.yaml`](https://sigwise.ai/docs/openapi.yaml). The API serves the one for the version it runs at `/openapi.json`. See [OpenAPI specification](https://sigwise.ai/docs/guide/openapi.md). ## Tips for agents writing integrations - Authentication needs a fresh HS256 JWT per request, signed with the API key's secret; see [Authentication](https://sigwise.ai/docs/guide/authentication.md). Prefer an [SDK](https://sigwise.ai/docs/sdks.md), which does this for you. - Request bodies are decoded strictly: unknown fields are a `400`. - Never retry `POST /v1/objects/{object_id}/events` blindly; it isn't idempotent. See [Errors and retries](https://sigwise.ai/docs/guides/errors.md). --- Source: https://sigwise.ai/docs/changelog.md # Changelog New features and changes to the SigWise API, console and docs, newest first. ## 2026-09-29 - **Safe retries for ingestion.** Send an `Idempotency-Key` header with `POST /v1/objects/{object_id}/events` and retry with the same key: the events are recorded once. See [Ingesting events](https://sigwise.ai/docs/guides/ingesting-events.md#retrying-safely). - **Two-factor authentication** for console sign-in with an authenticator app, plus recovery codes. Repeated failed sign-ins are now temporarily locked out. - Every API response carries an `X-Request-Id` header; quote it when you contact support. ## 2026-09-27 - **Built for more traffic.** Accounts share the analysis workers fairly, so one account's backlog no longer delays everyone else's answers. Objects that receive a steady stream of events are still analyzed at least once a minute, and an object is never analyzed twice at the same time. - **Rate limits.** `/v1` requests are limited per account, and throttled requests get `429` with a `Retry-After` header. A `wait: true` request the analyzer can't take right now gets `503 analyzer_busy`. See [Errors and retries](https://sigwise.ai/docs/guides/errors.md). - **Cursor pagination** for the object list: pass `next_cursor` back as `cursor` for pages that stay fast however deep you go. - **Docs, OpenAPI and SDKs.** These docs, an [OpenAPI 3.1 specification](https://sigwise.ai/docs/guide/openapi.md) that a test keeps in step with the server, an [API reference](https://sigwise.ai/docs/reference.md) rendered from it, and generated [SDKs](https://sigwise.ai/docs/sdks.md) for Node.js, Python, Go and PHP. Every page is available as Markdown, with [llms.txt](https://sigwise.ai/docs/guide/agents.md) for AI agents. - **Email notifications** for account events: welcome, new sign-ins, password and API key changes, low and empty balance, top-up receipts, and alerts from [rules](https://sigwise.ai/docs/guides/rules.md). - **Backfills.** Answer a new signal for objects analyzed before it existed, one signal at a time or automatically for every new signal. See [Backfills](https://sigwise.ai/docs/guides/backfills.md). - **Signed requests.** API requests carry a short-lived JWT signed with your key's secret and bound to the request's method and path, so the secret never leaves your server. See [Authentication](https://sigwise.ai/docs/guide/authentication.md). - **Password reset** and change password in the console. ## 2026-09-26 - **Sign in with Google or GitHub**, and link either to an existing account. - **Default signals.** Every new account starts with `is_scammer`, `trust_score` and `buyer_intent`, so the first events already get answers. - **Card top-ups** and a **Transactions** screen itemizing every charge. - **Help & docs** inside the console. ## 2026-09-25 - **Usage screen**: analyses, spend and tokens by day, model, signal and source. ## 2026-09-22 - **Cost-based billing.** Each analysis is billed at its actual model cost from a prepaid balance, and every new account starts with free credit. See [Billing and usage](https://sigwise.ai/docs/guides/billing.md). - **Priority queues.** Live traffic is analyzed ahead of bulk work, so a large re-analysis never delays real events. ## 2026-09-21 - **Rules** that send an email, a Slack message or a webhook when a condition over your signals becomes true. See [Rules](https://sigwise.ai/docs/guides/rules.md). - **Webhooks** with HMAC-SHA256 signatures, retries with backoff, a dead-letter queue and replay. See [Webhooks](https://sigwise.ai/docs/guides/webhooks.md). - **API keys** you can create, rotate and revoke from the console. - **Console accounts**: sign up and sign in to manage signals, objects, rules and webhooks.