diff --git a/api/errors.mdx b/api/errors.mdx index 33461fc..99ee040 100644 --- a/api/errors.mdx +++ b/api/errors.mdx @@ -63,12 +63,13 @@ Use `loc` to locate the field — the last segment is the field name. | 401 | `Invalid or revoked API key` | The `sk_…` token is unknown or revoked. | | 403 | `Token missing org_id — make sure you have an active organization selected` | Dashboard session has no active org. | | 403 | `API key management requires a dashboard session. API keys cannot revoke API keys.` | [Revoke API key](/api/keys/revoke) when called with `sk_…` auth. | +| 403 | `This endpoint requires a dashboard session. API keys are not permitted.` | [Create API key](/api/keys/create), [List API keys](/api/keys/list) when called with `sk_…` auth. | | 403 | `Admin role required` | [Update org settings](/api/org/update-settings), [Rotate webhook secret](/api/webhook-secret/rotate) when the caller is a non-admin. | | 403 | `This endpoint requires an admin dashboard session. API keys cannot access admin endpoints.` | [Update org settings](/api/org/update-settings), [Rotate webhook secret](/api/webhook-secret/rotate) when called with `sk_…` auth. | | 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/batch-get.mdx b/api/scans/batch-get.mdx index d31e3f7..cb24893 100644 --- a/api/scans/batch-get.mdx +++ b/api/scans/batch-get.mdx @@ -35,9 +35,8 @@ GET /api/v2/batches/{batch_id} - Count of profiles whose scans completed successfully. The reserved - `completed_with_partial` status is rolled into this count when it - starts firing — see [Status values](/reference/status). + Count of profiles whose scans completed successfully. See + [Status values](/reference/status). diff --git a/api/scans/batch.mdx b/api/scans/batch.mdx index d3b771b..df870b7 100644 --- a/api/scans/batch.mdx +++ b/api/scans/batch.mdx @@ -134,23 +134,29 @@ response = httpx.post( When the org's `daily_scan_limit` has capacity for fewer profiles than were requested, the batch is accepted with the leading N profiles only -and the response sets `daily_limit_truncated` and `profiles_skipped`: +and the response sets `daily_limit_truncated` and `profiles_skipped`. + +**Worked example.** Submit 50 URLs with 30 remaining in the daily +quota → the **first 30** URLs (in submission order) are queued; the +**last 20** are dropped: ```json { "batch_id": "9b8a7c6d-1234-5678-9abc-def012345678", "status": "processing", - "total_profiles": 2, + "total_profiles": 30, "submitted_at": "2026-04-29T12:00:00.123456+00:00", - "estimated_completion": "2026-04-29T12:04:00.123456+00:00", + "estimated_completion": "2026-04-29T13:00:00.123456+00:00", "daily_limit_truncated": true, - "profiles_skipped": 1 + "profiles_skipped": 20 } ``` -The remaining profile URLs (the last `profiles_skipped` entries from the -request, in order) were not queued. Resubmit them after the next daily -window opens at `00:00 UTC`. +`total_profiles` is the count actually queued (30), not the count +submitted (50). To recover the skipped URLs, take the last +`profiles_skipped` entries from your original `profile_urls` array — in +the same order — and resubmit them after the next daily window opens at +`00:00 UTC`. ## Errors diff --git a/api/scans/get.mdx b/api/scans/get.mdx index dda62df..d8027ec 100644 --- a/api/scans/get.mdx +++ b/api/scans/get.mdx @@ -58,6 +58,14 @@ The response is the scan record. Fields you should rely on: When pipeline processing finished. `null` while still processing. + + `null` until the ban-checker has evaluated this profile, then `true` + if the creator has been banned by the upstream platform (e.g. 404 / + 410 / redirect on the profile URL) or `false` if the profile is + still live. Useful for skipping enforcement on already-banned + creators. + + The callback URL the scan was submitted with (or the org default if not provided), as a string. Empty when no callback was configured. @@ -78,9 +86,8 @@ The response is the scan record. Fields you should rely on: - Present once `status` reaches a terminal value with a usable result — - today that means `completed` (`completed_with_partial` is - [reserved](/reference/status) and not currently emitted). Shape below. + Present once `status` reaches a terminal value with a usable result + (`completed`). Shape below. ### `triage_report` @@ -117,28 +124,18 @@ 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` — -that field is delivered as a top-level key in the -[webhook payload](/webhooks/payload). When polling, read the -`coverage` object (delivered alongside the triage report on the webhook -side) to see what was skipped; today the scan's `status` is `completed` -even when individual steps failed (`completed_with_partial` is -[reserved](/reference/status) and not currently emitted). +Poll responses include the [`coverage`](/concepts/coverage) object too +— it lives at `triage_report.coverage` on `GET /api/v2/scans/{scan_id}`. +On the [webhook payload](/webhooks/payload) the same object is delivered +as a top-level `coverage` key. Same fields either way; only the +placement differs. The scan's `status` is `completed` even when +individual steps were skipped — read `coverage` to see what ran. ## Example @@ -156,6 +153,7 @@ curl https://api.tumban.com/api/v2/scans/550e8400-e29b-41d4-a716-446655440000 \ "created_at": "2026-04-29T12:00:00.123456+00:00", "processing_started_at": "2026-04-29T12:00:01.234567+00:00", "processing_completed_at": "2026-04-29T12:01:38.987654+00:00", + "is_banned": false, "callback_url": "https://your-app.example/webhooks/tumban", "metadata": {"reviewer_id": "rv_42"}, "webhook_delivered_at": "2026-04-29T12:01:39.345678+00:00", @@ -167,8 +165,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 +178,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 +201,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..a163839 100644 --- a/authentication.mdx +++ b/authentication.mdx @@ -28,7 +28,7 @@ treated as a session token. ## What API keys can do API keys are scoped to your organization and can call every endpoint in -this reference **except** the three management endpoints below. These +this reference **except** the five management endpoints below. These require a dashboard session because they mutate authentication or organization-wide configuration. @@ -36,11 +36,15 @@ organization-wide configuration. |----------|---------------| | [`PATCH /org/settings`](/api/org/update-settings) | Dashboard session, **admin role** | | [`POST /org/webhook-secret/rotate`](/api/webhook-secret/rotate) | Dashboard session, **admin role** | +| [`POST /org/api-keys`](/api/keys/create) | Dashboard session (any role) | +| [`GET /org/api-keys`](/api/keys/list) | Dashboard session (any role) | | [`DELETE /org/api-keys/{key_id}`](/api/keys/revoke) | Dashboard session (any role; see [Revoke API key](/api/keys/revoke) for role-aware ownership rules) | -API keys hitting these endpoints get `403`. See [Errors → common detail -strings](/api/errors#common-detail-strings) for the exact `detail` text -to match on. +API keys hitting any of these endpoints get `403` — credential +management is intentionally locked out of `sk_…` auth so a leaked key +cannot mint, list, or revoke other keys. See +[Errors → common detail strings](/api/errors#common-detail-strings) for +the exact `detail` text to match on. Each request looks up the key by SHA-256 hash, scopes the request to the key's organization, and updates the key's `last_used_at` timestamp. @@ -64,11 +68,20 @@ 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 | |--------|---------| | 401 | Missing `Authorization` header, malformed `Bearer` prefix, or invalid/revoked credential. | -| 403 | Token decodes but the active organization context is missing (session tokens only), **or** API-key auth was used on one of the three endpoints that require a dashboard session (see table above), **or** the role is insufficient on an admin-only endpoint. | +| 403 | Token decodes but the active organization context is missing (session tokens only), **or** API-key auth was used on one of the five endpoints that require a dashboard session (see table above), **or** the role is insufficient on an admin-only endpoint. | See [Errors](/api/errors) for the full error envelope. 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/coverage.mdx b/concepts/coverage.mdx index af22257..1e444d1 100644 --- a/concepts/coverage.mdx +++ b/concepts/coverage.mdx @@ -61,9 +61,8 @@ before drawing conclusions about edges; the `recommendation` and `risk_score` are still meaningful, but they were produced from a narrower input set. -The `completed_with_partial` status is **reserved** for a future -partial-completion path and is not currently emitted (see -[Status values](/reference/status)). Keep it in your switch/match so -your integration remains forward-compatible, but do not gate -partial-handling logic on receiving it today — read from `coverage` -instead. +Scans that experienced step failures still come back as +`status: "completed"` — the `coverage` object is the canonical record +of what ran and what was skipped (see [Status values](/reference/status)). +Read from `coverage` to detect partial pipelines; do not gate +partial-handling logic on the `status` field. 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..5f01136 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 @@ -56,5 +45,4 @@ still catch something the pipeline missed. decisions warrant a second look regardless of recommendation tier. - Always check the [`coverage`](/concepts/coverage) object before acting on a result — partial pipelines surface there, not as a - distinct status (`completed_with_partial` is currently - [reserved](/reference/status)). + distinct status. diff --git a/concepts/scans-and-batches.mdx b/concepts/scans-and-batches.mdx index 629114e..c169cc2 100644 --- a/concepts/scans-and-batches.mdx +++ b/concepts/scans-and-batches.mdx @@ -18,8 +18,7 @@ asynchronously. The scan moves through these statuses: | Status | Meaning | |--------|---------| | `processing` | The scan is in flight. | -| `completed` | All analysis steps finished successfully. | -| `completed_with_partial` | **Reserved.** Declared in the API contract for future partial-completion paths, but not currently emitted by the pipeline — scans that experience a step failure today are reported as `completed` (with the [`coverage`](/concepts/coverage) object recording what was skipped) or `failed`. Handle this value in your switch statements so your integration is forward-compatible. | +| `completed` | Scan reached a terminal state with a usable triage report. Some steps may have been skipped — read the [`coverage`](/concepts/coverage) object to see what ran. | | `failed` | The scan could not produce a triage report. The `error` field explains why. | See [Status values](/reference/status) for the canonical reference. 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/quickstart.mdx b/quickstart.mdx index 695e7b7..694917b 100644 --- a/quickstart.mdx +++ b/quickstart.mdx @@ -95,8 +95,7 @@ You'll need a Tumban account with an active organization. - **Wait for the webhook** at your `callback_url` — see [Webhook payload](/webhooks/payload). - **Poll** `GET /api/v2/scans/{scan_id}` until `status` is one of - `completed` or `failed` (`completed_with_partial` is - [reserved](/reference/status) and not currently emitted). + `completed` or `failed`. The triage report includes `recommendation`, `risk_score`, `confidence`, `reason_codes`, and `evidence_index`. See diff --git a/reference/reason-codes.mdx b/reference/reason-codes.mdx index 15287be..9ebc190 100644 --- a/reference/reason-codes.mdx +++ b/reference/reason-codes.mdx @@ -4,11 +4,11 @@ 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. +an **unordered set** and as **open-ended**. New codes may be added over +time, so code defensively against unknown codes — never assume the list +is exhaustive or in a fixed order. ## Categorical codes @@ -31,7 +31,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 +44,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. | +| `TEXT_FLAGGED` | Profile text was flagged by the safety classifier. | +| `IMAGE_FLAGGED` | Profile or banner image was flagged by the safety classifier. | +| `UNSAFE_CONTENT_BLOCKED` | Upstream safety guardrails refused to analyze the input — treated as a finding, not an error. | +| `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 a follow-up review pass evaluated a borderline score. | 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. | +| `SECONDARY_REVIEW_CONFIRMED` | Follow-up review pass agreed with the initial score. | +| `SECONDARY_REVIEW_DOWNGRADED` | Follow-up review pass downgraded the initial score. | ## 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. | +| `ANALYSIS_ERROR` | Contextual analysis step failed due to an infrastructure error (timeout, network, upstream 5xx). Scores 10 (neutral). | +| `PARSE_ERROR` | Contextual analysis returned a malformed response. Conservative fallback score 50. | | `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/reference/status.mdx b/reference/status.mdx index cbe014b..b5e72b3 100644 --- a/reference/status.mdx +++ b/reference/status.mdx @@ -7,13 +7,12 @@ icon: "list" {/* sources: src/models/profile.py:ScanStatusT */} The `status` field on scan responses and webhook payloads takes one -of four values: +of three values: | Value | Meaning | |-------|---------| | `processing` | The scan is in flight. You'll only see this on poll responses; Tumban does not deliver webhooks while a scan is processing. | -| `completed` | All analysis steps finished successfully. Use the triage report directly. | -| `completed_with_partial` | **Reserved** — declared in the API contract for future partial-completion paths but not currently written by the pipeline. Today, a scan that experienced a step failure is reported as `completed` (with `coverage` recording what was skipped) or `failed`. Keep this value in your `switch`/`match` so your integration is forward-compatible. | +| `completed` | The scan reached a terminal state with a usable triage report. Some analysis steps may have been skipped (login walls, timeouts, blocked content); inspect the [`coverage`](/concepts/coverage) object to see what actually ran. | | `failed` | The scan could not produce a triage report. The `error` field explains why; the webhook defaults `recommendation` to `review_high` so the profile still lands in your review queue. | These values are stable. New statuses, if any, will be added without diff --git a/skill.md b/skill.md new file mode 100644 index 0000000..99f93b5 --- /dev/null +++ b/skill.md @@ -0,0 +1,322 @@ +--- +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 five 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` | +| `POST /api/v2/org/api-keys` | Dashboard session (any role) | +| `GET /api/v2/org/api-keys` | Dashboard session (any role) | +| `DELETE /api/v2/org/api-keys/{key_id}` | Dashboard session (role-aware — see below) | + +Credential-management endpoints (the three `/api-keys` rows) reject +`sk_…` auth so a leaked key cannot mint, list, or revoke other keys. +The exact 403 detail strings differ between create/list and revoke — +see `https://docs.tumban.com/api/errors` for the canonical list. + +### 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: + +- `processing` — in flight. +- `completed` — terminal; triage report available. May reflect a + pipeline where some steps were skipped (slow page, login wall, + transient model error). The `coverage` object records what actually + ran (e.g. `social_links_checked`, `blocked_by_login`). Read + `coverage` to detect partial pipelines; do not gate partial-handling + logic on the `status` field. +- `failed` — terminal; `error` field set. + +### `is_banned` + +`GET /api/v2/scans/{scan_id}` also returns a top-level `is_banned` +field (`bool | null`). `null` until the ban-checker has evaluated the +profile; then `true` if the upstream platform has banned the creator +(404 / 410 / redirect on the profile URL) or `false` if the profile is +still live. Useful for skipping enforcement on already-banned creators. + +## 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 + +- **Partial pipelines surface in `coverage`, not in `status`.** A + scan with step failures still reports `status: "completed"` — + read the `coverage` object to see what ran. +- **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..3feeae9 100644 --- a/webhooks/payload.mdx +++ b/webhooks/payload.mdx @@ -6,10 +6,9 @@ icon: "code" {/* sources: src/services/webhook.py:_build_payload, src/models/profile.py:WebhookPayload,Coverage */} -When a scan reaches a terminal status (today: `completed` or -`failed` — `completed_with_partial` is [reserved](/reference/status) -for a future partial-completion path), Tumban issues a `POST` to the -scan's `callback_url` with a JSON body. Headers: +When a scan reaches a terminal status (`completed` or `failed`), +Tumban issues a `POST` to the scan's `callback_url` with a JSON body. +Headers: - `Content-Type: application/json` - `X-Tumban-Signature`, `X-Tumban-Signature-V2`, `X-Tumban-Timestamp`, @@ -28,9 +27,8 @@ scan's `callback_url` with a JSON body. Headers: - `completed` or `failed` today. `completed_with_partial` is declared - in the schema and reserved but not currently emitted. See - [Status values](/reference/status) for the full reference. + `completed` or `failed`. See [Status values](/reference/status) for + the full reference. @@ -82,9 +80,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). @@ -92,12 +90,6 @@ scan's `callback_url` with a JSON body. Headers: wrong. - - Reserved for a future `completed_with_partial` payload — brief - description of what was skipped. Not currently emitted; do not depend - on its presence. - - ## Example — completed ```json