{"openapi":"3.0.3","info":{"title":"VITNA API (Phase 1)","version":"1.1.0","description":"The compliance evidence layer for AI agents. Pre-flight compliance checks agents call before they act (consent, breach reportability, EU AI Act classification, cross-border), and audit-grade, signed records of what they did. Phase 1 surface covers region routing labels (one database region is in use today), DPIA/ROPA generation, Data Subject Rights (scoped: answerable only for events linked to a subject via principal_ref at ingest, from the point linkage begins; earlier events are permanently unattributable and cannot be linked retroactively), consent lifecycle, Sahamati AA integration, breach notification, and INR/Razorpay billing for DPDP-IN owners (switched off pending merchant onboarding). Also includes one-command agent self-provisioning (/api/setup, mints a restricted trial key with no signup), a destructive-action safety pre-flight (/api/preflight/action-check), scoped engagement sessions with signed evidence bundles (/api/engagement/*), and account claim by a person, from the claim link (/claim).","contact":{"name":"COSTRINITY","email":"dpo@costrinity.xyz","url":"https://www.costrinity.xyz/"},"license":{"name":"Proprietary","url":"https://vitna.costrinity.xyz/terms"}},"servers":[{"url":"https://vitna.costrinity.xyz","description":"Production"},{"url":"https://vigil-staging.vercel.app","description":"Staging (preview branch)"}],"components":{"securitySchemes":{"ApiKey":{"type":"apiKey","in":"header","name":"x-vigil-key","description":"Per-agent API key. Format: vitna_<64-char-hex>; legacy vigil_ keys remain valid. Send on /api/ingest."},"Bearer":{"type":"http","scheme":"bearer","bearerFormat":"vitna_<key> (legacy vigil_<key> accepted)","description":"Same key as x-vigil-key, sent as `Authorization: Bearer <key>`."},"Session":{"type":"apiKey","in":"cookie","name":"vigil_session","description":"Operator dashboard session cookie. Set by /api/auth/verify-pin."},"CronSecret":{"type":"http","scheme":"bearer","description":"Bearer ${CRON_SECRET}. Only the Vercel cron infrastructure should hold this."}},"schemas":{"JurisdictionId":{"type":"string","enum":["DPDP-IN","GDPR-EU","CPRA-CA","LGPD-BR","PDPA-SG","US-FED"]},"RegionId":{"type":"string","description":"A routing label stored on the account, not a place. 'us-east-1' labels the primary database project, which runs in AWS us-west-2 (Oregon, USA). No ap-south-1 or eu-west-1 database is provisioned today, so every account's data is stored in the primary. The generated documents print where the data is, not the label.","enum":["us-east-1","ap-south-1","eu-west-1"]},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"detail":{"type":"string"}}},"ConsentArtifact":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"principal_id":{"type":"string","format":"uuid"},"source":{"type":"string","enum":["sahamati-aa","dpdp-cm","gdpr-self","cpra-self","lgpd-self","pdpa-self","operator-attested"]},"purpose_codes":{"type":"array","items":{"type":"string"}},"permitted_categories":{"type":"array","items":{"type":"string"}},"consent_start":{"type":"string","format":"date-time"},"consent_expiry":{"type":"string","format":"date-time","nullable":true},"revoked_at":{"type":"string","format":"date-time","nullable":true},"signature_verified":{"type":"boolean"},"frequency_max":{"type":"integer","nullable":true},"frequency_window_hours":{"type":"integer","nullable":true}}},"ConsentCheckResult":{"type":"object","required":["allowed","reason","matching_consent_id","principal_id"],"properties":{"allowed":{"type":"boolean"},"reason":{"type":"string","nullable":true,"enum":[null,"no_principal","no_active_consent","purpose_mismatch","category_overflow"]},"matching_consent_id":{"type":"string","format":"uuid","nullable":true},"principal_id":{"type":"string","format":"uuid","nullable":true}}},"Phase1Health":{"type":"object","properties":{"ok":{"type":"boolean"},"phase":{"type":"string","enum":["1"]},"generated_at":{"type":"string","format":"date-time"},"required":{"type":"object","properties":{"primary_supabase_reachable":{"type":"boolean"},"cron_secret_configured":{"type":"boolean"},"resend_configured":{"type":"boolean"}}},"optional":{"type":"object","properties":{"dpia_snapshots_migration_applied":{"type":"boolean"},"razorpay_configured":{"type":"boolean"},"sahamati_strict_mode":{"type":"boolean"},"regions_provisioned":{"type":"array","items":{"$ref":"#/components/schemas/RegionId"}}}},"surface":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}}}}},"paths":{"/api/health/regions":{"get":{"summary":"List provisioned Supabase regions","tags":["health"],"responses":{"200":{"description":"OK"}}}},"/api/health/phase1":{"get":{"summary":"Phase 1 readiness probe","tags":["health"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Phase1Health"}}}}}}},"/api/data-rights/summary":{"get":{"summary":"Catalogue of data-subject rights by jurisdiction","tags":["data-rights"],"parameters":[{"name":"jurisdiction","in":"query","schema":{"$ref":"#/components/schemas/JurisdictionId"}}],"responses":{"200":{"description":"Public catalogue"}}}},"/api/data-rights/access":{"get":{"summary":"Right of access, over principal_ref-linked records only","description":"Returns the record held for one data principal. SCOPE: only events linked at ingest via principal_ref are covered. Events recorded before the operator began sending principal_ref carry no data-subject identifier, cannot be linked retroactively, and are not included — they are not attributable to this or any other data subject. 2xx responses carry a `coverage` block giving the linkage start and the account-wide count of pre-linkage events.","tags":["data-rights"],"security":[{"Session":[]}],"parameters":[{"name":"principal_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","schema":{"type":"string","enum":["json","portable"]}}],"responses":{"200":{"description":"OK"},"401":{"description":"Unauthorized"},"404":{"description":"Principal not found"}}}},"/api/data-rights/portability":{"get":{"summary":"Right to data portability (GDPR Art 20) over principal_ref-linked events only, JSON-LD export","description":"Exports only events linked to this data subject by principal_ref at ingest. Events recorded before principal_ref was sent carry no data-subject identifier and cannot be linked retroactively, so they are neither searched nor exported. The 200 body carries a `coverage` block giving the linkage window. An account with no data-subject linkage receives 422 no_principal_linkage — not a 200 — because nothing was searched and nothing was exported; that is not a finding that the subject has no records.","tags":["data-rights"],"security":[{"Session":[]}],"parameters":[{"name":"principal_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","schema":{"type":"string","enum":["jsonld","json"]}}],"responses":{"200":{"description":"JSON-LD or JSON"}}}},"/api/data-rights/correction":{"post":{"summary":"Right of correction, files an incident","tags":["data-rights"],"security":[{"Session":[]}],"responses":{"200":{"description":"OK"}}}},"/api/data-rights/erasure":{"post":{"summary":"Right to erasure over principal_ref-linked records only, cascading delete + event anonymization","description":"Cascades only across rows carrying principal_id, i.e. events ingested with principal_ref. Events recorded before this account began sending principal_ref carry no data-subject identifier, cannot be attributed to any individual, and are not erased. Linkage is not retroactive. A 200 does not mean unlinked historical events were erased — read `outcome` and the `coverage` block. An account with no data-subject linkage receives 422 no_principal_linkage: nothing was erased and no records were searched. Signed-in owner only. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) it is refused 403 session_required before the body is read, and nothing is erased: an erasure removes part of the record of what agents did. Before 2026-10-05 any key of the owner could run one.","tags":["data-rights"],"security":[{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["principal_id","confirm"],"properties":{"principal_id":{"type":"string","format":"uuid"},"scope":{"type":"string","enum":["all","events","consents"]},"confirm":{"type":"boolean","enum":[true]}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"principal_id required, or confirm: true required"},"401":{"description":"forbidden: no signed-in session"},"403":{"description":"session_required: the request carried an API key"},"404":{"description":"principal not found"},"422":{"description":"no_principal_linkage"}}}},"/api/data-rights/grievance":{"post":{"summary":"Public grievance filing (no auth, IP rate-limited)","description":"Files the grievance, acknowledges it and fans it out to the operator's grievance channel and grievance_filed endpoints. The caller is not the operator, so registered_dispatches reports each delivery by endpoint_id without its url, and the acknowledgement emailed to contact_email names \"the operator\", never the email address on the operator account.","tags":["data-rights","public"],"parameters":[{"name":"owner_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"},"429":{"description":"Rate limited"}}}},"/api/data-rights/object":{"get":{"summary":"List active objections for a principal (GDPR Art 21)","tags":["data-rights"],"security":[{"Session":[]}],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Object to specific processing (GDPR Art 21)","tags":["data-rights"],"security":[{"Session":[]}],"responses":{"200":{"description":"OK"}}}},"/api/compliance/dpia":{"get":{"summary":"Live DPIA generation","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the document prints \"[operator name - visible when signed in]\" and \"[account contact email - visible when signed in]\" in place of the operator's name and contact address. The signed-in owner gets the real values. The sign-off name is a placeholder too.","tags":["compliance"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","schema":{"type":"string","enum":["md","json"]}}],"responses":{"200":{"description":"DPIA document"}}}},"/api/compliance/dpia-snapshots":{"get":{"summary":"Historical DPIA snapshots (from /api/cron/dpia-refresh)","tags":["compliance"],"security":[{"Session":[]}],"responses":{"200":{"description":"OK"}}}},"/api/compliance/ropa":{"get":{"summary":"Record of Processing Activities (GDPR Art 30 / DPDP §6(2))","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the document prints \"[operator name - visible when signed in]\" and \"[account contact email - visible when signed in]\" in place of the operator's name and contact address. The signed-in owner gets the real values.","tags":["compliance"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"responses":{"200":{"description":"ROPA document"}}}},"/api/compliance/sub-processors":{"get":{"summary":"Public sub-processor register for the VITNA product and website (GDPR Art 28(2))","tags":["compliance","public"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["md","json"]}}],"responses":{"200":{"description":"Register. The JSON view is schema v2: provider_terms_url and dpa_url, either of which can be null, replace public_dpa_url."}}}},"/api/compliance/pii-test":{"post":{"summary":"Dry-run PII/threat detection against a sample event (no persistence)","description":"sample_event.payload is read as /api/ingest reads it. A payload that is not a JSON object (text, a number, true or false, or an array) is kept whole as {\"value\": <the payload as sent>} and checked like any other field. Each threat found in the payload carries path, the exact place of the matched value (object keys as strings, array indices as numbers), beside field, which joins the key names for display. Redaction replaces the value at path, so a field whose key contains a dot or a bracket is replaced like any other. A match in a key carries in_key true, and its path and field are those of the object the key is in. A key in which the detectors find a secret or a regulated identifier is named \"[redacted: <label>]\" in the path and field of every threat below it, so no threat shows it. Before 2026-10-06 keys were not read. Each piece of text, list or object in the payload that lies more than 32 levels deep, after the first 1,000 items of a list, or after the first 10,000 pieces of text in the payload is not read by the detectors and is replaced as a whole with \"[redacted: beyond scan limit]\"; a number, true or false, or null there is kept, unless its key is past the text limit. At most 1,000 such parts in one request, across all its payloads, are replaced where they are; a payload whose parts would go past that is replaced whole instead, with {\"value\": \"[redacted: beyond scan limit]\"}. The keys of the objects in the payload are read too, each as a piece of text before its value, and count toward those 10,000. Where a key holds a leaked secret or a regulated identifier, the object it is in is replaced as a whole with \"[redacted: <label>]\", or, for a key at the top level of the payload, the whole payload with {\"value\": \"[redacted: <label>]\"}. A key after the first 10,000 pieces of text is not read, so it and the keys after it in its object, with their values, are replaced together by one entry, \"[redacted: beyond scan limit]\", whose value is how many entries it replaces, and count as one such part; when the object already has a key of that name before them, that key's value is replaced by the count. Before 2026-10-06 the detectors read 8 levels deep and the first 100 items of a list, read no keys, and left what lay beyond as sent. A payload with such parts has one threat of category scan_limit listed for it, however many parts were replaced: severity info, label \"beyond scan limit\", the path of the first such part (for keys past the text limit, the path of their object, with in_key true) and no match_preview. It counts once in the summary and raises no severity.","tags":["compliance","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Detection result: {applied_jurisdiction, threats[{category, severity, label, field, match_preview, in_key?}], threats_omitted, compliance_tags, max_severity, summary, redacted_payload, redacted_message, would_escalate}. threats lists at most 1,000; threats_omitted counts the rest. field is where the match was (\"message\" or \"payload.a.b[0]\"), cut at 200 characters with \"…\" after them when it is longer. Before 2026-10-06 every threat was listed with its whole field and its path, so one long key over many matches made the answer about 200 times the sample. match_preview does not show the text of a secret or regulated identifier the detectors find. For a threat of category secret_leak or pii it is the first 2 and last 2 characters of the match with \"****\" between them when the match has 16 or more characters (an AWS access key id reads like \"AK****LE\"), and \"[redacted: <label>]\" when it has fewer. For the attack categories (sql_destructive, shell_dangerous, prompt_injection, exfiltration, suspicious_network) it is the matched text, which a responder needs, with each secret or regulated identifier the detectors find in it replaced with \"[redacted: <label>]\", cut to 80 characters. Detection is heuristic: text the patterns do not recognise is shown as matched. Before 2026-10-06 match_preview was the matched text, cut to 80 characters, for every category, so this dry run returned the secrets it was sent."}}}},"/api/compliance/privacy-notice":{"get":{"summary":"Generate jurisdiction-templated privacy notice the Operator publishes","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the document prints \"[operator name - visible when signed in]\" and \"[account contact email - visible when signed in]\" in place of the operator's name and contact address. The signed-in owner gets the real values.","tags":["compliance"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["md","json"]}}],"responses":{"200":{"description":"Privacy notice"}}}},"/api/compliance/dpa-template":{"get":{"summary":"Generate Article 28-shape Data Processing Agreement template","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the document prints \"[operator name - visible when signed in]\" and \"[account contact email - visible when signed in]\" in place of the operator's name and contact address. The signed-in owner gets the real values.","tags":["compliance"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["md","json"]}}],"responses":{"200":{"description":"DPA template"}}}},"/api/compliance/health":{"get":{"summary":"Operator compliance posture roll-up (single JSON for the dashboard widget)","tags":["compliance"],"security":[{"Session":[]}],"responses":{"200":{"description":"Posture document"}}}},"/api/compliance/breach-classify":{"post":{"summary":"Decision-support: is this incident reportable per jurisdiction?","tags":["compliance","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Classification result"}}}},"/api/audit-log":{"get":{"summary":"Owner-scoped chronological audit trail (merged across 5 tables)","description":"An incident's summary is built from its title: a grievance's title and description hold the contact email of the person who filed it and what they wrote, so with an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) a grievance is shown with \"[grievance - details visible when signed in]\" in place of its title, and details_hidden true. The signed-in owner reads the title.","tags":["audit"],"security":[{"Session":[]}],"parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"kind","in":"query","schema":{"type":"string","description":"CSV: consent_access,consent_grant,consent_revoke,incident,breach_notification,dpia_snapshot"}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":1000}}],"responses":{"200":{"description":"Audit events"}}}},"/api/compliance/scc-annex-ii":{"get":{"summary":"Standard Contractual Clauses Annex II (technical + organisational measures)","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the document prints \"[operator name - visible when signed in]\" and \"[account contact email - visible when signed in]\" in place of the operator's name and contact address. The signed-in owner gets the real values.","tags":["compliance"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["md","json"]}}],"responses":{"200":{"description":"Annex II document"}}}},"/api/compliance/dpia-threshold-check":{"post":{"summary":"Decision-support: is a DPIA mandatory for this processing? (9-criterion WP29 check)","tags":["compliance","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Threshold decision"}}}},"/api/owners/notification-prefs/test":{"post":{"summary":"Fire a synthetic notification to verify operator's configured channel works","description":"Signed-in owner only. A request carrying an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included) is refused 403 session_required before anything is sent: the test posts to the owner's channels and reports where it sent.","tags":["owner"],"security":[{"Session":[]}],"responses":{"200":{"description":"Dispatched"},"403":{"description":"session_required: an API key cannot run the test"}}}},"/api/owners/grievance-officer":{"get":{"summary":"Read operator's Grievance Officer config (DPDP §9)","description":"Signed-in owner: the officer as stored, or fallback_email (the account address) while none is set. API key: officer_configured and officer_label only, never the officer's name, address or phone and never the account address.","tags":["owner","india"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"responses":{"200":{"description":"Officer config or null (session); officer_configured (API key)"}}},"post":{"summary":"Set operator's Grievance Officer (DPDP §9 / LGPD Art 41)","description":"Signed-in owner: always. API key: only while no officer is set; once one is set, a key is refused 403 session_required, so an agent can designate the first officer but never replace the owner's choice.","tags":["owner","india"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"responses":{"200":{"description":"Recorded"},"403":{"description":"session_required: an officer is already set and only the signed-in owner can replace it"}}}},"/api/pricing/local":{"get":{"summary":"Plan prices in local currency + SOL equivalent","tags":["public","pricing"],"parameters":[{"name":"country","in":"query","schema":{"type":"string","description":"ISO-3166 alpha-2"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Localized prices"}}}},"/api/payment-methods/india":{"get":{"summary":"Public guide: how Indian customers pay for VITNA (Razorpay UPI / PayPal)","tags":["public","india"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Payment guide"}}}},"/api/compliance/india-status":{"get":{"summary":"Public DPDP / India compliance posture (live vs planned per capability)","tags":["public","compliance","india"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Posture document"}}}},"/api/compliance/cross-border-notice":{"get":{"summary":"DPDP §16 cross-border transfer notice the operator distributes to data principals","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the document prints \"[operator name - visible when signed in]\" and \"[account contact email - visible when signed in]\" in place of the operator's name and contact address. The signed-in owner gets the real values. The JSON form's controller field is the name placeholder.","tags":["compliance","india"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["md","json"]}}],"responses":{"200":{"description":"Notice document"}}}},"/api/compliance/dpdp-checklist":{"get":{"summary":"Per-operator DPDP requirement checklist (configured/pending/N/A per §)","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the grievance officer row says configured or pending, and where it names the officer it prints \"[officer name - visible when signed in]\" and \"[officer email - visible when signed in]\" instead. The signed-in owner sees the officer's name and address.","tags":["compliance","india"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Checklist"}}}},"/api/compliance/na-status":{"get":{"summary":"Public NA compliance posture (Canada PIPEDA + US sectoral + state matrix)","tags":["compliance","public","north-america"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Posture"}}}},"/api/compliance/us-state-matrix":{"get":{"summary":"US state privacy law matrix (16 states with criteria, rights, DPIA reqs, UOOM support)","tags":["compliance","public","north-america"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Matrix"}}}},"/api/compliance/state-breach-deadlines":{"get":{"summary":"US state breach notification deadlines + AG recipient registry","tags":["compliance","public","north-america"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}},{"name":"state","in":"query","schema":{"type":"string","description":"ISO 2-letter state code"}}],"responses":{"200":{"description":"Deadlines"}}}},"/api/compliance/nist-pf-posture":{"get":{"summary":"NIST Privacy Framework v1.0 posture for the operator (5 Functions, signal-driven scoring)","tags":["compliance","north-america"],"security":[{"Session":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Posture"}}}},"/api/compliance/sectoral-check":{"post":{"summary":"US sectoral law applicability (HIPAA / GLBA / COPPA / FERPA / FCRA / SOX)","tags":["compliance","tools","north-america"],"security":[{"Session":[]}],"responses":{"200":{"description":"Sectoral matches"}}}},"/api/compliance/indigenous-data":{"get":{"summary":"COSTRINITY's public Indigenous data sovereignty position (CARE, UNDRIP)","tags":["compliance","public","north-america"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Position document"}}}},"/api/india/aadhaar-mask":{"post":{"summary":"Mask + Verhoeff-validate an Aadhaar (server-side; no persistence)","tags":["india","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Masked + reference token"}}}},"/api/india/verhoeff-verify":{"post":{"summary":"Verhoeff D5 checksum verification for 12-digit numbers (no persistence)","tags":["india","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validity"}}}},"/api/india/pan-classify":{"post":{"summary":"PAN entity-type classification from 4th character","tags":["india","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Entity classification"}}}},"/api/india/upi-validate":{"post":{"summary":"UPI handle format + PSP recognition","tags":["india","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"UPI validation"}}}},"/api/india/gstn-validate":{"post":{"summary":"GSTIN format + check-digit + state-code lookup","tags":["india","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"GSTIN validation"}}}},"/api/india/regulators":{"get":{"summary":"Public directory of India sectoral regulators (RBI/SEBI/IRDAI/TRAI/etc.)","tags":["india","public"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}},{"name":"sector","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Regulator directory"}}}},"/api/compliance/india-sectoral-check":{"post":{"summary":"Indian sectoral regulator applicability (RBI/SEBI/IRDAI/TRAI/PFRDA)","tags":["compliance","india","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Sectoral matches"}}}},"/api/compliance/india-cross-border-countries":{"get":{"summary":"DPDP §16 cross-border destination country registry","tags":["compliance","india","public"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}},{"name":"country","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Country registry"}}}},"/api/india/mobile-validate":{"post":{"summary":"Indian mobile +91 format + operator/circle hint (MNP-caveated)","tags":["india","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/india/ifsc-validate":{"post":{"summary":"IFSC format + bank code recognition (~50 banks)","tags":["india","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/india/pincode-validate":{"post":{"summary":"PIN code format + region/state hint (India Post numbering plan)","tags":["india","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/canada/sin-validate":{"post":{"summary":"Canadian SIN format + Luhn checksum + series-region hint (no persistence)","tags":["canada","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/compliance/ca-province-matrix":{"get":{"summary":"Canadian provincial privacy law matrix (PIPEDA + Loi 25 + Alberta/BC PIPA + Manitoba/Ontario PHIA)","tags":["compliance","canada","public","north-america"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}},{"name":"province","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Matrix"}}}},"/api/compliance/quebec-law25":{"get":{"summary":"Per-operator Quebec Law 25 (Loi 25) readiness scorecard","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the grievance officer row says configured or pending, and where it names the officer it prints \"[officer name - visible when signed in]\" and \"[officer email - visible when signed in]\" instead. The signed-in owner sees the officer's name and address.","tags":["compliance","canada"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Readiness"}}}},"/api/compliance/soc2-readiness":{"get":{"summary":"SOC 2 Trust Service Criteria readiness scorecard (pre-audit)","tags":["compliance","enterprise","north-america"],"security":[{"Session":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Readiness"}}}},"/api/japan/mynumber-validate":{"post":{"summary":"Japan My Number 12-digit format + mod-11 checksum (no persistence)","tags":["japan","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/nigeria/nin-validate":{"post":{"summary":"Nigeria NIN format validator (no persistence)","tags":["nigeria","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/nigeria/bvn-validate":{"post":{"summary":"Nigeria BVN format validator (no persistence)","tags":["nigeria","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/eu/iban-validate":{"post":{"summary":"IBAN mod-97 (ISO 7064) validator + country length lookup","tags":["eu","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/eu/vat-validate":{"post":{"summary":"EU VAT format validator per member state pattern","tags":["eu","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/compliance/appi-readiness":{"get":{"summary":"Japan APPI readiness scorecard (Art 17/18/21/23/26/28/30/35/40)","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the grievance officer row says configured or pending, and where it names the officer it prints \"[officer name - visible when signed in]\" and \"[officer email - visible when signed in]\" instead. The signed-in owner sees the officer's name and address.","tags":["compliance","japan"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Readiness"}}}},"/api/compliance/ndpa-readiness":{"get":{"summary":"Nigeria NDPA 2023 readiness scorecard (§24/25/26/27/28/32/33/40/41)","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the grievance officer row says configured or pending, and where it names the officer it prints \"[officer name - visible when signed in]\" and \"[officer email - visible when signed in]\" instead. The signed-in owner sees the officer's name and address.","tags":["compliance","nigeria"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Readiness"}}}},"/api/compliance/ai-act-classify":{"post":{"summary":"EU AI Act risk classifier (prohibited / high-risk / limited / minimal + GPAI obligations)","tags":["compliance","eu","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Classification"}}}},"/api/compliance/dsa-status":{"post":{"summary":"EU Digital Services Act applicability (intermediary / hosting / online platform / VLOP)","tags":["compliance","eu","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"DSA tier"}}}},"/api/compliance/japan-cross-border":{"get":{"summary":"Japan APPI Art 28 cross-border country registry","tags":["compliance","japan","public"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}},{"name":"country","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Registry"}}}},"/api/compliance/global-status":{"get":{"summary":"Master endpoint listing every privacy/security/sectoral regime VITNA covers","tags":["compliance","public"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Global status"}}}},"/api/compliance/hipaa-readiness":{"get":{"summary":"HIPAA Business Associate readiness scorecard (45 CFR §§164.308-316)","tags":["compliance","enterprise"],"security":[{"Session":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Readiness"}}}},"/api/compliance/iso27001-readiness":{"get":{"summary":"ISO/IEC 27001:2022 Annex A readiness (22 controls evaluated)","tags":["compliance","enterprise"],"security":[{"Session":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Readiness"}}}},"/api/compliance/pci-dss-readiness":{"get":{"summary":"PCI DSS v4.0 readiness (12 requirements)","tags":["compliance","enterprise"],"security":[{"Session":[]}],"responses":{"200":{"description":"Readiness"}}}},"/api/compliance/uk-gdpr-readiness":{"get":{"summary":"UK GDPR + DPA 2018 readiness","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the DPO row says configured or pending, and where it names the officer it prints \"[officer name - visible when signed in]\" instead. The signed-in owner sees the officer's name.","tags":["compliance","uk"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Readiness"}}}},"/api/compliance/pipa-kr-readiness":{"get":{"summary":"South Korea PIPA readiness (24h breach window)","tags":["compliance","korea"],"security":[{"Session":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Readiness"}}}},"/api/compliance/app-au-readiness":{"get":{"summary":"Australia Privacy Act + 13 APPs readiness","tags":["compliance","australia"],"security":[{"Session":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Readiness"}}}},"/api/compliance/pipl-cn-readiness":{"get":{"summary":"China PIPL readiness scorecard","tags":["compliance","china"],"security":[{"Session":[]}],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","md"]}}],"responses":{"200":{"description":"Readiness"}}}},"/api/uk/nin-validate":{"post":{"summary":"UK National Insurance Number validator (no persistence)","tags":["uk","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/korea/rrn-validate":{"post":{"summary":"Korea RRN 13-digit + weighted mod-11 + century/sex decode (no persistence)","tags":["korea","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/australia/tfn-validate":{"post":{"summary":"Australia TFN + weighted mod-11 checksum (no persistence)","tags":["australia","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/australia/abn-validate":{"post":{"summary":"Australia ABN + first-1 + mod-89 checksum (no persistence)","tags":["australia","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/china/id-card-validate":{"post":{"summary":"China ID Card 18-char + ISO 7064 mod-11-2 + region/birth/sex decode (no persistence)","tags":["china","tools"],"security":[{"Session":[]}],"responses":{"200":{"description":"Validation result"}}}},"/api/changelog":{"get":{"summary":"Public VITNA changelog (hand-curated operator-facing milestones)","tags":["public"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["md","json"]}},{"name":"since","in":"query","schema":{"type":"string","description":"Filter to phase ≥ N"}}],"responses":{"200":{"description":"Changelog"}}}},"/api/.well-known/openapi":{"get":{"summary":"This document, OpenAPI 3.0 spec covering the Phase 1 surface","tags":["public","meta"],"parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","yaml"]}}],"responses":{"200":{"description":"OpenAPI document"}}}},"/api/consent/request":{"post":{"summary":"Record a new consent artifact","tags":["consent"],"security":[{"Session":[]}],"responses":{"200":{"description":"OK"}}}},"/api/consent/revoke":{"post":{"summary":"Revoke a specific consent artifact","tags":["consent"],"security":[{"Session":[]}],"responses":{"200":{"description":"OK"}}}},"/api/consent/active":{"get":{"summary":"List active (unrevoked, unexpired) consents","tags":["consent"],"security":[{"Session":[]}],"responses":{"200":{"description":"OK"}}}},"/api/consent/check":{"get":{"summary":"Pre-flight enforcement, is processing allowed?","tags":["consent"],"security":[{"Session":[]}],"parameters":[{"name":"principal_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"principal_ref","in":"query","schema":{"type":"string"}},{"name":"purpose","in":"query","required":true,"schema":{"type":"string"}},{"name":"category","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsentCheckResult"}}}}}},"post":{"summary":"Pre-flight enforcement (POST variant)","tags":["consent"],"security":[{"Session":[]}],"responses":{"200":{"description":"OK"}}}},"/api/consent/renew":{"post":{"summary":"Extend an active consent's expiry","tags":["consent"],"security":[{"Session":[]}],"responses":{"200":{"description":"OK"}}}},"/api/consent/bulk-revoke":{"post":{"summary":"Revoke ALL active consents for a principal","tags":["consent"],"security":[{"Session":[]}],"responses":{"200":{"description":"OK"}}}},"/api/consent/receipt":{"get":{"summary":"Downloadable Kantara-shaped consent receipt","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the document prints \"[operator name - visible when signed in]\" and \"[account contact email - visible when signed in]\" in place of the operator's name and contact address. The signed-in owner gets the real values. The JSON form's data_controller.name is the name placeholder.","tags":["consent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"consent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","schema":{"type":"string","enum":["md","json"]}}],"responses":{"200":{"description":"OK"}}}},"/api/sahamati/consent-handle":{"post":{"summary":"Sahamati Account Aggregator consent webhook","tags":["consent","india"],"parameters":[{"name":"x-sahamati-signature","in":"header","schema":{"type":"string"},"description":"JWS header.payload.signature (RS256). Verified against SAHAMATI_PUBKEY_PEM when set."}],"responses":{"200":{"description":"OK"},"401":{"description":"Signature invalid"}}}},"/api/owners":{"post":{"summary":"Sign up / create owner (region-aware)","tags":["owner"],"responses":{"201":{"description":"Created"}}}},"/api/owners/delete-account":{"post":{"summary":"Full operator account deletion (DPDP §12 / GDPR Art 17)","description":"Signed-in owner only, with confirm: true in the body. A request carrying an API key (any header form, a restricted trial key included, even beside a session cookie) is refused 403 session_required and deletes nothing.","tags":["owner"],"security":[{"Session":[]}],"responses":{"200":{"description":"Deleted"},"400":{"description":"confirm: true required"},"401":{"description":"session_required: no signed-in session"},"403":{"description":"session_required: the request carried an API key. Or the owner_id is not the signed-in owner"}}}},"/api/owners/detection-settings":{"get":{"summary":"Read the owner's detection settings (which checks are on, block / warn / log_only per category, what is recorded)","description":"Readable by the signed-in owner and by an API key: the settings are not a secret and hold no owner data. Defaults when never saved.","tags":["owner"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{settings}"},"401":{"description":"forbidden: no valid session or key"}}},"put":{"summary":"Change the owner's detection settings (signed-in owner only)","description":"Signed-in owner only, from the signed-in browser. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) it is refused 403 session_required and nothing is saved, because a key that could set a check to log_only or turn recording off would switch off the checks on itself and on every other agent on the account. Before 2026-10-05 any key of the owner could change them.","tags":["owner"],"security":[{"Session":[]}],"parameters":[{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"checks":{"type":"object","additionalProperties":{"type":"boolean"}},"actions":{"type":"object","additionalProperties":{"type":"string","enum":["block","warn","log_only"]}},"default_action":{"type":"string","enum":["block","warn","log_only"]},"audit":{"type":"object","properties":{"record_allowed":{"type":"boolean"},"record_blocked":{"type":"boolean"}}}}}}}},"responses":{"200":{"description":"{settings} as saved, after validation"},"401":{"description":"forbidden: no signed-in session, or the owner_id is not the signed-in owner"},"403":{"description":"session_required: the request carried an API key"}}}},"/api/owners/notification-prefs":{"get":{"summary":"Read operator notification preferences","description":"Signed-in owner: each channel's email and webhook_url, defaults_from_owner_email, last_test_at and email_off. API key: view \"api_key\" with only webhook_configured and email_configured per channel, last_test_at and email_off; no webhook URL and no email address, the account's included.","tags":["owner"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Update operator notification preferences (signed-in owner only)","description":"Only the signed-in owner sets the alert channel, the breach and grievance channels and email_off; the dashboard's Alert settings, Breach alerts and Grievance alerts forms use this. A request carrying an API key (any header form, a restricted trial key included, even beside a session cookie) is refused 403 session_required and writes nothing. An agent that wants its own copy of its alerts opens its session with webhook_url on /api/engagement/open: alerts then go to the owner's channel and that URL. Merges field by field: a channel left out is kept; in a channel that is sent, a field that is sent replaces that field, a field left out is kept, and null or \"\" clears it; a channel sent as null is cleared; email_off left out is kept. webhook_url must be a public https URL (the same SSRF rules as /api/engagement/open).","tags":["owner"],"security":[{"Session":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"alert":{"type":"object","nullable":true,"properties":{"email":{"type":"string","nullable":true},"webhook_url":{"type":"string","nullable":true}}},"breach":{"type":"object","nullable":true,"properties":{"email":{"type":"string","nullable":true},"webhook_url":{"type":"string","nullable":true}}},"grievance":{"type":"object","nullable":true,"properties":{"email":{"type":"string","nullable":true},"webhook_url":{"type":"string","nullable":true}}},"email_off":{"type":"boolean","description":"true stops every email notice to the owner, sign-in codes included. Webhooks and the dashboard are unaffected."}}}}}},"responses":{"200":{"description":"{ok, owner_id, prefs} (the stored prefs after the merge)"},"400":{"description":"A webhook_url that is not a public https URL, a malformed email, a non-boolean email_off or a channel that is not an object or null. Nothing is written."},"401":{"description":"No session"},"403":{"description":"session_required: an API key cannot change notification settings"}}}},"/api/incidents/breach-notify":{"get":{"summary":"Read notification state for an incident","description":"Returns the incident's title, severity and category with its deadline. The route takes any incident id: a grievance's title and description hold the contact email of the person who filed it and what they wrote, so with an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) a grievance is shown with \"[grievance - details visible when signed in]\" in place of its title, and details_hidden true. The signed-in owner reads the title.","tags":["breach"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"responses":{"200":{"description":"OK"}}},"post":{"summary":"Queue a breach notification (per-jurisdiction window)","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) the owner's own regulator-draft copy is reported as 'account email', and each entry of registered_dispatches has url null and url_hidden true: breach endpoints are the owner's. The signed-in owner sees the addresses. A notice carries the incident's title and description, so a key is refused for a grievance (403 session_required, nothing sent): only the signed-in owner can send a notice for one.","tags":["breach"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"responses":{"200":{"description":"OK"},"403":{"description":"session_required: an API key asked for a grievance incident"}}}},"/api/incidents":{"get":{"summary":"List the incidents on the account, newest first","description":"Returns each incident's columns. One exception: a grievance's title and description hold the contact email of the person who filed it and what they wrote, so with an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) a grievance is shown with \"[grievance - details visible when signed in]\" in place of its title, and details_hidden true. The signed-in owner reads the title. A key still reads a grievance's id, category, severity, status and times, with description null, and the response carries a details_note. Other categories are returned whole.","tags":["breach"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"owner_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","maximum":200}}],"responses":{"200":{"description":"{incidents:[...], details_note?}"},"401":{"description":"forbidden"}}},"post":{"summary":"File an incident from an agent (API key)","tags":["breach"],"security":[{"ApiKey":[]},{"Bearer":[]}],"responses":{"201":{"description":"{incident}"},"400":{"description":"title is required"},"401":{"description":"Missing or invalid API key"}}}},"/api/upgrade/razorpay/order":{"post":{"summary":"Create Razorpay INR order (DPDP-IN owners). Switched off: there is no merchant account, so it answers 503 and nothing is sent to Razorpay.","tags":["billing","india"],"security":[{"Session":[]}],"responses":{"200":{"description":"Order created"},"503":{"description":"Skip-mode — Razorpay env triple not configured"}}}},"/api/webhooks/razorpay":{"post":{"summary":"Razorpay payment webhook","tags":["billing","india"],"parameters":[{"name":"x-razorpay-signature","in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"description":"Signature invalid"}}}},"/api/webhooks/compliance/register":{"post":{"summary":"Register a compliance webhook endpoint","description":"Operator registers a URL to receive a specific compliance event kind (breach_notification, grievance_filed, consent_revoked, data_subject_request, cross_border_transfer, dpia_threshold_triggered). Deliveries are signed with HMAC-SHA256 and audit-logged. Today VITNA sends only breach_notification and grievance_filed to registered endpoints, and only the signed-in owner can register those two, because their payloads concern people's personal data. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) a request for either is refused 403 session_required and nothing is stored. A key may register the other four kinds.","tags":["webhooks","compliance"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"responses":{"200":{"description":"Endpoint registered"},"400":{"description":"Invalid event_kind or url"},"401":{"description":"Auth required"},"403":{"description":"session_required: an API key asked for breach_notification or grievance_filed"}}}},"/api/webhooks/compliance/list":{"get":{"summary":"List registered compliance webhook endpoints","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) breach_notification and grievance_filed endpoints are listed with url and description null and url_hidden true. The signed-in owner sees every address.","tags":["webhooks","compliance"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"event_kind","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"description":"Auth required"}}}},"/api/webhooks/compliance/test":{"post":{"summary":"Fire a test compliance webhook","description":"Sends a synthetic breach_notification payload (data.test=true) to the endpoint named by endpoint_id, or with none named to every active breach_notification endpoint. Helps operators verify their receiver before the first real event. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) a request naming a breach_notification or grievance_filed endpoint, or naming none, is refused 403 session_required and nothing is sent; a key may test an endpoint of another kind by its endpoint_id.","tags":["webhooks","compliance"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"responses":{"200":{"description":"OK"},"401":{"description":"Auth required"},"403":{"description":"session_required: only the signed-in owner tests breach and grievance endpoints"}}}},"/api/webhooks/compliance/deliveries":{"get":{"summary":"Read compliance webhook delivery audit log","description":"Returns every delivery attempt (delivered + failed + SSRF-refused) with payload_sha256 for tamper-evidence. The regulator-facing proof log. Read with an API key, a row posted to the owner's notification channels (alert, breach, grievance, or their test), to a session's webhook_url, or to a breach_notification or grievance_filed endpoint has url null and url_hidden true, so a key never reads the owner's webhook addresses; rows for endpoints of the other kinds keep their url. The signed-in owner sees every url.","tags":["webhooks","compliance","audit"],"security":[{"ApiKey":[]},{"Session":[]}],"parameters":[{"name":"event_kind","in":"query","schema":{"type":"string"}},{"name":"delivered","in":"query","schema":{"type":"boolean"}},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":500}}],"responses":{"200":{"description":"OK"},"401":{"description":"Auth required"}}}},"/api/alerts":{"get":{"summary":"List an agent's alert rules","description":"With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) each rule has notify_email_set and notify_webhook_set (true or false) in place of notify_email and notify_webhook. The signed-in owner sees both.","tags":["alerts"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Rules"},"401":{"description":"Auth required"},"404":{"description":"Agent not found"}}},"post":{"summary":"Create an alert rule (cost_exceeded, error_rate, no_activity, anomaly_risk)","description":"Recipients are the owner's. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) a request with a non-empty notify_email or notify_webhook is refused 403 session_required and nothing is stored; a key may create a rule without them, and such a rule records when it fires but sends nothing. notify_webhook must be a public https URL (the SSRF rules notification-prefs uses).","tags":["alerts"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"responses":{"201":{"description":"Created"},"400":{"description":"Missing fields, unknown condition_type, or notify_webhook not a public https URL"},"403":{"description":"session_required: an API key sent a recipient"}}},"delete":{"summary":"Delete an alert rule","description":"Signed-in owner only. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) every rule is refused 403 session_required before the rule is looked up, and nothing is deleted: deleting a rule silences it, whoever it sends to, and a rule on another agent is that agent's oversight. A key can still create a rule. No route edits a rule, so nothing lets a key raise a threshold or switch a rule off. Before 2026-10-05 a key could delete a rule that had no recipient.","tags":["alerts"],"security":[{"Session":[]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted"},"401":{"description":"forbidden: no signed-in session"},"403":{"description":"session_required: the request carried an API key"},"404":{"description":"Not found"}}}},"/api/licenses/mine":{"get":{"summary":"The signed-in owner's VITNA Desktop licence key","description":"Signed-in owner only. A request carrying an API key (any header form, a restricted trial key included, even beside a session cookie) is refused 403 session_required and nothing is read. VITNA Desktop does not call this route: it activates a pasted key with POST /api/licenses/activate.","tags":["billing","owner"],"security":[{"Session":[]}],"responses":{"200":{"description":"{ license_key, tier, expires_at } or { license_key: null }"},"403":{"description":"session_required"}}},"post":{"summary":"Regenerate the licence key ({ \"regenerate\": true }), revoking the old one","description":"Signed-in owner only, as GET. A request carrying an API key is refused 403 session_required before anything is minted or revoked.","tags":["billing","owner"],"security":[{"Session":[]}],"responses":{"200":{"description":"{ license_key, tier, expires_at }"},"400":{"description":"No paid plan, or nothing to do"},"403":{"description":"session_required"}}}},"/api/cron/check-alerts":{"get":{"summary":"Scheduled alert + heartbeat evaluation (5-min Vercel cron)","tags":["cron"],"security":[{"CronSecret":[]}],"responses":{"200":{"description":"OK"},"401":{"description":"Unauthorized"}}}},"/api/cron/dpia-refresh":{"get":{"summary":"Scheduled DPIA snapshot refresh (weekly Sunday cron)","tags":["cron"],"security":[{"CronSecret":[]}],"responses":{"200":{"description":"OK"}}}},"/api/ingest":{"post":{"summary":"Ingest agent event (single or batch)","description":"An event's payload is taken from payload, or from data, details, metadata, context or extra, and may be any JSON value. A JSON object is stored as sent, with the tracing and tool fields the event names added to it. A payload that is not a JSON object (text, a number, true or false, or an array) is kept whole as {\"value\": <the payload as sent>} and checked like any other field. Before the event is stored, each payload field in which the detectors find a leaked secret or a regulated identifier is replaced with \"[redacted: <label>]\", whatever its key is called (a key may contain dots, brackets or quotes, or be empty). Each piece of text, list or object in the payload that lies more than 32 levels deep, after the first 1,000 items of a list, or after the first 10,000 pieces of text in the payload is not read by the detectors and is replaced as a whole with \"[redacted: beyond scan limit]\"; a number, true or false, or null there is kept, unless its key is past the text limit. At most 1,000 such parts in one request, across all its payloads, are replaced where they are; a payload whose parts would go past that is replaced whole instead, with {\"value\": \"[redacted: beyond scan limit]\"}. The keys of the objects in the payload are read too, each as a piece of text before its value, and count toward those 10,000. Where a key holds a leaked secret or a regulated identifier, the object it is in is replaced as a whole with \"[redacted: <label>]\", or, for a key at the top level of the payload, the whole payload with {\"value\": \"[redacted: <label>]\"}. A key after the first 10,000 pieces of text is not read, so it and the keys after it in its object, with their values, are replaced together by one entry, \"[redacted: beyond scan limit]\", whose value is how many entries it replaces, and count as one such part; when the object already has a key of that name before them, that key's value is replaced by the count. Before 2026-10-06 the detectors read 8 levels deep and the first 100 items of a list, read no keys, and left what lay beyond as sent. The tracing and tool fields added to the payload are part of it, so they are replaced with it. A payload with any such part counts as one match, labelled \"beyond scan limit\", in the summary added to the message, however many parts were replaced, and raises no severity; events stored before 2026-10-06 are unchanged. Before 2026-10-05 a payload sent as text was stored one character per key and was not checked, a number or true or false was dropped, and a secret under a key containing a dot or a bracket was stored as sent. Events stored before that are unchanged. An event's type is read from event_type or, when that is absent or null, from the first of action, type, kind, event and name that is present, and it must be text: an event whose type is a list, an object, a number, or true or false is refused with 400 event_type_not_text, and nothing in the request is stored, the rest of a batch included. Before 2026-10-06 such a type was not checked: one event failed with 500, and in a batch it could be stored as sent. An event cannot claim to be a command from the owner: an event whose type is command (from event_type, action, type, kind, event or name, in any letter case), or that carries issued_by dashboard at its top level or at its payload's, is refused with 400 owner_command_refused, and nothing in the request is stored, the rest of a batch included. Only POST /api/commands, for the signed-in owner, stores a command. Before 2026-10-06 an agent could store an event of type command with issued_by dashboard here, and its own live stream sent it back as a command frame.","tags":["ingest"],"security":[{"ApiKey":[]},{"Bearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"description":"One event (the properties below), an array of events (up to 500), or {events: [...]}. Each event may carry more fields than are listed here (status, tool_name, run_id and others).","properties":{"event_type":{"type":"string","description":"Text. When it is absent or null, the first of action, type, kind, event and name that is present is read instead. A type that is not text is refused with 400 event_type_not_text."},"message":{"type":"string","nullable":true},"payload":{"description":"Optional. Any JSON value. A payload that is not a JSON object (text, a number, true or false, or an array) is kept whole as {\"value\": <the payload as sent>} and checked like any other field.","nullable":true},"storage_mode":{"type":"string","enum":["cloud","local"]}}}}}},"responses":{"200":{"description":"OK. Single event: { success, storage_mode, event_id, timestamp }; batch (up to 500): { success, storage_mode, count, events }. With storage_mode=local the events are checked and returned, not stored."},"202":{"description":"Every event in the batch was a heartbeat and the agent blocks heartbeats; nothing stored"},"400":{"description":"wrapper_field_invalid: a wrapped batch ({agent_name, events: [...]} and the like) has an agent_name, source, service, run_id or session_id that is neither a number nor text of at most 256 characters. Ingest copies those into every event, so it is refused as {error, message, refused: [<field>, ...]} and nothing is stored (2026-10-06). event_type_not_text: an event's type is not text. owner_command_refused: an event claims to be a command from the owner. Either is {error, message, refused: [{index, field}]}, where index is the event's place among the events read from the body; field is where the type was read from (event_type, action, type, kind, event or name) for event_type_not_text, and event_type, issued_by or payload.issued_by for owner_command_refused. A request with an event whose type is not text is answered event_type_not_text. Nothing in the request is stored."},"401":{"description":"Invalid API key"},"402":{"description":"The batch would exceed the agent's SOL budget; a budget_exceeded event is recorded instead"},"413":{"description":"Body or batch too large"},"429":{"description":"Rate limited"}}}},"/api/agents/rotate":{"post":{"summary":"Rotate an agent's API key. The old key stops working at once and the new one is returned once.","description":"The signed-in owner can rotate the key of any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be rotated. Asked for another agent it answers 403 session_required and changes nothing, because the answer would hand one agent another's credential.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["agent_id"],"properties":{"agent_id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"{success, api_key, message}"},"400":{"description":"agent_id required"},"401":{"description":"forbidden: no valid session or key"},"403":{"description":"forbidden: the agent is not on the caller's account. session_required: an API key asked to rotate another agent's key"}}}},"/api/agents":{"post":{"summary":"Create an agent. Its API key is returned once.","description":"Signed-in owner only: the owner creates an agent with New Agent in the dashboard. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) no agent can be created: it answers 403 session_required before the owner is looked up and creates nothing, so an agent cannot make a new agent without its own limits and carry on as it. Before 2026-10-05 a key on any account not on the free plan could create an agent here.","tags":["agent"],"security":[{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"description":{"type":"string"},"owner_id":{"type":"string","format":"uuid"}}}}}},"responses":{"201":{"description":"{agent: {id, name, description, api_key_prefix, created_at, last_seen, api_key}, key_shown_once: true}. The api_key is shown in this response only."},"400":{"description":"name required"},"401":{"description":"no signed-in session, or the owner_id is not the signed-in owner"},"403":{"description":"session_required: the request carried an API key. Or the free plan's agent limit is reached"},"404":{"description":"Owner not found"}}},"delete":{"summary":"Delete an agent. Its key stops working and its events are deleted with it.","description":"Signed-in owner only: the owner can delete any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) no agent can be deleted, its own included: it answers 403 session_required before the agent is looked up and deletes nothing, because deleting an agent destroys the record of what it did, and deleting another would lock that agent out. Before 2026-10-05 a key could delete its own agent.","tags":["agent"],"security":[{"Session":[]}],"parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{success: true}"},"400":{"description":"valid id required"},"401":{"description":"forbidden: no signed-in session, or the owner_id is not the signed-in owner"},"403":{"description":"session_required: the request carried an API key. forbidden: the agent is not on the signed-in owner's account"}}}},"/api/agents/config":{"patch":{"summary":"Change an agent's heartbeat interval, budget caps, anomaly thresholds and block_heartbeats.","description":"The signed-in owner can change the settings of any agent on the account, in either direction. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent's settings can be changed. Asked for another agent it answers 403 session_required and changes nothing: these settings decide which of an agent's events are stored (block_heartbeats drops heartbeats, a budget cap refuses costed events with 402). On its own agent a key may only tighten: lower a budget cap or set one where there was none, shorten the heartbeat interval or set one, set block_heartbeats to false, or lower an anomaly threshold (a null threshold counts as the default, 15 for the error rate and 3 for the cost multiplier). Raising or clearing a cap, lengthening or clearing the heartbeat interval, setting block_heartbeats to true or raising an anomaly threshold answers 403 session_required with the fields named in loosens, and nothing is written, even when the same request also tightens something. Before 2026-10-05 a key could change its own agent's settings in either direction.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["agent_id","owner_id"],"properties":{"agent_id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"heartbeat_interval_seconds":{"type":"integer","nullable":true},"budget_sol_per_hour":{"type":"number","nullable":true},"budget_sol_per_day":{"type":"number","nullable":true},"anomaly_error_rate_threshold":{"type":"number","nullable":true},"anomaly_cost_multiplier":{"type":"number","nullable":true},"block_heartbeats":{"type":"boolean"}}}}}},"responses":{"200":{"description":"{success, agent} with the configuration fields only"},"400":{"description":"a field is out of range, or nothing to update"},"401":{"description":"no valid session or key"},"403":{"description":"the owner_id is not the caller's. session_required: an API key asked to change another agent's settings, or to loosen its own agent's limits ({error, note, loosens: [field names]})"},"404":{"description":"the agent is not on the caller's account"}}}},"/api/events":{"get":{"summary":"Read one agent's events, newest first (the dashboard timeline).","description":"The signed-in owner can read any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be read: asked for another agent of the same owner it answers 403 session_required with a note, after the tenant check and before anything of that agent is read. Before 2026-10-06 any key of the owner could read any of the owner's agents here.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":10000}},{"name":"offset","in":"query","schema":{"type":"integer","default":0}},{"name":"type","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"exclude_system","in":"query","schema":{"type":"string","enum":["1","true"]}},{"name":"category","in":"query","schema":{"type":"string"},"description":"Comma list of governed_action, lifecycle, activity, system"}],"responses":{"200":{"description":"{events: [{id, event_type, status, payload, cost_sol, cost_usd, message, created_at}], pagination}"},"400":{"description":"valid agent_id required"},"401":{"description":"no valid session or key"},"403":{"description":"the owner_id is not the caller's. session_required: an API key asked for another agent's events"},"404":{"description":"the agent is not on the caller's account"}}},"delete":{"summary":"Delete every event of one agent.","description":"Signed-in owner only: the owner can delete the events of any agent on the account (the dashboard's clear-events button). With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) no agent's events can be deleted, its own included: it answers 403 session_required before anything is looked up and deletes nothing, because those events are the record of what the agent did. Before 2026-10-05 a key could delete its own agent's events.","tags":["agent"],"security":[{"Session":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{success: true}"},"400":{"description":"valid agent_id required"},"401":{"description":"no signed-in session"},"403":{"description":"session_required: the request carried an API key. Or the owner_id is not the signed-in owner"},"404":{"description":"the agent is not on the caller's account"}}}},"/api/events/system":{"get":{"summary":"Count one agent's system events by type, before deleting them.","description":"The signed-in owner can read any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be read: asked for another agent of the same owner it answers 403 session_required with a note, after the tenant check and before anything of that agent is read. Before 2026-10-06 any key of the owner could read any of the owner's agents here.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{total, by_type, system_event_types}"},"400":{"description":"valid agent_id required"},"401":{"description":"no valid session or key"},"403":{"description":"the owner_id is not the caller's. session_required: an API key asked for another agent's"},"404":{"description":"the agent is not on the caller's account"}}},"delete":{"summary":"Delete one agent's system events (heartbeat_missed, budget_warning, budget_exceeded, cross_agent_correlation, key_rotated), optionally only those older than older_than_hours.","description":"Signed-in owner only: the owner can delete them for any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) no agent's can be deleted, its own included: it answers 403 session_required before anything is looked up and deletes nothing. Before 2026-10-05 a key could delete its own agent's.","tags":["agent"],"security":[{"Session":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"older_than_hours","in":"query","schema":{"type":"number"}}],"responses":{"200":{"description":"{success, deleted, cutoff}"},"400":{"description":"valid agent_id required, or older_than_hours is not a non-negative number"},"401":{"description":"no signed-in session"},"403":{"description":"session_required: the request carried an API key. Or the owner_id is not the signed-in owner"},"404":{"description":"the agent is not on the caller's account"}}}},"/api/commands":{"post":{"summary":"Send a command to one of your agents (the dashboard's Wake Up button).","description":"Signed-in owner only: the owner can send a command to any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) no command can be sent, to its own agent or any other: it answers 403 session_required before the body is looked at, and nothing is stored or forwarded, because one agent directing another is a way around oversight. The command is stored as an event of type command, with \"Command: <command>\" as its message, and forwarded to the webhook of the agent's first active alert rule, if it has one. The command text and payload are scanned as /api/ingest scans an event, with the account's jurisdiction: each secret or regulated identifier found is replaced with \"[redacted: <label>]\" before the command is stored and before it is forwarded, and the redacted text is all the webhook body and the response carry. Each piece of text, list or object in the payload that lies more than 32 levels deep, after the first 1,000 items of a list, or after the first 10,000 pieces of text in the payload is not read by the detectors and is replaced as a whole with \"[redacted: beyond scan limit]\"; a number, true or false, or null there is kept, unless its key is past the text limit. At most 1,000 such parts in one request, across all its payloads, are replaced where they are; a payload whose parts would go past that is replaced whole instead, with {\"value\": \"[redacted: beyond scan limit]\"}. The keys of the objects in the payload are read too, each as a piece of text before its value, and count toward those 10,000. Where a key holds a leaked secret or a regulated identifier, the object it is in is replaced as a whole with \"[redacted: <label>]\", or, for a key at the top level of the payload, the whole payload with {\"value\": \"[redacted: <label>]\"}. A key after the first 10,000 pieces of text is not read, so it and the keys after it in its object, with their values, are replaced together by one entry, \"[redacted: beyond scan limit]\", whose value is how many entries it replaces, and count as one such part; when the object already has a key of that name before them, that key's value is replaced by the count. Before 2026-10-06 the detectors read 8 levels deep and the first 100 items of a list, read no keys, and left what lay beyond as sent. Attack text (destructive SQL, dangerous shell, prompt injection, exfiltration, suspicious network) is kept, the summary of what was found is added to the stored message, and the status is raised as ingest raises it. In the stored payload, issued_by is \"dashboard\" and, with command, issued_at and webhook_delivered, is set by VITNA after the caller's payload, so the payload cannot set them. Before 2026-10-05 any key of the owner could send a command to any of the owner's agents, the text and payload were stored and forwarded unscanned, and the payload could set issued_by. An agent receives each command as a command frame, {id, command, payload, issued_at}, on its own live stream (GET /api/stream with its key), beside the event frame for the same event. When the stream opens it sends the agent's latest 20 events, so a client that reconnects sees recent commands again and should act on each command id once. There is no GET: until 2026-10-06 GET /api/commands returned an agent's command events whose status was pending, which no command sent here ever had, so it returned only events the agent had stored itself through /api/ingest. It was removed.","tags":["agent"],"security":[{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["agent_id","owner_id","command"],"properties":{"agent_id":{"type":"string","format":"uuid"},"owner_id":{"type":"string","format":"uuid"},"command":{"type":"string","description":"For example wake_up, ping or report"},"payload":{"type":"object","description":"Optional. Must be a JSON object; a string or an array is refused with 400."}}}}}},"responses":{"201":{"description":"{status: \"sent\", message, command_id, created_at, webhook: null or {delivered, status} or {delivered: false, error}}. The message carries the command as stored, secrets and identifiers redacted."},"400":{"description":"agent_id, owner_id or command missing or not valid, or payload is not a JSON object"},"401":{"description":"no signed-in session, or the owner_id is not the signed-in owner"},"403":{"description":"session_required: the request carried an API key"},"404":{"description":"the agent is not on the signed-in owner's account"}}}},"/api/stream":{"get":{"summary":"One agent's live events as Server-Sent Events: connected, event, command, anomaly, status, stats and error frames.","description":"The signed-in owner can read any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be read: asked for another agent of the same owner it answers 403 session_required with a note, after the tenant check and before anything of that agent is read. Before 2026-10-06 any key of the owner could read any of the owner's agents here. A key refused for another agent gets that JSON answer and no stream: nothing is polled and no frame is sent, so another agent's commands never reach it. The dashboard streams with the session cookie. This is how an agent receives the commands its owner sends it (POST /api/commands): An agent receives each command as a command frame, {id, command, payload, issued_at}, on its own live stream (GET /api/stream with its key), beside the event frame for the same event. When the stream opens it sends the agent's latest 20 events, so a client that reconnects sees recent commands again and should act on each command id once. Only POST /api/commands stores an event of type command; /api/ingest refuses one.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"text/event-stream. New events are polled every 2 seconds, statistics every 10 seconds, with a keepalive comment every 25 seconds."},"400":{"description":"valid agent_id and owner_id required"},"401":{"description":"no valid session or key"},"403":{"description":"the owner_id is not the caller's. session_required: an API key asked for another agent's stream"},"404":{"description":"the agent is not on the caller's account"}}}},"/api/events/export":{"get":{"summary":"Download one agent's events as CSV or JSON.","description":"The signed-in owner can read any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be read: asked for another agent of the same owner it answers 403 session_required with a note, after the tenant check and before anything of that agent is read. Before 2026-10-06 any key of the owner could read any of the owner's agents here.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","schema":{"type":"string","enum":["csv","json"],"default":"csv"}},{"name":"start","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"end","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"types","in":"query","schema":{"type":"string"},"description":"Comma list of event types"},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50000,"maximum":100000}}],"responses":{"200":{"description":"text/csv, or JSON {agent_id, agent_name, exported_at, filters, count, events}"},"400":{"description":"valid agent_id required, or format is not csv or json"},"401":{"description":"no valid session or key"},"403":{"description":"the owner_id is not the caller's. session_required: an API key asked for another agent's events"},"404":{"description":"the agent is not on the caller's account"}}}},"/api/graph":{"get":{"summary":"One agent's activity graph: actions or events as nodes, causal links as edges.","description":"The signed-in owner can read any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be read: asked for another agent of the same owner it answers 403 session_required with a note, after the tenant check and before anything of that agent is read. Before 2026-10-06 any key of the owner could read any of the owner's agents here.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"group","in":"query","schema":{"type":"string","enum":["action","event"],"default":"action"}},{"name":"limit","in":"query","schema":{"type":"integer","default":2000,"maximum":5000}},{"name":"include_noise","in":"query","schema":{"type":"string","enum":["0","1","true","false"]}},{"name":"include_system","in":"query","schema":{"type":"string","enum":["0","1"]}}],"responses":{"200":{"description":"{nodes, edges, stats, agent: {id, name}}"},"400":{"description":"valid agent_id required"},"403":{"description":"forbidden: no valid session or key, or the agent is not on the caller's account. session_required: an API key asked for another agent's graph"},"404":{"description":"the agent was not found"}}}},"/api/cef":{"get":{"summary":"One agent's events for a SIEM, as CEF, LEEF or JSON.","description":"The signed-in owner can read any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be read: asked for another agent of the same owner it answers 403 session_required with a note, after the tenant check and before anything of that agent is read. Before 2026-10-06 any key of the owner could read any of the owner's agents here.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"format","in":"query","schema":{"type":"string","enum":["cef","leef","json"],"default":"cef"}},{"name":"limit","in":"query","schema":{"type":"integer","default":500,"maximum":5000}},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"text/plain CEF or LEEF lines, or JSON {agent, count, events}"},"400":{"description":"agent_id required"},"403":{"description":"Forbidden (text): no valid session or key, or the agent is not on the caller's account. session_required (JSON): an API key asked for another agent's events"},"404":{"description":"the agent was not found"}}}},"/api/anomaly":{"get":{"summary":"One agent's anomaly score and the anomalies found in a time window.","description":"The signed-in owner can read any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be read: asked for another agent of the same owner it answers 403 session_required with a note, after the tenant check and before anything of that agent is read. Before 2026-10-06 any key of the owner could read any of the owner's agents here.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"window","in":"query","schema":{"type":"integer","default":60,"maximum":1440},"description":"Minutes"}],"responses":{"200":{"description":"{detected, anomalies, risk_score, risk_level}"},"400":{"description":"valid agent_id required"},"401":{"description":"no valid session or key"},"403":{"description":"the owner_id is not the caller's. session_required: an API key asked for another agent's"},"404":{"description":"the agent is not on the caller's account"},"429":{"description":"rate limited"}}}},"/api/health":{"get":{"summary":"With agent_id, one agent's health summary built from its last 7 days of events: its anomaly risk, its event, error and cost totals, its 7-day trend, its budget use and its heartbeat. With no parameters, a public liveness check.","description":"The signed-in owner can read any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be read: asked for another agent of the same owner it answers 403 session_required with a note, after the tenant check and before anything of that agent is read. Before 2026-10-06 any key of the owner could read any of the owner's agents here. The liveness check with no parameters needs no credential.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{agent, health, risk, online, last_seen, silent_seconds, heartbeat, stats, budget, trend_7d, generated_at}, or {status, service, time} with no parameters"},"400":{"description":"valid agent_id required"},"403":{"description":"forbidden: no valid session or key, or the agent is not on the caller's account. session_required: an API key asked for another agent's"},"404":{"description":"the agent was not found"}}}},"/api/stats":{"get":{"summary":"One agent's statistics: its events, errors and cost over the last 24 hours, its events per day over the last 7 days, and the time of its last event.","description":"The signed-in owner can read any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be read: asked for another agent of the same owner it answers 403 session_required with a note, after the tenant check and before anything of that agent is read. Before 2026-10-06 any key of the owner could read any of the owner's agents here.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"bust","in":"query","schema":{"type":"string","enum":["1"]},"description":"Skip the 15-second cache"}],"responses":{"200":{"description":"{stats: {total_events, total_errors, total_cost_sol, error_rate, events_by_type, events_by_bucket, events_by_day, last_event_at}}"},"400":{"description":"valid agent_id required"},"401":{"description":"no valid session or key"},"403":{"description":"the owner_id is not the caller's. session_required: an API key asked for another agent's"},"404":{"description":"the agent is not on the caller's account"},"429":{"description":"rate limited"}}}},"/api/compliance":{"get":{"summary":"One agent's events mapped to SOC 2, PCI DSS, ISO 27001, NIST CSF and GDPR Article 30 controls.","description":"The signed-in owner can read any agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent can be read: asked for another agent of the same owner it answers 403 session_required with a note, after the tenant check and before anything of that agent is read. Before 2026-10-06 any key of the owner could read any of the owner's agents here.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"agent_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"days","in":"query","schema":{"type":"integer","default":30}}],"responses":{"200":{"description":"{agent, period_days, total_events, generated_at, overall_score, summary, frameworks, controls}"},"400":{"description":"agent_id and owner_id required"},"401":{"description":"no valid session or key"},"403":{"description":"the owner_id is not the caller's. session_required: an API key asked for another agent's report"},"404":{"description":"the agent is not on the caller's account"}}}},"/api/search":{"get":{"summary":"Search agent names, events and alert rules.","description":"The signed-in owner searches every agent on the account. With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only that key's own agent is searched: other agents' names, events and alert rules are left out, and a key that names no agent on the account finds nothing. Before 2026-10-06 any key of the owner searched every agent of the owner.","tags":["agent"],"security":[{"Session":[]},{"ApiKey":[]},{"Bearer":[]}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"{results: [{type: agent, event or alert, id, agent_id, agent_name, ...}], total, query}"},"400":{"description":"q required"},"401":{"description":"no valid session or key, or the owner_id is not the caller's"}}}},"/api/setup":{"post":{"summary":"One-command agent self-provisioning: create/resolve an owner and mint an API key (a RESTRICTED trial key for a fresh email, no signup)","description":"An email no account has creates a new trial account and needs no credential. Adding an agent to an account that already exists (owner_id, or the owner_email of an existing account) needs the signed-in owner: with an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) it answers 403 session_required and creates nothing. Before 2026-10-05 a key could name its own account here and get a new agent with a full key.","tags":["agent"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["agent_name"],"properties":{"agent_name":{"type":"string"},"owner_email":{"type":"string","description":"Required unless owner_id is given. A fresh email mints a RESTRICTED trial key."},"owner_id":{"type":"string","format":"uuid","description":"Attach to an existing owner instead (mints a full key). Signed-in owner only; an API key gets 403 session_required."},"description":{"type":"string"},"capabilities":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"Existing agent for this owner+name; api_key is null (rotate to get a new one)."},"201":{"description":"Created. Returns agent_id, api_key (shown once), owner_id; for a fresh email also restricted:true + claim_url + trial{daily_checks,lifetime_checks,expires_at}. This is the only response that carries a claim link, for the person who set up the trial: since 2026-10-05 no response to an API key carries one (claim_url is absent or null, and claim_note says where the link is)."},"400":{"description":"agent_name and (owner_email or owner_id) required"},"401":{"description":"An existing account was named with no signed-in session"},"403":{"description":"Email not verified (existing owner), free-plan agent limit reached, or the signed-in session is another account's. session_required: an existing account was named and the request carried an API key"},"429":{"description":"IP rate limited (20 / 10 min)"}}}},"/api/preflight/action-check":{"post":{"summary":"Safety pre-flight: check a dangerous action (shell / file / DB / network) BEFORE you run it -> allowed | blocked | flagged. VITNA evaluates + records; the caller enforces","tags":["agent","safety"],"security":[{"ApiKey":[]},{"Bearer":[]},{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","description":"Proposed command/action text, e.g. \"rm -rf /\". Text only: an action sent as any other JSON value is refused with 400. Before 2026-10-06 it was ignored and only the payload was checked."},"payload":{"nullable":true,"description":"Optional. A JSON object, checked field by field. A payload that is not a JSON object (text, a number, true or false, or an array) is kept whole as {\"value\": <the payload as sent>} and checked like any other field. The action text and the payload are checked apart, so a payload field named command is checked too and the decision is the worst match in either; before 2026-10-06 the action text replaced that field before the check. redacted_action shows the payload that way with the action text beside it under command, or {\"command\": <the action text>, \"payload\": <the payload>} when the payload has its own command field. Before 2026-10-05 a payload that was not an object was not checked. Each piece of text, list or object in the payload that lies more than 32 levels deep, after the first 1,000 items of a list, or after the first 10,000 pieces of text in the payload is not read by the detectors and is replaced as a whole with \"[redacted: beyond scan limit]\"; a number, true or false, or null there is kept, unless its key is past the text limit. At most 1,000 such parts in one request, across all its payloads, are replaced where they are; a payload whose parts would go past that is replaced whole instead, with {\"value\": \"[redacted: beyond scan limit]\"}. The keys of the objects in the payload are read too, each as a piece of text before its value, and count toward those 10,000. Where a key holds a leaked secret or a regulated identifier, the object it is in is replaced as a whole with \"[redacted: <label>]\", or, for a key at the top level of the payload, the whole payload with {\"value\": \"[redacted: <label>]\"}. A key after the first 10,000 pieces of text is not read, so it and the keys after it in its object, with their values, are replaced together by one entry, \"[redacted: beyond scan limit]\", whose value is how many entries it replaces, and count as one such part; when the object already has a key of that name before them, that key's value is replaced by the count. Before 2026-10-06 the detectors read 8 levels deep and the first 100 items of a list, read no keys, and left what lay beyond as sent. A call with such a part is not allowed: it counts as one threat of category scan_limit, label \"beyond scan limit\", at severity high, once per call however many parts were replaced (in the engagement action, in the payload, the context or both), and the echo shows the marker in their place. Before 2026-10-06 such a part was not counted, so a call whose only risky text lay beyond the limits was allowed; earlier on 2026-10-06 the engagement action counted it once for the payload and once more for the context. The owner's detection settings make it blocked or flagged, as for any other match. The reason lists the first four distinct \"category: label\" pairs of the matches, in the order found, joined by \" · \", so with no other match it reads \"scan_limit: beyond scan limit\". A match in a key counts like a match in a value, and the echo shows the object that key is in replaced when it holds a secret. Before 2026-10-06 keys were not read, so a secret or a dangerous command sent as a key was allowed and echoed as sent."},"action_type":{"type":"string"}}}}}},"responses":{"200":{"description":"Full key: {decision, threat_category, severity, reason, threats[], redacted_action, ...}. Restricted trial key: label-only {check, effect, decision, flagged, trial:true}."},"400":{"description":"Provide action and/or payload; action, when sent, must be text"},"401":{"description":"Forbidden (missing/invalid key)"},"403":{"description":"Restricted key: key_expired"},"429":{"description":"Restricted-key cap: daily_cap_reached / lifetime_cap_reached / global_capacity_reached / too_many_concurrent. No claim link in the body; claim_note says where the person who set up the trial finds it."},"503":{"description":"audit_record_write_failed (fail-closed), or service_paused / metering_unavailable"}}}},"/api/engagement/open":{"post":{"summary":"Open a scoped engagement session; VITNA evaluates every later action against its ScopePolicy","description":"The session belongs to the agent that opened it. The session record stores opened_by_agent_id: the agent whose API key made the request (read from the key, also beside a session cookie), or null when the signed-in owner opened it. From then on only that agent's key, or the signed-in owner, can act in the session, read it, poll its holds, start a drill in it or close it.","tags":["engagement"],"security":[{"ApiKey":[]},{"Bearer":[]},{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scope_policy"],"properties":{"scope_policy":{"type":"object","properties":{"allowed_domains":{"type":"array","items":{"type":"string"}},"allowed_actions":{"type":"array","items":{"type":"string"},"description":"Non-empty; forbid everything else."},"max_writes":{"type":"integer","nullable":true},"expires_at":{"type":"string","nullable":true}}},"metadata":{"type":"object","description":"Optional. Stored with the session, each secret or regulated identifier the detectors find replaced with \"[redacted: <label>]\". Each piece of text, list or object in the payload that lies more than 32 levels deep, after the first 1,000 items of a list, or after the first 10,000 pieces of text in the payload is not read by the detectors and is replaced as a whole with \"[redacted: beyond scan limit]\"; a number, true or false, or null there is kept, unless its key is past the text limit. At most 1,000 such parts in one request, across all its payloads, are replaced where they are; a payload whose parts would go past that is replaced whole instead, with {\"value\": \"[redacted: beyond scan limit]\"}. The keys of the objects in the payload are read too, each as a piece of text before its value, and count toward those 10,000. Where a key holds a leaked secret or a regulated identifier, the object it is in is replaced as a whole with \"[redacted: <label>]\", or, for a key at the top level of the payload, the whole payload with {\"value\": \"[redacted: <label>]\"}. A key after the first 10,000 pieces of text is not read, so it and the keys after it in its object, with their values, are replaced together by one entry, \"[redacted: beyond scan limit]\", whose value is how many entries it replaces, and count as one such part; when the object already has a key of that name before them, that key's value is replaced by the count. Before 2026-10-06 the detectors read 8 levels deep and the first 100 items of a list, read no keys, and left what lay beyond as sent."},"webhook_url":{"type":"string","description":"Optional https alert URL (SSRF-guarded): a per-session copy. Receives a signed engagement_alert for each deny and hold in this session, in addition to the owner's alert channel; it never replaces the owner's channel, which only the signed-in owner sets. When both are the same URL it is posted once. A hold alert names the hold and its deadline and carries no link that can approve or deny it."}}}}}},"responses":{"200":{"description":"{session_id, status:\"open\", scope_policy, webhook_configured, opened_at}"},"400":{"description":"allowed_actions must be non-empty, or bad webhook_url"},"401":{"description":"Forbidden"},"402":{"description":"plan_limit_exceeded (active engagements: free 3 / pro 100 / team 500 / enterprise unlimited). An open session counts while it is in use: it opened or had an action checked within idle_minutes (10), or a hold in it is waiting for a person. The answer carries active, open and idle_minutes."}}}},"/api/engagement/action":{"post":{"summary":"Per-action decision loop: evaluate one action against the session policy + threat detector -> allow | deny | hold with a signed VigilDecision","description":"Bound to the agent that opened the session: With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) it works only in a session that key's own agent opened. Any other session of the owner (another agent's, one the signed-in owner opened, or one opened before 2026-10-05, which records no agent) answers a key exactly as an unknown session id does (404 session_not_found, after the same validation and metering), so the id is not confirmed, and nothing is recorded in that session. The signed-in owner acts in any session.","tags":["engagement"],"security":[{"ApiKey":[]},{"Bearer":[]},{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["session_id","type"],"properties":{"session_id":{"type":"string","format":"uuid"},"type":{"type":"string","description":"Whitespace-free action identifier, e.g. dns.write."},"target":{"type":"string","nullable":true,"description":"Text or null: a target sent as any other JSON value is refused with 400. Before 2026-10-06 it was ignored and not checked."},"payload":{"nullable":true,"description":"Optional. The call's arguments, a JSON object. A payload that is not a JSON object (text, a number, true or false, or an array) is kept whole as {\"value\": <the payload as sent>} and checked like any other field. A held call's argument excerpt shows it that way. Before 2026-10-05 text, a number or true or false was not checked, and an array was checked one key per index. Each piece of text, list or object in the payload that lies more than 32 levels deep, after the first 1,000 items of a list, or after the first 10,000 pieces of text in the payload is not read by the detectors and is replaced as a whole with \"[redacted: beyond scan limit]\"; a number, true or false, or null there is kept, unless its key is past the text limit. At most 1,000 such parts in one request, across all its payloads, are replaced where they are; a payload whose parts would go past that is replaced whole instead, with {\"value\": \"[redacted: beyond scan limit]\"}. The keys of the objects in the payload are read too, each as a piece of text before its value, and count toward those 10,000. Where a key holds a leaked secret or a regulated identifier, the object it is in is replaced as a whole with \"[redacted: <label>]\", or, for a key at the top level of the payload, the whole payload with {\"value\": \"[redacted: <label>]\"}. A key after the first 10,000 pieces of text is not read, so it and the keys after it in its object, with their values, are replaced together by one entry, \"[redacted: beyond scan limit]\", whose value is how many entries it replaces, and count as one such part; when the object already has a key of that name before them, that key's value is replaced by the count. Before 2026-10-06 the detectors read 8 levels deep and the first 100 items of a list, read no keys, and left what lay beyond as sent. A call with such a part is not allowed: it counts as one threat of category scan_limit, label \"beyond scan limit\", at severity high, once per call however many parts were replaced (in the engagement action, in the payload, the context or both), and the echo shows the marker in their place. Before 2026-10-06 such a part was not counted, so a call whose only risky text lay beyond the limits was allowed; earlier on 2026-10-06 the engagement action counted it once for the payload and once more for the context. In scope, such a call is held for a person. Its reason is \"threat_flag: \" and the categories of its flags in the order found, joined by \", \", with scan_limit named once, so with no other flag it reads \"threat_flag: scan_limit\" (an action type listed in hold_actions gives \"hold_action: <type>\" instead). A match in a key counts like a match in a value, and the echo shows the object that key is in replaced when it holds a secret. Before 2026-10-06 keys were not read, so a secret or a dangerous command sent as a key was allowed and echoed as sent. The context is read the same way."},"context":{"nullable":true,"description":"Optional. Any JSON value, read the way payload is and checked apart from it, as are type and target, so a key in one cannot hide a value in another. A context that is not a JSON object is kept whole as {\"value\": <the context as sent>}. When a key is in two of these parts, redacted_action and a held call's argument excerpt keep each part whole under its own name; a part replaced whole is {\"value\": <the marker>} there, so its value key counts. Before 2026-10-06 the parts were merged before the check, so a context key hid a payload value of the same name, and a context sent as text was not checked."},"_meta":{"type":"object","description":"Optional W3C trace context (traceparent / tracestate)."}}}}}},"responses":{"200":{"description":"{decision:\"allow\"|\"deny\"|\"hold\", reason, threat_flags[], vigil_decision{...signature...}, alert, enforced_by:\"caller\"}. On deny or hold, alert is {dispatched:true, channels:[\"owner\",\"session\"]} naming which webhooks were posted to (the owner's alert channel and the session's webhook_url; never their addresses), or {dispatched:false, reason:\"no_webhook_configured\"}. A hold's routed_to records owner:webhook and session:webhook for exactly those. Restricted key: vigil_decision unsigned + trial:true."},"400":{"description":"session_id and type required; type must be whitespace-free; target, when sent, must be text or null"},"401":{"description":"Forbidden"},"404":{"description":"session_not_found: no such session on the account, or an API key asked about a session its own agent did not open"},"409":{"description":"session_closed: the session is closed and takes no new actions. {error, session_id, closed_at}. Nothing is recorded or metered."},"429":{"description":"Restricted-key caps"},"503":{"description":"audit_record_write_failed (fail-closed)"}}}},"/api/engagement/close":{"post":{"summary":"Close a session and emit a signed, offline-verifiable Ed25519 evidence bundle (claimed accounts only). A session closes once; after that it takes no new actions.","description":"Bound to the agent that opened the session: With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) it works only in a session that key's own agent opened. Any other session of the owner (another agent's, one the signed-in owner opened, or one opened before 2026-10-05, which records no agent) answers a key 404 session_not_found, as an unknown id does, and nothing is signed or written. The signed-in owner closes any session.","tags":["engagement"],"security":[{"ApiKey":[]},{"Bearer":[]},{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["session_id"],"properties":{"session_id":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"{evidence_package, package_signature (Ed25519), verification{...}}"},"401":{"description":"Forbidden"},"403":{"description":"restricted_key_forbidden (signed evidence is a claimed-account capability)"},"404":{"description":"session_not_found: no such session on the account, or an API key asked about a session its own agent did not open"},"409":{"description":"session_closed: the session was already closed. {error, session_id, closed_at}. A session closes once; a second close signs and records nothing and issues no new bundle."},"503":{"description":"close_write_failed: the close could not be recorded, so no bundle was issued and the session is still open. Retry."}}}},"/api/engagement/hold":{"get":{"summary":"Status of one hold (pending, approved, denied, expired). Reading a hold past its deadline records it as expired.","description":"Bound to the agent that opened the hold's session: With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only the holds of sessions that key's own agent opened can be read. Any other hold of the owner answers a key 404 hold_not_found, as an unknown id does, before the read could record an expiry. The signed-in owner reads any hold.","tags":["engagement"],"security":[{"ApiKey":[]},{"Bearer":[]},{"Session":[]}],"parameters":[{"name":"hold_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{hold_id, session_id, status, deadline, decided_at, drill?}"},"401":{"description":"Forbidden"},"404":{"description":"hold_not_found"}}}},"/api/engagement/hold/decide":{"get":{"summary":"Confirmation page for the emailed decision link. A GET never decides.","tags":["engagement"],"parameters":[{"name":"hold_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"token","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"HTML page with approve and deny"},"404":{"description":"invalid link"}}},"post":{"summary":"Approve or deny a hold. Never with an API key, including the agent's own: any request carrying one is refused. Accepts the signed link from the hold email (never sent to a webhook), or a dashboard session for the hold's owner.","tags":["engagement"],"security":[{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["hold_id","outcome"],"properties":{"hold_id":{"type":"string","format":"uuid"},"outcome":{"type":"string","enum":["approve","deny"]},"token":{"type":"string"}}}}}},"responses":{"200":{"description":"{hold_id, status, decided_by, decided_at, deadline, drill}; after the deadline the status is expired"},"403":{"description":"agent_keys_cannot_resolve_holds, or forbidden"},"404":{"description":"hold_not_found"}}}},"/api/engagement/hold/pending":{"get":{"summary":"The signed-in owner's holds waiting for a decision (no outcome, deadline not passed), soonest deadline first. For the dashboard; any request carrying an API key is refused.","tags":["engagement"],"security":[{"Session":[]}],"responses":{"200":{"description":"{now, holds: [{hold_id, session_id, action, action_detail, reason, held_at, deadline}]}"},"401":{"description":"session_required"},"403":{"description":"agent_keys_cannot_list_holds"}}}},"/api/engagement/hold/canary":{"post":{"summary":"Open a canary (drill) hold through the same path as a real one; revealed as a drill only once decided","description":"Bound to the agent that opened the session: With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) it works only in a session that key's own agent opened. Any other session of the owner (another agent's, one the signed-in owner opened, or one opened before 2026-10-05, which records no agent) answers a key 404 session_not_found, as an unknown id does, and nothing is recorded. The signed-in owner can start one in any session.","tags":["engagement"],"security":[{"ApiKey":[]},{"Bearer":[]},{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["session_id"],"properties":{"session_id":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"{hold_id, status: pending, deadline, notified}"},"401":{"description":"Forbidden"},"404":{"description":"session_not_found"},"409":{"description":"session_closed: the session is closed and takes no new drills. {error, session_id, closed_at, note}. Nothing is recorded."},"503":{"description":"hold_write_failed"}}}},"/api/engagement/list":{"get":{"summary":"List the owner's engagements with state, scope, decision counts, and plan metering","description":"Each engagement carries opened_by_agent_id (null when none is recorded). With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) only the sessions that key's own agent opened are listed, and count is theirs; metering still counts the whole account, because the plan's cap does: metering.active is the open sessions in use that count against active_limit (opened or acted in within idle_minutes, or holding a waiting hold), metering.open is every open session. The signed-in owner sees every session.","tags":["engagement"],"security":[{"ApiKey":[]},{"Bearer":[]},{"Session":[]}],"parameters":[{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"{count, metering, engagements[]}"},"401":{"description":"Forbidden"}}}},"/api/engagement/get":{"get":{"summary":"One engagement's full status + itemized decisions (owner-scoped)","description":"Bound to the agent that opened the session: With an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside a session cookie) it works only in a session that key's own agent opened. Any other session of the owner (another agent's, one the signed-in owner opened, or one opened before 2026-10-05, which records no agent) answers a key 404 session_not_found, as an unknown id does. The signed-in owner reads any session. opened_by_agent_id names the opening agent, or is null when none is recorded.","tags":["engagement"],"security":[{"ApiKey":[]},{"Bearer":[]},{"Session":[]}],"parameters":[{"name":"session_id","in":"query","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"owner_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"EngagementDetail"},"400":{"description":"session_id required"},"401":{"description":"Forbidden"},"404":{"description":"session_not_found"}}}},"/api/owners/claim":{"post":{"summary":"Claim by emailed code, step 1: with the claim link's token, bind an email to an unclaimed trial owner and email a 6-digit code","description":"The claim link's token (its t= value) is the only proof accepted; owner_id alone is refused. A request carrying an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside the claim link or a claim cookie) is refused 403 claim_by_key_refused: a person claims from the claim link in the MCP server log, and an API key cannot start or finish a claim. The claim page does not use this route: it claims with GitHub or a passkey.","tags":["auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["owner_id","email","token"],"properties":{"owner_id":{"type":"string","format":"uuid"},"email":{"type":"string"},"token":{"type":"string","description":"The t= value of the claim link."}}}}}},"responses":{"200":{"description":"{sent:true, owner_id, email_delivery}"},"400":{"description":"valid owner_id / email required"},"403":{"description":"claim_by_key_refused, claim_link_invalid or claim_link_expired"},"404":{"description":"not_claimable"},"409":{"description":"already_claimed, claim_link_used, or email_in_use"},"429":{"description":"IP rate limited (20 / 10 min)"},"503":{"description":"claim_unavailable (no signing secret; fails closed)"}}}},"/api/owners/claim/confirm":{"post":{"summary":"Claim by emailed code, step 2: verify the code, bind + verify the email, spend the claim link, and lift the restriction on all the owner's keys","description":"A request carrying an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside the claim link or a claim cookie) is refused 403 claim_by_key_refused: a person claims from the claim link in the MCP server log, and an API key cannot start or finish a claim. A pending claim that the claim link did not start (one an API key started before 2026-10-05) is refused the same way, before the code is compared, and wiped.","tags":["auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["owner_id","pin"],"properties":{"owner_id":{"type":"string","format":"uuid"},"pin":{"type":"string","pattern":"^\\d{6}$"}}}}}},"responses":{"200":{"description":"{claimed:true, owner_id, keys_unlocked}"},"400":{"description":"valid owner_id / pin required; no_claim_in_progress; code_expired"},"401":{"description":"invalid_code"},"403":{"description":"claim_by_key_refused"},"404":{"description":"not_claimable"},"409":{"description":"not_claimable (not a trial account), claim_link_used, or email_in_use"},"429":{"description":"IP rate limited (30 / 10 min), or too_many_attempts"}}}},"/api/owners/claim/begin":{"post":{"summary":"Check a claim link and exchange it for a 10-minute HttpOnly claim cookie, or renew an expired one. Nothing is claimed here.","description":"A request carrying an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside the claim link or a claim cookie) is refused 403 claim_by_key_refused: a person claims from the claim link in the MCP server log, and an API key cannot start or finish a claim. With renew: true, an expired but genuine link for an unclaimed trial that started less than 72 hours ago is renewed once: the old link is spent, a fresh one is minted and the claim cookie is set for it. The fresh link is never in the body. The claim page sends this only when the person chooses Get a fresh link; checking a link never renews it.","tags":["auth"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"owner_id":{"type":"string","format":"uuid"},"token":{"type":"string","description":"The t= value of the claim link. Omit both fields to resume from the claim cookie."},"renew":{"type":"boolean","description":"true renews an expired link (see above). Ignored for a link that has not expired."}}}}}},"responses":{"200":{"description":"{claimable:true, owner_id, link_expires_at, trial:{provisioned_at, agents}} and Set-Cookie vitna_claim; after a renewal also renewed:true, with no link in the body"},"400":{"description":"invalid_request"},"403":{"description":"claim_by_key_refused, claim_link_invalid (a forged or incomplete link, with renew or without), claim_link_not_renewable (renew: true refused for a genuine link: renewed or spent already, the trial claimed, or its 72 hours passed; the same message for each, renewable:false and fresh_link with the new-trial command, never a link), claim_link_expired (links last 24 hours; renewable:true when the page can renew it once, otherwise fresh_link says how to start a new trial), claim_session_expired on resume, or origin_rejected"},"409":{"description":"already_claimed or claim_link_used"},"429":{"description":"IP rate limited (20 / 10 min)"},"503":{"description":"claim_unavailable (no signing secret; fails closed)"}}}},"/api/auth/github/start":{"get":{"summary":"Start a GitHub round trip (scope read:user, PKCE). Redirects to GitHub.","description":"For intent=claim: A request carrying an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside the claim link or a claim cookie) is refused 403 claim_by_key_refused: a person claims from the claim link in the MCP server log, and an API key cannot start or finish a claim. That refusal is JSON, not a redirect.","tags":["auth"],"parameters":[{"name":"intent","in":"query","schema":{"type":"string","enum":["signin","claim","link"]}}],"responses":{"302":{"description":"To GitHub, with Set-Cookie vitna_oauth (state, Lax, 10 min)"},"303":{"description":"Back to /dashboard?auth_error= or /claim?error= (github_unavailable, claim_session_expired, reauth_required, rate_limited)"},"403":{"description":"claim_by_key_refused (intent=claim with an API key)"}}}},"/api/auth/github/callback":{"get":{"summary":"GitHub returns here. State must match the browser cookie and is used once. An unlinked GitHub account creates nothing.","description":"For a claim round trip: A request carrying an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside the claim link or a claim cookie) is refused 403 claim_by_key_refused: a person claims from the claim link in the MCP server log, and an API key cannot start or finish a claim. That refusal is JSON, not a redirect, and the state is not spent.","tags":["auth"],"parameters":[{"name":"code","in":"query","schema":{"type":"string"}},{"name":"state","in":"query","schema":{"type":"string"}}],"responses":{"303":{"description":"/dashboard?signed_in=github, ?linked=github or ?from=claim on success; otherwise ?auth_error= / /claim?error= with oauth_state_invalid, github_not_linked, identity_in_use, github_cancelled, github_failed"},"403":{"description":"claim_by_key_refused (a claim round trip with an API key)"}}}},"/api/auth/passkey/register/options":{"post":{"summary":"WebAuthn registration options for a claim (claim cookie) or for adding a passkey (session signed in within 10 minutes)","description":"The options ask for user verification (userVerification: required). For flow claim: A request carrying an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside the claim link or a claim cookie) is refused 403 claim_by_key_refused: a person claims from the claim link in the MCP server log, and an API key cannot start or finish a claim.","tags":["auth"],"security":[{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["flow"],"properties":{"flow":{"type":"string","enum":["claim","link"]}}}}}},"responses":{"200":{"description":"{options} and Set-Cookie vitna_webauthn (challenge, 5 min)"},"400":{"description":"passkeys_unavailable_here or invalid_request"},"401":{"description":"claim_session_expired or no_session"},"403":{"description":"claim_by_key_refused (flow claim with an API key), reauth_required or origin_rejected"},"409":{"description":"already_claimed"},"429":{"description":"IP rate limited (30 / 10 min)"}}}},"/api/auth/passkey/register/verify":{"post":{"summary":"Verify a new passkey and bind it: claims the trial, or adds it to the signed-in account","description":"A response whose authenticator data does not have the UV (user verified) flag set is refused 400 passkey_verification_failed. For flow claim: A request carrying an API key (x-vitna-key, x-vigil-key, x-api-key or Authorization: Bearer, a restricted trial key included, even beside the claim link or a claim cookie) is refused 403 claim_by_key_refused: a person claims from the claim link in the MCP server log, and an API key cannot start or finish a claim.","tags":["auth"],"security":[{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["flow","response"],"properties":{"flow":{"type":"string","enum":["claim","link"]},"response":{"type":"object"},"label":{"type":"string"}}}}}},"responses":{"200":{"description":"{claimed:true, owner} with a session cookie, or {linked:true, method}"},"400":{"description":"passkey_verification_failed, challenge_expired or invalid_request"},"401":{"description":"claim_session_expired or no_session"},"403":{"description":"claim_by_key_refused (flow claim with an API key), reauth_required or origin_rejected"},"409":{"description":"challenge_used, identity_in_use, already_claimed, claim_link_used or too_many_methods"},"429":{"description":"IP rate limited (30 / 10 min)"}}}},"/api/auth/passkey/login/options":{"post":{"summary":"WebAuthn sign-in options (no account named; the browser offers its VITNA passkeys)","description":"The options ask for user verification (userVerification: required).","tags":["auth"],"responses":{"200":{"description":"{options} and Set-Cookie vitna_webauthn (challenge, 5 min)"},"400":{"description":"passkeys_unavailable_here"},"403":{"description":"origin_rejected"},"429":{"description":"IP rate limited (30 / 10 min)"}}}},"/api/auth/passkey/login/verify":{"post":{"summary":"Verify a passkey assertion and start a session. An unknown passkey creates nothing.","description":"An assertion whose authenticator data does not have the UV (user verified) flag set is refused 401 passkey_verification_failed.","tags":["auth"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["response"],"properties":{"response":{"type":"object"}}}}}},"responses":{"200":{"description":"{owner} with a session cookie"},"400":{"description":"challenge_expired or invalid_request"},"401":{"description":"passkey_not_recognized or passkey_verification_failed"},"409":{"description":"challenge_used"},"429":{"description":"IP rate limited (30 / 10 min)"}}}},"/api/auth/session":{"get":{"summary":"The signed-in owner, from the session cookie alone. email is null when the owner has no real address.","description":"A request that carries an API key in any header form is refused 403 session_required, with or without a session cookie beside it: the answer is the owner's email address, name and plan.","tags":["auth"],"security":[{"Session":[]}],"responses":{"200":{"description":"{owner:{id, email, name, plan, created_at, plan_expires_at?}}"},"401":{"description":"no_session"},"403":{"description":"session_required: the request carried an API key"},"429":{"description":"IP rate limited (120 / 10 min)"}}}},"/api/auth/methods":{"get":{"summary":"The ways the signed-in owner can sign in (display only: no subject, key or address)","tags":["auth"],"security":[{"Session":[]}],"responses":{"200":{"description":"{methods:[{id, provider, label, created_at, last_used_at}], email_code, password, recent_auth}"},"401":{"description":"no_session"},"429":{"description":"IP rate limited (60 / 10 min)"}}}},"/api/auth/methods/unlink":{"post":{"summary":"Remove a GitHub account or passkey (session signed in within 10 minutes). The last way to sign in cannot be removed.","tags":["auth"],"security":[{"Session":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["identity_id"],"properties":{"identity_id":{"type":"string"}}}}}},"responses":{"200":{"description":"{removed:true, remaining}"},"401":{"description":"no_session"},"403":{"description":"reauth_required or origin_rejected"},"404":{"description":"not_found"},"409":{"description":"last_method"},"429":{"description":"IP rate limited (10 / 10 min)"}}}},"/api/engage/start":{"post":{"summary":"Door 2: start a fixed-scope assessment engagement without a call","tags":["assessment"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["companyLegalName","contactName","contactEmail"],"properties":{"companyLegalName":{"type":"string"},"contactName":{"type":"string"},"contactEmail":{"type":"string","format":"email"}}}}}},"responses":{"200":{"description":"{ok:true, next} where next is the signed scope-agreement URL"},"400":{"description":"company, name and email are all required; malformed email"},"429":{"description":"IP rate limited (5 / 15 min)"},"503":{"description":"SALES_ENABLED is false, or links cannot be signed"}}}},"/api/engage/accept":{"post":{"summary":"Accept the scope agreement. Records the accepted text with name, title, timestamp, IP and user agent, then raises the deposit","tags":["assessment"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["e","t","clientLegalName","clientAddress","name","title","agreed"],"properties":{"e":{"type":"string","format":"uuid"},"t":{"type":"string","description":"signed engage token, purpose \"agreement\""},"clientLegalName":{"type":"string"},"clientAddress":{"type":"string"},"name":{"type":"string"},"title":{"type":"string"},"agreed":{"type":"boolean","enum":[true]}}}}}},"responses":{"200":{"description":"{ok:true, invoiceUrl, checkoutUrl} — either may be null if PayPal was unreachable"},"400":{"description":"incomplete link, or named missing fields"},"403":{"description":"token invalid or expired"},"409":{"description":"already accepted, or not in a state that accepts a signature"},"410":{"description":"engagement cancelled"},"429":{"description":"IP rate limited (10 / 15 min)"},"503":{"description":"SALES_ENABLED is false, or the write failed; nothing recorded"}}}},"/api/engage/intake":{"post":{"summary":"Return the intake questionnaire. Resubmission replaces; at least one AI system is required","tags":["assessment"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["e","t","intake"],"properties":{"e":{"type":"string","format":"uuid"},"t":{"type":"string","description":"signed engage token, purpose \"intake\""},"intake":{"type":"object","description":"company, systems[], controls, pressure, access — see lib/intakeSchema"}}}}}},"responses":{"200":{"description":"{ok:true, answered, total}"},"400":{"description":"no AI systems listed"},"403":{"description":"token invalid or expired"},"409":{"description":"the questionnaire is closed (delivered or cancelled)"},"429":{"description":"IP rate limited (30 / hour)"}}}},"/api/webhooks/cal":{"post":{"summary":"cal.com booking webhook. Creates an engagement and sends the pre-call packet","tags":["assessment"],"description":"Authenticated by HMAC-SHA256 over the raw body in x-cal-signature-256. Always 200 once the signature verifies, because cal.com retries a non-2xx.","responses":{"200":{"description":"acknowledged; body says whether an engagement was created"},"400":{"description":"invalid JSON"},"401":{"description":"signature missing or wrong"},"503":{"description":"CAL_WEBHOOK_SECRET is not configured"}}}},"/api/webhooks/paypal":{"post":{"summary":"PayPal webhook. Records a settled deposit and moves the engagement to deposit_paid","tags":["assessment"],"description":"Handles PAYMENT.CAPTURE.COMPLETED and INVOICING.INVOICE.PAID. Verified by asking PayPal via /v1/notifications/verify-webhook-signature, so a verification that could not be performed returns 503 and is redelivered rather than accepted.","responses":{"200":{"description":"acknowledged; body says whether the settlement matched and moved"},"400":{"description":"invalid JSON"},"401":{"description":"PayPal rejected the signature"},"503":{"description":"PayPal unconfigured, PAYPAL_WEBHOOK_ID unset, or verification unavailable"}}}},"/api/cron/engage-chase":{"get":{"summary":"Deposit chase, day 3 and day 7 after acceptance, then never. Sends reminders only; changes no state","tags":["assessment"],"parameters":[{"name":"dry","in":"query","schema":{"type":"string","enum":["1"]},"description":"decide and report, send nothing"}],"responses":{"200":{"description":"{considered, due, sent, failures}"},"401":{"description":"missing or wrong CRON_SECRET bearer"}}}}}}