diff --git a/api/errors.mdx b/api/errors.mdx index 33461fc..752dd40 100644 --- a/api/errors.mdx +++ b/api/errors.mdx @@ -68,7 +68,7 @@ Use `loc` to locate the field — the last segment is the field name. | 422 | `Invalid callback URL: ` | [Update org settings](/api/org/update-settings) when `default_callback_url` resolves to a private/internal address. | | 404 | `Scan not found` | [Get scan](/api/scans/get). | | 404 | `Batch not found` | [Get batch](/api/scans/batch-get). | -| 404 | `Org settings not found` | Org settings or usage endpoints. | +| 404 | `Org settings not found` | Org settings endpoints. | | 404 | `API key not found or already revoked` | [Revoke API key](/api/keys/revoke). | | 400 | `No fields to update` | [Update org settings](/api/org/update-settings). | | 400 | `Invalid recommendation values: [...]` | [List org scans](/api/org/list-scans). | diff --git a/api/scans/get.mdx b/api/scans/get.mdx index dda62df..54a702e 100644 --- a/api/scans/get.mdx +++ b/api/scans/get.mdx @@ -117,19 +117,10 @@ The response is the scan record. Fields you should rely on: - Per-URL evidence the contextual model cited. See + Per-URL evidence Tumban cited in support of the decision. See [Evidence index](/concepts/evidence-index). - - Internal per-strategy scores: `blocklist`, `content_safety`, `llm`. - Useful for debugging unexpected recommendations. - - - - Whether the judge model was invoked to resolve a borderline score. - - ## Coverage The scan record's `triage_report` does **not** include `coverage` — @@ -167,8 +158,6 @@ curl https://api.tumban.com/api/v2/scans/550e8400-e29b-41d4-a716-446655440000 \ "reason_summary": "Direct link to a prohibited platform combined with adult keywords in bio.", "review_targets": ["https://prohibited-platform.example/username"], "link_chain": "Profile -> External site", - "strategy_scores": {"blocklist": 50, "content_safety": 0, "llm": 85}, - "judge_model_invoked": false, "evidence_index": [ { "ref": "link_1", @@ -182,10 +171,7 @@ curl https://api.tumban.com/api/v2/scans/550e8400-e29b-41d4-a716-446655440000 \ ``` - The response also includes internal fields not listed above — - `canonical_url`, `username`, `platform`, `last_scanned_at`, `bio`, - `profile_image_url`, `banner_image_url`, `social_links`, - `direct_links`, `blob_references`, `updated_at`, and `org_id`. These + The response also includes internal fields not listed above. These reflect the underlying scan record and are subject to change. Do not rely on their names, types, or presence; treat them as opaque. @@ -208,7 +194,5 @@ read-only sections: codes, reason summary. - **Coverage** — which analysis steps ran, login-blocked URLs, referrer match counts. -- **Strategy scores** — internal per-strategy scores. Useful for - debugging an unexpected recommendation. - **Raw JSON** — collapsible *"Show full document"* panel that exposes the entire scan record. diff --git a/api/usage/priority.mdx b/api/usage/priority.mdx deleted file mode 100644 index 0f35c05..0000000 --- a/api/usage/priority.mdx +++ /dev/null @@ -1,71 +0,0 @@ ---- -title: "Get priority distribution" -description: "Recommendation breakdown across the last 30 days." -icon: "chart-pie" ---- - -{/* sources: src/api/routes_auth.py:get_usage_priority, src/services/usage_aggregations.py:compute_priority_distribution */} - -Return how the organization's recommendations have been distributed -across the last 30 days of completed scans. Useful for review-queue -sizing and reporting. - -```http -GET /api/v2/org/usage/priority -``` - -## Response - - - Object keyed by recommendation value. Always includes every - recommendation even when zero, plus an `unknown` bucket for - completed scans missing a recommendation. - - - - Sum of all bucket counts. - - - - Always `30`. - - - - ISO 8601 UTC timestamp of when the data was generated. - - - - `cache` if served from the warmer cache, `live` if computed on the - fly. - - -### `counts` keys - -- `no_flags` -- `review_low` -- `review_medium` -- `review_high` -- `unknown` - -## Example - -```bash -curl https://api.tumban.com/api/v2/org/usage/priority \ - -H "Authorization: Bearer sk_xxx" -``` - -```json -{ - "counts": { - "no_flags": 812, - "review_low": 167, - "review_medium": 92, - "review_high": 41, - "unknown": 0 - }, - "total": 1112, - "window_days": 30, - "updated_at": "2026-04-29T12:00:00.123456+00:00", - "source": "cache" -} -``` diff --git a/api/usage/scans.mdx b/api/usage/scans.mdx deleted file mode 100644 index c874178..0000000 --- a/api/usage/scans.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: "Get scan timeseries" -description: "Daily or monthly scan counts for the organization." -icon: "chart-line" ---- - -{/* sources: src/api/routes_auth.py:get_usage_scans, src/services/usage_aggregations.py:compute_daily_scan_counts,compute_monthly_scan_counts */} - -Return per-day or per-month scan counts, broken down by terminal -status. Useful for charting throughput over time. - -```http -GET /api/v2/org/usage/scans -``` - -## Query parameters - - - `daily` returns the last 30 days. `monthly` returns the last 12 - months. - - -## Response - - - Echoes the `range` you requested. - - - - One entry per day or month. Missing periods are filled with zeros so - the series is continuous. - - - - ISO 8601 UTC timestamp of when the data was generated. - - - - `cache` if served from the warmer cache, `live` if computed on the - fly. - - -### `points[]` - - - ISO 8601 UTC timestamp pinned to `T00:00:00Z`. For monthly ranges, - always the first of the month. - - - - Scans submitted in this period (sum of the status-broken-out - fields). - - - - Scans whose final status was `completed`. - - - - Scans whose final status was `completed_with_partial`. - - - - Scans whose final status was `failed`. - - - - Scans still in flight at the time the response was generated. - - -## Example - -```bash -curl "https://api.tumban.com/api/v2/org/usage/scans?range=daily" \ - -H "Authorization: Bearer sk_xxx" -``` - -```json -{ - "range": "daily", - "points": [ - { - "date": "2026-04-29T00:00:00.123456+00:00", - "total": 42, - "completed": 40, - "completed_with_partial": 1, - "failed": 1, - "processing": 0 - } - ], - "updated_at": "2026-04-29T12:00:00.654321+00:00", - "source": "cache" -} -``` diff --git a/api/usage/totals.mdx b/api/usage/totals.mdx deleted file mode 100644 index 555b25d..0000000 --- a/api/usage/totals.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "Get usage totals" -description: "Lifetime scan counters for the organization." -icon: "chart-simple" ---- - -{/* sources: src/api/routes_auth.py:get_usage */} - -Return lifetime scan counters for the authenticated organization. - -```http -GET /api/v2/org/usage -``` - -## Response - - - Lifetime count of scans that produced a triage report (status - `completed` or `completed_with_partial`). - - - - Lifetime count of scans that did not produce a triage report - (failures and timeouts). - - -## Example - -```bash -curl https://api.tumban.com/api/v2/org/usage \ - -H "Authorization: Bearer sk_xxx" -``` - -```json -{ - "total_scans_completed": 1284, - "total_scans_dropped": 7 -} -``` - -## Errors - -| Status | Detail | -|--------|--------| -| 404 | `Org settings not found`. | - -## Using the dashboard - -The **Usage** page reads from this and the related usage endpoints to -render: - -- Three stat tiles — **Total scans (lifetime)**, **Last 30 days**, and - **Today**. -- A bar chart **Scans over time** with a toggle between - **Daily (30d)** and **Monthly (12m)**. Powered by - [Get scan timeseries](/api/usage/scans). -- A donut chart **Priority distribution — last 30 days** broken down - by recommendation. Powered by - [Get priority distribution](/api/usage/priority). - -The captions under each chart show *"Updated X min ago · cache"* or -*"… · live"*, indicating whether the response came from the -pre-aggregated cache or a live database query. - - - The Usage page in the dashboard is admin-only. The underlying API - endpoints are not role-gated — members can call them directly. - diff --git a/api/usage/warmer-health.mdx b/api/usage/warmer-health.mdx deleted file mode 100644 index e8c8bcb..0000000 --- a/api/usage/warmer-health.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: "Get warmer health" -description: "Liveness probe for the usage stats pre-aggregator." -icon: "heart-pulse" ---- - -{/* sources: src/api/routes_auth.py:get_warmer_health */} - -Tumban pre-computes the timeseries powering -[Get scan timeseries](/api/usage/scans) and -[Get priority distribution](/api/usage/priority) in a background -worker. This endpoint reports whether each loop is alive. - -```http -GET /api/v2/org/health/warmer -``` - -## Response - - - Status of the scan-counts aggregation loop. - - - - Status of the priority-distribution aggregation loop. - - -### Each entry - - - `true` if the loop has written its heartbeat key recently. When - `false`, usage endpoints fall back to live aggregation and may be - slower. - - - - ISO 8601 UTC timestamp of the last heartbeat. `null` if no - heartbeat has been seen. - - -## Example - -```bash -curl https://api.tumban.com/api/v2/org/health/warmer \ - -H "Authorization: Bearer sk_xxx" -``` - -```json -{ - "scans": {"alive": true, "last_seen": "2026-04-29T12:00:00.123456+00:00"}, - "priority": {"alive": true, "last_seen": "2026-04-29T11:45:00.654321+00:00"} -} -``` diff --git a/authentication.mdx b/authentication.mdx index 8d0830d..62b3a00 100644 --- a/authentication.mdx +++ b/authentication.mdx @@ -64,6 +64,15 @@ Tumban only stores the SHA-256 hash of an API key. The raw `sk_…` value is returned exactly once at creation time. If you lose it, revoke the key and create a new one. +## Organization identifier + +Every authenticated request resolves to exactly one `org_id`. Tumban +treats it as an **opaque, case-sensitive string** — do not regex-match +it against a hex or numeric alphabet. The leading `org_` prefix is the +only guaranteed part of the shape. Examples (`org_2abc...`) in this +reference are illustrative; the body is alphanumeric and may include +mixed case. + ## Errors | Status | Meaning | diff --git a/concepts/confidence.mdx b/concepts/confidence.mdx index 8c5115a..4d8cc5b 100644 --- a/concepts/confidence.mdx +++ b/concepts/confidence.mdx @@ -4,47 +4,29 @@ description: "How confident Tumban is in a recommendation." icon: "gauge-simple-high" --- -{/* sources: src/services/aggregator.py:_determine_confidence */} - Every triage report includes a `confidence` field with one of three -values. Each value is produced by a specific set of triggers: +values: -| Value | Triggered by (any one) | -|-------|------------------------| -| `high` | Blocklist score `≥ 85`; **or** two or more strategies returned a non-zero score; **or** aggregated score is `0` (the `no_flags` fast-path always reports `high`). | -| `medium` | Aggregated score `≥ 60` from a single non-zero strategy; **or** the contextual model itself reported `confidence: "high"`. | -| `low` | The judge model was invoked and reported `low`; **or** a single strategy fired with a score `> 0` but none of the higher triggers matched. | +| Value | What it means | +|-------|---------------| +| `high` | Tumban has strong, corroborated evidence backing the recommendation. Act on it without further qualification. | +| `medium` | Tumban has solid signal but less corroboration. Useful, but a manual reviewer may want to spot-check on edge cases. | +| `low` | Tumban surfaced something worth looking at, but the signal is thin or borderline. Treat as a review-worthy lead, not a verdict. | `confidence` is **never `low` when `recommendation` is `no_flags`** — -the score-`0` fast-path always returns `high`. Don't gate review work -on `confidence: low` for clean profiles; it can't happen. - -## How it's derived - -Tumban determines confidence after aggregation. Rules are evaluated **in -order**; the first matching rule wins: - -1. **Strong deterministic match** — blocklist strategy score `≥ 85` - → `high`. The blocklist is exact and fast, so a hit is treated as - high confidence on its own. -2. **Multiple strategies agree** — two or more of (blocklist, - content safety, contextual model) returned a non-zero score - → `high`. -3. **Single strong strategy** — aggregated score `≥ 60` from a single - non-zero strategy → `medium`. One signal pointing this high is - meaningful even without corroboration. -4. **Contextual model is confident** — the contextual model reports - its own `confidence` as `high` → `medium`. -5. **Borderline case adjudicated by the judge** — the judge model was - invoked → result inherits the judge's own `confidence_level` - (`low` or `medium`). -6. **Single weak signal** — aggregated score `> 0` with none of the - above → `low`. -7. **No flags detected** — aggregated score is `0` → `high` confidence - in `no_flags`. +clean profiles always come back with `high` confidence in the +`no_flags` decision. Don't gate review work on that combination; it +cannot occur. ## When to use it -Use `confidence` to weight review priority within a recommendation tier. -A `review_high` with `confidence: "high"` is worth processing before a -`review_high` with `confidence: "low"`. +Use `confidence` to weight review priority **within** a recommendation +tier. A `review_high` with `confidence: "high"` is worth processing +before a `review_high` with `confidence: "low"`. Within a queue sorted +by `risk_score`, breaking ties on `confidence` is a reasonable second +key. + +How Tumban arrives at each level is not part of the public contract — +the exact triggers may change as detection is tuned. Treat `confidence` +as an interpretation of the same evidence summarised by `reason_codes` +and `evidence_index`, not as an independent signal. diff --git a/concepts/evidence-index.mdx b/concepts/evidence-index.mdx index b7ba2b4..fd251d6 100644 --- a/concepts/evidence-index.mdx +++ b/concepts/evidence-index.mdx @@ -4,8 +4,6 @@ description: "Per-URL evidence the triage report cites." icon: "list-tree" --- -{/* sources: src/services/strategies/llm_decision.py, docs/API_FIELD_REFERENCE.md */} - `evidence_index` is an array of structured entries representing the URLs and external context the triage report cites. Use it to render reviewer-facing panels without parsing the free-text `reason_summary`. @@ -23,10 +21,10 @@ Every entry has: - When the contextual model cited the entry by reference (`link_3`, + When Tumban cited the entry by reference (`link_3`, `external_mention_2`, …), this field correlates the entry to the matching token inside `reason_summary` — use it to highlight the - cited URL alongside the model's prose. + cited URL alongside the prose summary. Type-specific fields: @@ -102,7 +100,7 @@ Type-specific fields: ## Empty array -`evidence_index` can be `[]` — for example, when only the deterministic -strategies fired, or when the contextual model hit an infrastructure -error. Treat an empty array as "no contextual evidence to surface", not -as a feature failure. +`evidence_index` can be `[]` even on a flagged scan — Tumban may reach +a decision from signals that don't have a per-URL citation to surface. +Treat an empty array as "no per-URL evidence to render", not as a +feature failure. diff --git a/concepts/recommendations.mdx b/concepts/recommendations.mdx index de0ad4f..32662b4 100644 --- a/concepts/recommendations.mdx +++ b/concepts/recommendations.mdx @@ -4,8 +4,6 @@ description: "How a 0–100 risk score maps to one of four recommendation tiers. icon: "gauge" --- -{/* sources: src/services/aggregator.py, src/models/profile.py, docs/API_FIELD_REFERENCE.md */} - Every completed scan returns a `recommendation` and a `risk_score` (0–100). The recommendation is derived from the score: @@ -19,35 +17,26 @@ The recommendation is derived from the score: See [Recommendation values](/reference/recommendation) for the canonical reference. -## How the score is produced - -Tumban runs three independent detection strategies in parallel and takes -the maximum of their scores. A judge model is invoked on borderline -aggregated scores (11–70) and may bump the score up or down based on -contextual analysis: - -- **`bump_up`** — the judge promotes a borderline score to at least - `51` (i.e. into the `review_medium` band). If the original score was - already higher, it is left unchanged. -- **`bump_down`** — the judge demotes the score to the contextual - model's own score, falling back to `20` when the contextual model - reported nothing. This is how the judge de-escalates likely false - positives (e.g. a brand mention misread as a creator profile). -- **`keep_score`** — the aggregated score is used as-is. +## What the score means -When a `bump_up` or `bump_down` fires, the resulting `risk_score` may -not map back to any single strategy's score — `strategy_scores` will -still show the raw per-strategy values. Use `judge_model_invoked: true` -to detect that adjudication happened. +`risk_score` reflects Tumban's **confidence that a policy violation is +present**, not uncertainty. A score of `0` means nothing in the profile +or its external footprint triggered a signal; `100` means the evidence +is overwhelming. Missing or unreachable data does not push the score +up — partial coverage is recorded transparently in the +[`coverage`](/concepts/coverage) object, never as inflated risk. -The score reflects **confidence that a violation exists**, not -uncertainty. Missing data does not raise the score. +The exact internals that produce the score are not part of the public +contract and may change without notice. Build against the published +fields — `risk_score`, `recommendation`, `confidence`, +`reason_codes`, `reason_summary`, `review_targets`, `evidence_index`, +and `coverage` — and treat any other field on the response as opaque. ## What `no_flags` does and doesn't mean `no_flags` means Tumban's automated analysis did not detect a violation. -It does **not** prove a profile is clean — your manual review process may -still catch something the pipeline missed. +It does **not** prove a profile is clean — your manual review process +may still catch something automation missed. ## Integration tips diff --git a/dashboard-overview.mdx b/dashboard-overview.mdx index cdfad82..0e7dcad 100644 --- a/dashboard-overview.mdx +++ b/dashboard-overview.mdx @@ -24,9 +24,9 @@ The dashboard has three primary regions: | Item | What it covers | API pages | |------|----------------|-----------| -| **Home** | Overview dashboard with **Needs your attention** and **Recent scans** panels, plus stat tiles. | [List org scans](/api/org/list-scans), [Get usage totals](/api/usage/totals) | +| **Home** | Overview dashboard with **Needs your attention** and **Recent scans** panels, plus stat tiles. | [List org scans](/api/org/list-scans) | | **Scan** | Submit a single profile URL, view your local submission history (persisted in your browser). | [Create scan](/api/scans/create), [Get scan](/api/scans/get) | -| **Usage** | Bar chart and donut chart for scan throughput and recommendation distribution. Admin-only in the dashboard. | [Get scan timeseries](/api/usage/scans), [Get priority distribution](/api/usage/priority), [Get usage totals](/api/usage/totals) | +| **Usage** | Bar chart and donut chart for scan throughput and recommendation distribution. Admin-only in the dashboard. Usage data is dashboard-only — there is no public API to read it. | (Dashboard-only.) | | **API Keys** | Create, list, and revoke API keys. | [Create API key](/api/keys/create), [List API keys](/api/keys/list), [Revoke API key](/api/keys/revoke) | | **Webhooks** | Set the default callback URL and rotate the webhook secret. | [Update org settings](/api/org/update-settings), [Rotate webhook secret](/api/webhook-secret/rotate) | | **Organisation** | Manage members and organization profile. Admin-only — hidden for members. | (Not exposed on the API.) | @@ -50,7 +50,7 @@ documented on each endpoint page. | Revoke API key | Trash icon visible to all members; the API rejects revocations a member is not authorized for | Members revoke their own keys; admins revoke any | | Set default callback URL | Hidden for non-admins | Admin-only | | Rotate webhook secret | Hidden for non-admins | Admin-only | -| View Usage page | Members see "Admin only" notice | API itself is not role-gated; members can call directly | +| View Usage page | Members see "Admin only" notice | (Dashboard-only; not on the public API.) | | View Organisation page | Hidden for non-admins | (Not on the API.) | ## Sign-in diff --git a/docs.json b/docs.json index d6ae8eb..3171454 100644 --- a/docs.json +++ b/docs.json @@ -67,15 +67,6 @@ "api/webhook-secret/rotate" ] }, - { - "group": "Usage", - "pages": [ - "api/usage/totals", - "api/usage/scans", - "api/usage/priority", - "api/usage/warmer-health" - ] - }, "api/errors" ] }, diff --git a/index.mdx b/index.mdx index 05dd60f..9485e40 100644 --- a/index.mdx +++ b/index.mdx @@ -13,9 +13,8 @@ behind the decision. ## How it works -Submit a profile URL. Tumban runs the URL through its scraping, web search, -link traversal, and multi-strategy detection pipeline and returns the result -either by webhook or by polling. +Submit a profile URL. Tumban analyses the profile and its external +footprint, then returns the result either by webhook or by polling. diff --git a/reference/reason-codes.mdx b/reference/reason-codes.mdx index 15287be..72e554b 100644 --- a/reference/reason-codes.mdx +++ b/reference/reason-codes.mdx @@ -4,8 +4,6 @@ description: "Machine-readable codes that explain a recommendation." icon: "tags" --- -{/* sources: src/services/aggregator.py:_aggregate_reason_codes, src/services/strategies/llm_decision.py, docs/API_FIELD_REFERENCE.md */} - `reason_codes` is a list of short identifiers explaining why a scan received its recommendation. Codes can be combined; treat the list as an unordered set. @@ -31,7 +29,7 @@ Emitted when matching keywords or domains are found. ## Pattern codes -Emitted when contextual analysis identifies a violation pattern. +Emitted when Tumban's analysis identifies a violation pattern. | Code | Meaning | |------|---------| @@ -44,36 +42,36 @@ Emitted when contextual analysis identifies a violation pattern. | Code | Meaning | |------|---------| | `EXCULPATORY_CONTEXT` | Prohibited keywords appear, but in journalism, education, advocacy, or past-tense framing. The score is suppressed. | -| `CLEAN_PROFILE` | Contextual analysis explicitly cleared the profile. | +| `CLEAN_PROFILE` | Tumban's analysis explicitly cleared the profile. | -## Content Safety codes +## Content safety codes -Emitted when the content classifier flagged a body of text or an image. +Emitted when Tumban's content classification flagged text or an image. | Code | Meaning | |------|---------| -| `CONTENT_SAFETY_TEXT_FLAGGED` | Profile text triggered the content classifier. | -| `CONTENT_SAFETY_IMAGE_FLAGGED` | Profile or banner image triggered the content classifier. | -| `CONTENT_FILTER_TRIGGERED` | The contextual model's content filter blocked analysis. | -| `VIOLENCE_CONTENT` | Content classifier flagged violence. | -| `HATE_CONTENT` | Content classifier flagged hate speech. | -| `SELF_HARM_CONTENT` | Content classifier flagged self-harm content. | +| `CONTENT_SAFETY_TEXT_FLAGGED` | Profile text was flagged by the safety classifier. | +| `CONTENT_SAFETY_IMAGE_FLAGGED` | Profile or banner image was flagged by the safety classifier. | +| `CONTENT_FILTER_TRIGGERED` | A content filter blocked further analysis. | +| `VIOLENCE_CONTENT` | Violence flagged by the safety classifier. | +| `HATE_CONTENT` | Hate speech flagged by the safety classifier. | +| `SELF_HARM_CONTENT` | Self-harm content flagged by the safety classifier. | ## Adjudication codes -Emitted when the judge model adjusted a borderline aggregated score. +Emitted when Tumban adjusted a borderline score after additional review. | Code | Meaning | |------|---------| -| `JUDGE_BUMP_UP` | Judge raised the score after seeing additional context. | -| `JUDGE_BUMP_DOWN` | Judge lowered the score after concluding the underlying signal was a false positive. | +| `JUDGE_BUMP_UP` | Tumban raised the score after additional review found supporting context. | +| `JUDGE_BUMP_DOWN` | Tumban lowered the score after additional review concluded the underlying signal was a false positive. | ## Failure codes | Code | Meaning | |------|---------| -| `LLM_API_ERROR` | The contextual model failed due to infrastructure (timeout, network, 5xx). Score defaulted to a neutral value. | -| `LLM_PARSE_ERROR` | The contextual model returned a response that could not be parsed (invalid or truncated JSON). Score defaulted to a neutral value. | +| `LLM_API_ERROR` | A downstream analysis call failed due to infrastructure (timeout, network, 5xx). Tumban substituted a neutral score. | +| `LLM_PARSE_ERROR` | A downstream analysis call returned a response that could not be parsed. Tumban substituted a neutral score. | | `SCAN_FAILED` | The scan as a whole could not produce a triage report. Webhook payload only — see the `error` field. | diff --git a/reference/recommendation.mdx b/reference/recommendation.mdx index efa142a..4869c6f 100644 --- a/reference/recommendation.mdx +++ b/reference/recommendation.mdx @@ -4,8 +4,6 @@ description: "Allowed values for the recommendation field, with score thresholds icon: "list" --- -{/* sources: src/models/profile.py:RecommendationT, src/services/aggregator.py:_map_recommendation */} - The `recommendation` field on triage reports and webhook payloads takes one of four values, mapped from the underlying `risk_score`: diff --git a/skill.md b/skill.md new file mode 100644 index 0000000..c8d850c --- /dev/null +++ b/skill.md @@ -0,0 +1,309 @@ +--- +name: tumban +description: Tumban is a creator-profile compliance scanning API. Use this skill when an agent needs to submit profile URLs for ToS / prohibited-content analysis, retrieve the resulting triage report, integrate webhooks for asynchronous results, or manage organization API keys and webhook secrets. Covers every v2 endpoint, the asynchronous scan lifecycle, rate-limit semantics, webhook signature verification, and the role-aware permission model. +license: Proprietary. Contact hello@tumban.com for terms. +compatibility: Tumban v2 public API. Requires HTTPS, JSON over `application/json`, and a bearer credential (`sk_…` API key or dashboard session token). +metadata: + api-version: v2 + base-url: https://api.tumban.com + docs: https://docs.tumban.com +--- + +# Tumban v2 — agent skill + +Tumban analyses creator profile URLs and returns a triage report +(`recommendation`, `risk_score`, `confidence`, `reason_codes`, +`evidence_index`, `coverage`). Scans are asynchronous: submission +returns a `scan_id` immediately, and final results are delivered via +webhook to a `callback_url` or polled with `GET /api/v2/scans/{scan_id}`. + +## Base URL and auth + +``` +Base URL: https://api.tumban.com +All v2 endpoints mounted under: /api/v2 +``` + +Every request requires `Authorization: Bearer `. Two token kinds: + +- **API key** — `sk_<64-hex>` (67 characters total). Long-lived + server-side credential. Returned exactly once at creation; Tumban + stores only its SHA-256 hash. +- **Dashboard session token** — short-lived browser credential issued + by the auth provider. Used by the Tumban dashboard. + +`org_id` (returned in `Get org settings` and on signed webhooks) is an +**opaque, case-sensitive string** with the prefix `org_`. Do not regex +against hex or any fixed alphabet — the body is alphanumeric and may +include mixed case. + +## Endpoint catalogue (canonical paths) + +### Scans + +| Method | Path | Purpose | +|--------|------|---------| +| `POST` | `/api/v2/scan` | Submit a single profile URL | +| `POST` | `/api/v2/batch` | Submit up to N profile URLs in one request | +| `GET` | `/api/v2/scans/{scan_id}` | Fetch a scan record (status + triage report) | +| `GET` | `/api/v2/batches/{batch_id}` | Fetch aggregate batch progress | + +### Organization + +| Method | Path | Purpose | +|--------|------|---------| +| `GET` | `/api/v2/org/settings` | Read org settings | +| `PATCH` | `/api/v2/org/settings` | Update `default_callback_url` (admin-only, dashboard session) | +| `POST` | `/api/v2/org/webhook-secret/rotate` | Rotate webhook signing secret (admin-only, dashboard session) | +| `GET` | `/api/v2/org/scans` | List recent scans for the org | + +### API key management + +| Method | Path | Purpose | +|--------|------|---------| +| `POST` | `/api/v2/org/api-keys` | Create a new API key (raw value shown once) | +| `GET` | `/api/v2/org/api-keys` | List API keys (hashes + metadata) | +| `DELETE` | `/api/v2/org/api-keys/{key_id}` | Revoke an API key | + +Usage / analytics endpoints are **not** part of the public API. The +dashboard renders usage data internally; there is no supported way to +read it programmatically. + +## Endpoint details that summarisers commonly get wrong + +These are the high-precision facts the autogenerator must not collapse. + +### Revoke API key — `DELETE`, no `/revoke` suffix + +``` +DELETE /api/v2/org/api-keys/{key_id} +``` + +There is **no** `POST /api/v2/org/api-keys/{key_id}/revoke` variant. +Hitting that path returns `404`; hitting the correct path with `POST` +returns `405`. The canonical form is `DELETE` on the bare key resource. +Successful response is `204 No Content` with no body. + +### Rotate webhook secret — `/org/` segment is required + +``` +POST /api/v2/org/webhook-secret/rotate +``` + +`POST /api/v2/webhook-secret/rotate` (without `/org/`) does not exist +and returns `404`. There are no path variants — only the `/org/`-prefixed +form is wired up. + +## Authentication and role model + +API keys can call almost every endpoint. The three exceptions below +require a dashboard session — API-key auth gets `403` immediately: + +| Endpoint | Required auth | +|----------|---------------| +| `PATCH /api/v2/org/settings` | Dashboard session, role `admin` | +| `POST /api/v2/org/webhook-secret/rotate` | Dashboard session, role `admin` | +| `DELETE /api/v2/org/api-keys/{key_id}` | Dashboard session (role-aware — see below) | + +### Revoke role rules + +Revoke is **not admin-only**. Both admins and members may call it from +a dashboard session, but scope differs: + +- **Admins** — may revoke any key in the organization. +- **Members** — may revoke **only keys they created themselves**. A + member targeting another user's key gets `404` (same status as a + missing key, to prevent `key_id` enumeration). +- **API keys (`sk_…`)** — always rejected with `403`. + +## Webhook secret storage + +The webhook signing secret is stored in **plaintext** on the server +(it has to be — Tumban computes the HMAC on every outbound webhook). +This contrasts with API keys, which are kept only as SHA-256 hashes. + +Because the secret is shown to you exactly once at rotation time and +never reappears in `GET /api/v2/org/settings`, **there is no recovery +path** — capture it from the rotation response immediately. If you +lose it, rotate again and update every verifier in lockstep before +sending any more outbound traffic that would expect the old signature. + +## Scan lifecycle + +``` +POST /api/v2/scan + → { scan_id, status: "processing", submitted_at, estimated_completion } + +(async) + → scan runs server-side (timeout: 450 s) + → on terminal status, Tumban POSTs to callback_url (when configured) + → result also queryable via GET /api/v2/scans/{scan_id} +``` + +`status` values today: + +- `processing` — in flight. +- `completed` — terminal; triage report available. +- `failed` — terminal; `error` field set. +- `completed_with_partial` — **reserved**, declared in the schema but + not currently emitted by the pipeline. Today, scans that experience + step failures (slow page, login wall, transient model error) still + return as `completed`; the partial-coverage details surface inside + the `coverage` object (e.g. `social_links_checked`, `blocked_by_login`). + Keep `completed_with_partial` in `switch`/`match` statements so your + integration is forward-compatible, but don't gate partial-handling + logic on receiving it. + +## Confidence semantics + +`confidence` ∈ `{high, medium, low}`: + +- **`high`** — strong, corroborated evidence. Act on it directly. +- **`medium`** — solid signal, less corroboration. Useful, but a + manual reviewer may want to spot-check on edge cases. +- **`low`** — a thin or borderline lead. Treat as review-worthy, not + as a verdict. + +`confidence` is **never `low` when `recommendation` is `no_flags`** — +clean profiles always come back with `high` confidence in the +`no_flags` decision. This combination cannot occur. + +How Tumban arrives at each level is **not part of the public contract** +and may change without notice. Build against `confidence`, +`risk_score`, `recommendation`, `reason_codes`, and `evidence_index`; +ignore any other field on the response. + +## Score → recommendation bands + +| Range | Recommendation | +|-------|----------------| +| 0–10 | `no_flags` | +| 11–40 | `review_low` | +| 41–60 | `review_medium` | +| 61–100 | `review_high` | + +`risk_score` reflects confidence that a policy violation is present +(not uncertainty); missing or unreachable data does not push the score +up — partial coverage surfaces in the `coverage` object, never as +inflated risk. + +The four enum values above are **frozen API contract**. The score +thresholds may be tuned over time as detection improves; the enum +strings will not change. How the score is produced internally is not +part of the contract. + +## Rate limits + +When an org has a `daily_scan_limit` configured, exceeding it returns +`429` with a **structured** detail body (not a string): + +```json +{ + "detail": { + "error": "daily_scan_limit_exceeded", + "limit": 1000, + "used": 1000 + } +} +``` + +- Counter resets at `00:00 UTC`. +- Batch submissions have a **partial-acceptance** path: when remaining + capacity is less than the requested batch size, the batch is accepted + with the leading N profiles and the response sets + `daily_limit_truncated: true` plus `profiles_skipped: `. Only a + zero-capacity submission returns `429`. +- Tumban does **not** return `X-RateLimit-*` headers and does **not** + honour `Idempotency-Key`. Use the returned `scan_id` (or per-profile + scan ids in a batch) as the natural idempotency key in your handler. + +## Webhook delivery and verification + +Tumban POSTs JSON to your `callback_url` when a scan reaches a terminal +status. Up to 3 attempts; backoff `1s` then `2s`. `Retry-After` is +honoured when numeric (decimal seconds allowed, e.g. `2.5`); HTTP-date +form is not parsed. + +### Signed headers (when org has a webhook secret) + +| Header | Meaning | +|--------|---------| +| `X-Tumban-Signature` | `sha256=` over the raw body bytes (V1). | +| `X-Tumban-Signature-V2` | `sha256=` over `"{timestamp}.{org_id}." + body` (raw bytes concatenation). **Recommended.** | +| `X-Tumban-Timestamp` | Unix seconds when the payload was signed. | +| `X-Tumban-Org-Id` | The `org_id` this webhook is for. Treat as opaque. Under rare error paths Tumban may send an empty value (`""`) — V2 verifiers reject these because they will not match `EXPECTED_ORG_ID`. | + +A correct V2 verifier performs three checks: + +1. Tenant binding — `X-Tumban-Org-Id` matches the receiver's expected + `org_id`. +2. Replay protection — `X-Tumban-Timestamp` is within ~5 minutes of now. +3. Constant-time signature compare — never use `==` on the hex digest. + +## Common workflows + +### Submit a single scan and read the result via webhook + +1. `POST /api/v2/scan` with `profile_url`, `callback_url`, + optional `metadata` (any JSON, echoed back). +2. Receive webhook at `callback_url`. Verify the V2 signature first; + then process `recommendation`, `risk_score`, `evidence_index`, and + `coverage`. +3. Acknowledge with any `2xx` status. Non-2xx triggers retry. + +### Submit a batch and watch aggregate progress + +1. `POST /api/v2/batch` with `profile_urls`. Per-profile webhooks fire + independently as each scan completes. +2. Poll `GET /api/v2/batches/{batch_id}` for aggregate counts + (`completed`, `failed`, `in_progress`). +3. If `daily_limit_truncated: true` is set on the submission response, + the trailing `profiles_skipped` URLs were not queued — resubmit + them after the next `00:00 UTC`. + +### Set up webhook signature verification + +1. `POST /api/v2/org/webhook-secret/rotate` from a dashboard admin + session. Capture `webhook_secret` from the response (shown once). +2. Store the secret in your secret manager alongside the `org_id` your + receiver expects. +3. Implement V2 verification — see `https://docs.tumban.com/webhooks/signatures` + for reference verifiers in Python / Node / Ruby. +4. Roll out the verifier **before** the secret is actively used by + senders. Tumban switches signing to the new secret immediately on + rotation; old secrets become inactive at the same instant. + +### Rotate an API key without downtime + +1. `POST /api/v2/org/api-keys` to create a new key. Capture the raw + `sk_…` value (shown once). +2. Deploy the new key. Both keys are valid simultaneously. +3. Once traffic has cut over (watch `last_used_at` on the old key via + `GET /api/v2/org/api-keys`), call + `DELETE /api/v2/org/api-keys/{old_key_id}` from a dashboard session + to revoke. There is no auto-expiry. + +## Gotchas worth surfacing to a user + +- **`completed_with_partial` is reserved.** Reading from `coverage` is + the only reliable way to detect partial pipelines today. +- **Webhook secret is plaintext server-side.** Treat any leak as a + full compromise of webhook authenticity; rotate immediately. +- **Revoke is `DELETE`, not `POST` + `/revoke`.** And the path has no + `/revoke` suffix — see Endpoint details above. +- **Webhook rotate path includes `/org/`.** `/api/v2/webhook-secret/rotate` + (no `/org/`) does not exist. +- **No `X-RateLimit-*` headers, no `Idempotency-Key`.** Plan around + the structured 429 body and `scan_id` as natural idempotency key. +- **`org_id` is opaque.** Do not regex against hex or any specific + alphabet. Compare for exact equality. +- **The `confidence` field is never `low` when `recommendation` is + `no_flags`.** This combination cannot occur. + +## See also + +- Full reference docs: `https://docs.tumban.com` +- Status values: `https://docs.tumban.com/reference/status` +- Reason codes: `https://docs.tumban.com/reference/reason-codes` +- Webhook signature verifiers: `https://docs.tumban.com/webhooks/signatures` +- Errors and rate-limit detail: `https://docs.tumban.com/api/errors` diff --git a/webhooks/payload.mdx b/webhooks/payload.mdx index c8c3541..1d5d379 100644 --- a/webhooks/payload.mdx +++ b/webhooks/payload.mdx @@ -82,9 +82,9 @@ scan's `callback_url` with a JSON body. Headers: - Per-URL evidence the contextual model cited. May be `[]` when only - deterministic strategies fired or the contextual model hit an - infrastructure error. See [Evidence index](/concepts/evidence-index). + Per-URL evidence Tumban cited in support of the decision. May be `[]` + when Tumban reached its decision without a per-URL citation to + surface. See [Evidence index](/concepts/evidence-index).