Introduction
Screening is an input. Resolution is the product.
Traditional compliance tooling generates alerts. Veris resolves them. The distinction is fundamental: screening vendors deliver a list of problems. Veris delivers finished compliance work, including disposition decisions, evidence packages, SAR narratives, BSA/CTR XML filings, and immutable audit trails. Every alert that enters the system exits as a completed work product ready for CCO review and regulatory submission.
Zero-Touch Resolution
Veris operates an autonomous compliance pipeline that transforms raw blockchain activity into regulatory-ready output. No analyst intervention is required for the majority of alerts.
The pipeline executes in three sequential stages:
| Stage | Agent | Latency | Output |
|---|---|---|---|
| Triage | Classification engine | 2-3 seconds | Disposition category, confidence score, reasoning chain |
| Investigation | Evidence synthesis engine | 15-25 seconds | Counterparty analysis, transaction timeline, risk assessment, recommendation |
| SAR Generation | Narrative and filing engine | 10-20 seconds | FinCEN-compliant narrative, BSA XML, typology classification |
Total resolution time from alert ingestion to finished work: 30-50 seconds.
What "Finished Work" Means
A resolved alert produces five concrete deliverables:
- Disposition. A classification decision (clear, suspicious, sar_required) with confidence scoring and a full reasoning chain explaining the decision rationale.
- Evidence Package. Structured JSON containing counterparty analysis, transaction flows, risk factor enumeration, and source attribution for every claim.
- SAR Narrative. Publication-ready prose suitable for direct submission to FinCEN, written to the standards expected by examiners.
- BSA XML. Schema-validated XML conforming to the FinCEN BSA E-Filing specification, ready for upload without manual editing.
- Audit Trail. Hash-chained, immutable log entries recording every agent decision, data source consulted, and intermediate output produced during resolution.
Alert in, finished compliance work out.
API Products
Veris exposes three product surfaces through a unified REST API.
1. Zero-Touch Resolution
The core pipeline. Submit a transaction hash or alert identifier, receive a complete resolution with disposition, evidence, and filing artifacts.
| Endpoint | Method | Purpose |
|---|---|---|
/v1/resolve | POST | Submit a transaction or alert for autonomous resolution |
/v1/resolutions/{id} | GET | Retrieve a completed resolution and its artifacts |
/v1/evidence/{id} | GET | Download the evidence package for a resolution |
/v1/sar/{id}/narrative | GET | Retrieve the SAR narrative text |
/v1/sar/{id}.xml | GET | Download the BSA-compliant XML filing |
2. Continuous Monitoring
Always-on surveillance across stablecoin reserves, travel rule compliance, and regulatory change detection. These agents operate on periodic schedules independent of individual alerts.
| Endpoint | Method | Purpose |
|---|---|---|
/v1/monitor/reserves | GET | Current reserve health and attestation status |
/v1/monitor/travel-rule | GET | Travel rule compliance state across jurisdictions |
/v1/monitor/regulatory | GET | Regulatory change feed with impact analysis |
3. Intelligence
Programmatic access to Veris intelligence data, including entity resolution, address attribution, and risk scoring.
| Endpoint | Method | Purpose |
|---|---|---|
/v1/intelligence/entity/{address} | GET | Entity profile and attribution data |
/v1/intelligence/risk/{address} | GET | Composite risk score with factor breakdown |
/v1/intelligence/graph/{address} | GET | Transaction graph and counterparty network |
Getting Started
How Veris works with your compliance team. Four inputs from you, continuous autonomous output from the engine.
What Your Team Provides
Four inputs. All delivered through the dashboard or API.
1. Stablecoin Selection
Veris indexes USDC, USDT, EURC, PYUSD, and RLUSD across ten blockchains: Ethereum, Base, Polygon, Arbitrum, Avalanche, BSC, Optimism, Celo, Solana, and Tron. During onboarding, select the tokens and networks relevant to your business. Veris begins monitoring within minutes.
2. Entity Data
Upload KYC and CDD records through the API or dashboard. Map blockchain addresses to legal entities with jurisdiction and verification status. Veris does not perform KYC. Your team handles customer onboarding and feeds the results into Veris via POST /v1/veris/entities.
Example: "Address 0xabc belongs to John's OTC Desk LLC, incorporated in Singapore, KYC verified." Veris uses this mapping for counterparty analysis, travel rule compliance, and investigation context.
3. Compliance Policy
Configure rules for your jurisdiction:
| Setting | Default | Purpose |
|---|---|---|
| Alert threshold | Risk score 80+ | Minimum score to generate an alert |
| Travel rule limit (US) | $3,000 USD | Threshold for originator/beneficiary capture |
| Travel rule limit (EU) | EUR 1,000 | PSD2/MiCA threshold |
| High-risk jurisdiction multiplier | 2.0x | Risk score multiplier for flagged jurisdictions |
| Transaction monitoring lookback | 90 days | Historical window for counterparty analysis |
Each setting has a regulatory default. Override any value through the Settings page or PATCH /v1/settings/rules/{rule_key}. Changes apply immediately across all six agents.
4. Analyst Decisions
When a compliance analyst marks an alert as "False Positive" or "Escalate to Case," that decision flows back into the ML pipeline as a training label. The analyst does not need to know this. Every disposition improves the next triage cycle automatically.
Thirteen ML models consume analyst feedback: the meta-learner, false positive classifier, behavioral similarity model, community detection, BNN uncertainty estimator, and eight others. The system becomes more accurate with every resolved alert.
What Veris Generates Automatically
Every output below runs without customer action. Veris reads the blockchain, applies ML models, and delivers finished compliance work.
Core Pipeline
Runs continuously on every transaction.
Transaction Monitoring. The Go indexer reads every block from ten chains directly. Your team does not send transaction data. The system processes 7M+ stablecoin transfers per day with sub-second latency.
Sanctions Screening. Every transfer is checked against OFAC SDN and 70,000+ labeled addresses. Sanctions matches produce immediate alerts. Travel rule thresholds are evaluated per jurisdiction.
Risk Scoring. Every active address receives a composite risk score every 15 minutes. The scoring engine evaluates 16 factors including counterparty risk, behavioral drift, stablecoin health, and cross-chain evasion patterns.
AI Agents (Zero-Touch Resolution)
Three agents form the resolution pipeline. They handle the volume. Your analysts handle the judgment.
Triage Agent. Classifies every alert in 2 to 3 seconds. Deterministic pre-filter rules auto-resolve high-confidence false positives. Sanctions matches are escalated without delay. Ambiguous alerts go to the LLM for deeper analysis.
Investigation Agent. When an alert escalates to a case, the agent gathers 90 days of on-chain history, runs counterparty analysis, checks sanctions exposure, and produces a structured evidence package. Time to completion: 15 to 25 seconds.
SAR Generation Agent. When the investigation recommends filing, this agent drafts a FinCEN narrative and generates BSA XML. A quality gate validates every field. The analyst reviews the draft and approves filing.
Supporting Agents
Three agents run on periodic schedules, included in the platform fee.
| Agent | Schedule | Output |
|---|---|---|
| Travel Rule | Per transaction (above threshold) | Originator/beneficiary PII capture |
| Reserve Monitor | Daily 07:00 UTC | Proof-of-reserves attestation, GENIUS Act report |
| Regulatory Change | Daily 08:00 UTC | Rule change detection, parameter update webhook |
Continuous ML
Background processes that run without intervention.
| Process | Frequency | Purpose |
|---|---|---|
| Anomaly detection | Every 15 min | Seven-detector ensemble, entity-aware thresholds |
| Typology detection | Every 30 min | Structuring, mixer usage, flash loan patterns |
| Entity clustering | Every 60 sec | Address-to-entity grouping via on-chain heuristics |
| Behavioral baselines | Every 5 min | Per-address behavioral profiles for self-drift detection |
| Cross-chain evasion | Daily | Bridge correlation, chain-hopping detection |
| Community detection | Daily | Leiden algorithm, GLOSH outlier scoring |
| Depeg monitoring | Every 10 min | Reserve ratios, burn acceleration, volume anomalies |
| Sanctions update | Daily 03:00 UTC | OFAC SDN automatic refresh |
| Model self-improvement | Continuous | Analyst verdicts feed 13 ML training pipelines |
The Analyst Workflow
What your compliance team does day to day. Five steps.
Step 1: Log in. Open the dashboard. Alerts are already triaged, scored, and prioritized. High-confidence false positives are auto-resolved. The queue shows only alerts that need human review.
Step 2: Review alerts. Each alert shows the risk score, entity details, counterparty analysis, and the AI triage reasoning. Two options: dismiss as false positive or escalate to a case.
Step 3: Read the investigation. Escalated alerts become cases. The AI investigation agent has already gathered evidence, built a timeline, and produced a recommendation.
Step 4: Approve the SAR. If filing is warranted, review the AI-drafted narrative and BSA XML. Edit if needed. Click "File." Veris submits to FinCEN via secure SFTP.
Step 5: Go home. Veris continues working overnight. Sanctions lists update at 03:00 UTC. Reserves are attested at 07:00 UTC. Regulatory feeds are scanned at 08:00 UTC. Anomaly detection runs every 15 minutes. Nothing is missed.
The Autonomous Workforce
The CCO reviews. The agents produce.
Veris deploys six specialized AI agents organized into two operational tiers. Each agent performs a discrete compliance function, produces structured output with explicit reasoning, and hands off to the next agent in the pipeline through evidence chaining. No agent operates as a black box. Every decision includes the rationale that produced it.
Tier 1: Resolution Team
Tier 1 agents execute in real time on a per-alert basis. They form the sequential pipeline that transforms a raw alert into a finished resolution. Each agent receives the output of the previous agent as input context, creating a continuous chain of evidence and reasoning.
Triage Agent
Classifies incoming alerts within 2-3 seconds. Determines whether an alert is a false positive, requires investigation, or warrants immediate SAR filing.
| Property | Value |
|---|---|
| Trigger | Alert ingestion (real-time) |
| Latency | 2-3 seconds |
| Model | Gemini Flash |
| Output | Classification, confidence score, reasoning chain, auto-resolution eligibility |
The Triage Agent evaluates 39 feature dimensions including transaction velocity, counterparty risk exposure, mixer proximity, and behavioral deviation from established baselines. Its reasoning field provides a plain-language explanation of which factors drove the classification decision.
Alerts classified as false_positive with confidence above 0.95 are eligible for auto-resolution, closing the alert without human review. All other classifications advance to the Investigation Agent.
Investigation Agent
Conducts deep-context analysis over a 90-day transaction window. Synthesizes counterparty intelligence, on-chain evidence, sanctions screening results, and related alert history into a structured evidence package.
| Property | Value |
|---|---|
| Trigger | Triage classification of needs_investigation or immediate_sar |
| Latency | 15-25 seconds |
| Model | Claude |
| Output | Summary, evidence items, transaction timeline, counterparty analysis, risk assessment, recommendation |
The Investigation Agent queries multiple data sources: the transaction graph (90-day lookback), entity attribution database (70,000+ labeled addresses), sanctions lists, and the vector store of previously resolved cases. Its recommendation field contains one of three values: clear, escalate, or file_sar. Every claim in the evidence package includes source attribution.
SAR Generation Agent
Produces FinCEN-compliant SAR narratives and BSA XML filings. Operates only when the Investigation Agent recommends file_sar or when manually triggered by a compliance officer.
| Property | Value |
|---|---|
| Trigger | Investigation recommendation of file_sar or manual request |
| Latency | 10-20 seconds |
| Model | Claude |
| Output | SAR narrative (prose), BSA XML (schema-validated), typology codes with justifications, filing metadata |
The SAR narrative is written to examiner standards. Typology codes reference FinCEN guidance and are accompanied by justification text explaining why each code applies to the specific case. The XML output passes XSD validation before delivery.
Tier 2: Always-on Monitoring
Tier 2 agents operate on periodic schedules independent of individual alerts. They provide continuous surveillance across compliance dimensions that require ongoing attention rather than per-transaction analysis.
Travel Rule Agent
Monitors compliance with FATF Travel Rule requirements across jurisdictions. Tracks originator and beneficiary information completeness for transactions exceeding applicable thresholds.
| Property | Value |
|---|---|
| Schedule | Continuous (event-driven on qualifying transactions) |
| Scope | Cross-jurisdictional threshold monitoring |
| Output | Compliance status per transaction, missing data fields, jurisdiction-specific requirements |
Reserve Monitor Agent
Tracks stablecoin reserve health, attestation timing, and depeg risk indicators. Provides early warning when reserve conditions deviate from expected parameters.
| Property | Value |
|---|---|
| Schedule | Every 5 minutes |
| Scope | All monitored stablecoin issuers |
| Output | Reserve ratio, attestation freshness, depeg probability score, anomaly flags |
Regulatory Change Agent
Monitors regulatory developments across relevant jurisdictions and assesses impact on current compliance configurations. Flags changes that require policy updates or procedural modifications.
| Property | Value |
|---|---|
| Schedule | Daily |
| Scope | US (FinCEN, OCC, SEC), EU (MiCA), FATF guidance |
| Output | Change summaries, impact assessments, recommended policy adjustments |
Agent Summary
| Agent | Tier | Trigger | Latency | Primary Output |
|---|---|---|---|---|
| Triage | 1 | Per alert | 2-3s | Classification + reasoning |
| Investigation | 1 | Post-triage | 15-25s | Evidence package + recommendation |
| SAR Generation | 1 | Post-investigation | 10-20s | Narrative + BSA XML |
| Travel Rule | 2 | Event-driven | Continuous | Compliance status |
| Reserve Monitor | 2 | Every 5 min | Periodic | Reserve health + depeg risk |
| Regulatory Change | 2 | Daily | Periodic | Impact assessment |
Agent Coordination
Tier 1 agents coordinate through a pipeline handoff model. Each agent writes its output to a structured artifact, and the next agent in the sequence receives that artifact as part of its input context. This design provides three properties:
Evidence chaining. The Investigation Agent sees the Triage Agent's reasoning. The SAR Agent sees both the triage classification and the investigation evidence. No context is lost between stages.
Auditability. Each handoff is recorded as an immutable audit log entry with a SHA-256 hash linking it to the previous entry. The complete chain from alert ingestion to final disposition is reconstructable at any time.
Parallelism where possible. While the core resolution pipeline is sequential (triage must complete before investigation begins), Tier 2 agents operate independently and concurrently. Reserve monitoring does not block triage. Regulatory change detection does not delay SAR generation.
Capacity
The workforce scales horizontally. Each Tier 1 agent runs on dedicated Celery worker pools with configurable concurrency. Under standard load, the system processes hundreds of concurrent resolutions without queue backpressure. Tier 2 agents run on separate worker pools to ensure monitoring tasks never compete with resolution workload for compute resources.
The Orchestrator: POST /v1/resolve
One call. Three agents. Complete resolution.
POST /v1/resolve is the primary entry point to Veris. It accepts a transaction hash or alert identifier, orchestrates the full resolution pipeline (triage, investigation, SAR generation), and delivers finished compliance work to your system via webhook callback.
Endpoint
POST https://api.useveris.finance/v1/resolve
Authentication
All requests require a Bearer token in the Authorization header.
Authorization: Bearer vrs_live_a1b2c3d4e5f6...
Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | API key with resolve:write scope |
Content-Type | Yes | application/json |
Idempotency-Key | Recommended | Client-generated UUID. Prevents duplicate resolution for the same request. Valid for 24 hours. |
Request
Submit either a transaction hash or an existing alert identifier. Both trigger the full resolution pipeline.
By Transaction Hash
{
"tx_hash": "0x8a3f7b2e1d9c4a6f5e8b3d7c2a1f9e4d6b8c3a5f7e2d1c9b4a6f8e3d7c2a9d",
"callback_url": "https://your-system.com/webhooks/veris",
"priority": "standard"
}
By Alert ID
{
"alert_id": "alt_9f3b2c8d-4e7a-11ef-a1b2-0242ac130003",
"callback_url": "https://your-system.com/webhooks/veris",
"priority": "standard"
}
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
tx_hash | string | Conditional | Transaction hash to resolve. Required if alert_id is not provided. |
alert_id | string | Conditional | Existing alert UUID. Required if tx_hash is not provided. |
callback_url | string | Yes | HTTPS endpoint that receives the completed resolution via webhook POST. |
priority | string | No | standard (default) or expedited. Expedited resolutions are placed at the front of the processing queue. |
Response
Synchronous Response (HTTP 202 Accepted)
The endpoint returns immediately with a resolution identifier. The full resolution is delivered asynchronously to your callback_url.
{
"resolution_id": "res_7f3b2c9d",
"status": "processing",
"estimated_completion_ms": 35000,
"poll_url": "https://api.useveris.finance/v1/resolutions/res_7f3b2c9d"
}
Webhook Callback Payload (Complete Resolution)
When all three agents have completed their work, Veris delivers the full resolution to your callback_url via HTTP POST. The payload contains the complete output of every agent, including reasoning fields that explain each decision.
{
"resolution_id": "res_7f3b2c9d",
"status": "completed",
"disposition": "sar_required",
"resolution_time_ms": 32400,
"triage": {
"classification": "needs_investigation",
"confidence": 0.94,
"reasoning": "OFAC proximity detected at 2-hop distance. Structuring pattern across 7 transactions below $3,000 threshold within 48-hour window. Counterparty address 0x4a2f... appears in mixer cluster with 89% attribution confidence.",
"risk_factors": [
"ofac_proximity_2hop",
"structuring_pattern",
"mixer_exposure"
],
"auto_resolution_eligible": false,
"agent_version": "triage-v2.4.1",
"completed_at": "2026-03-24T14:22:03.412Z"
},
"investigation": {
"counterparties_analyzed": 14,
"history_days": 90,
"evidence_package_url": "https://api.useveris.finance/v1/evidence/res_7f3b2c9d",
"risk_assessment": "High risk. Mixer exposure 34% of total volume. Cross-chain layering detected via Arbitrum bridge with 3 intermediate wallets. Source of funds traces to unhosted wallet cluster active since 2024-09 with no KYC-verified on-ramp.",
"recommendation": "file_sar",
"reasoning": "Investigation examined 14 counterparties across 90-day lookback. 4 counterparties have direct mixer interaction. Transaction graph reveals a fan-out pattern consistent with layering typology (FinCEN Advisory 2023-A002). Arbitrum bridge usage introduces jurisdictional complexity requiring disclosure.",
"sources_consulted": [
"transaction_graph_90d",
"entity_attribution_db",
"sanctions_screening",
"related_alerts",
"vector_store_precedents"
],
"agent_version": "investigation-v3.1.0",
"completed_at": "2026-03-24T14:22:21.534Z"
},
"sar": {
"narrative_url": "https://api.useveris.finance/v1/sar/res_7f3b2c9d/narrative",
"xml_url": "https://api.useveris.finance/v1/sar/res_7f3b2c9d.xml",
"typology_codes": ["004", "030"],
"typology_justifications": {
"004": "Structuring: 7 transactions between $2,400 and $2,950 within 48 hours, consistent with deliberate avoidance of $3,000 reporting threshold.",
"030": "Layering through virtual currency: Funds moved through 3 intermediate wallets and 1 cross-chain bridge before consolidation at destination address."
},
"amount_involved_usd": 184750.00,
"filing_type": "initial",
"agent_version": "sar-v2.2.0",
"completed_at": "2026-03-24T14:22:33.012Z"
},
"audit_hash": "sha256:a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}
Pipeline Sequence
SDK Examples
Python
import veris
client = veris.Client(api_key="vrs_live_a1b2c3d4e5f6...")
# Synchronous resolution (blocks until complete)
resolution = client.resolve(
tx_hash="0x8a3f7b2e1d9c4a6f5e8b3d7c2a1f9e4d6b8c3a5f7e2d1c9b4a6f8e3d7c2a9d",
priority="standard"
)
print(resolution.disposition) # "sar_required"
print(resolution.triage.reasoning) # Full reasoning chain
print(resolution.investigation.recommendation) # "file_sar"
print(resolution.sar.narrative_url) # Download URL for SAR narrative
# Async resolution with webhook
resolution_ref = client.resolve(
tx_hash="0x8a3f7b2e1d9c4a6f5e8b3d7c2a1f9e4d6b8c3a5f7e2d1c9b4a6f8e3d7c2a9d",
callback_url="https://your-system.com/webhooks/veris",
wait=False
)
print(resolution_ref.resolution_id) # "res_7f3b2c9d"
print(resolution_ref.status) # "processing"
Node.js
const Veris = require("@veris/sdk");
const client = new Veris.Client({ apiKey: "vrs_live_a1b2c3d4e5f6..." });
// Synchronous resolution
const resolution = await client.resolve({
txHash: "0x8a3f7b2e1d9c4a6f5e8b3d7c2a1f9e4d6b8c3a5f7e2d1c9b4a6f8e3d7c2a9d",
priority: "standard",
});
console.log(resolution.disposition); // "sar_required"
console.log(resolution.triage.reasoning); // Full reasoning chain
console.log(resolution.investigation.recommendation); // "file_sar"
console.log(resolution.sar.narrativeUrl); // Download URL
// Async resolution with webhook
const ref = await client.resolve({
txHash: "0x8a3f7b2e1d9c4a6f5e8b3d7c2a1f9e4d6b8c3a5f7e2d1c9b4a6f8e3d7c2a9d",
callbackUrl: "https://your-system.com/webhooks/veris",
wait: false,
});
console.log(ref.resolutionId); // "res_7f3b2c9d"
curl
curl -X POST https://api.useveris.finance/v1/resolve \
-H "Authorization: Bearer vrs_live_a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"tx_hash": "0x8a3f7b2e1d9c4a6f5e8b3d7c2a1f9e4d6b8c3a5f7e2d1c9b4a6f8e3d7c2a9d",
"callback_url": "https://your-system.com/webhooks/veris",
"priority": "standard"
}'
Webhook Callback Specification
Veris delivers the completed resolution to your callback_url as an HTTP POST with the following properties:
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| Signature Header | X-Veris-Signature (HMAC-SHA256 of the request body using your webhook secret) |
| Timeout | 30 seconds |
| Retry Policy | 3 attempts with exponential backoff (10s, 60s, 300s) |
| Expected Response | HTTP 2xx. Any non-2xx response triggers a retry. |
Verifying Webhook Signatures
import hmac
import hashlib
def verify_webhook(payload_bytes, signature_header, webhook_secret):
expected = hmac.new(
webhook_secret.encode(),
payload_bytes,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature_header)
Error Handling
| HTTP Status | Meaning | Action |
|---|---|---|
| 202 | Accepted. Resolution is processing. | Await webhook or poll poll_url. |
| 400 | Invalid request. Missing or malformed fields. | Fix the request and retry. |
| 401 | Authentication failure. Invalid or expired API key. | Verify your API key. |
| 409 | Duplicate. An active resolution already exists for this transaction. | Use the existing resolution_id from the response body. |
| 422 | Unprocessable. Transaction hash not found on any monitored chain. | Verify the transaction hash and chain. |
| 429 | Rate limit exceeded. | Retry after the duration specified in Retry-After header. |
| 500 | Internal error. | Retry with exponential backoff. Contact support if persistent. |
Idempotency
Include an Idempotency-Key header (UUID v4 recommended) to prevent duplicate resolutions. If a request with the same idempotency key is received within 24 hours, Veris returns the existing resolution instead of creating a new one. The response includes a 304 Not Modified status when returning a cached result.
This is critical for webhook-based architectures where network failures may cause your system to retry the initial resolve call. Without an idempotency key, each retry creates a new resolution.
Rate Limits
| Plan | Requests per minute | Concurrent resolutions |
|---|---|---|
| Standard | 60 | 10 |
| Professional | 300 | 50 |
| Enterprise | Custom | Custom |
Rate limit status is returned in response headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1711288800
Resolution Artifacts
Every completed resolution produces a set of structured, immutable artifacts.
Each artifact captures the output of a specific agent in the resolution pipeline, including the reasoning that drove every decision. Artifacts are immutable after creation and are retained according to regulatory requirements.
Artifact Overview
| Artifact | Produced By | Format | Endpoint |
|---|---|---|---|
| Triage Result | Triage Agent | JSON | Included in resolution response |
| Evidence Package | Investigation Agent | JSON | GET /v1/evidence/{resolution_id} |
| SAR Narrative | SAR Agent | Text (Markdown) | GET /v1/sar/{resolution_id}/narrative |
| BSA XML Filing | SAR Agent | XML | GET /v1/sar/{resolution_id}.xml |
| Audit Trail | System | JSON | GET /v1/resolutions/{resolution_id}/audit |
Triage Artifact
The Triage Agent produces a classification result within 2-3 seconds of alert ingestion. The artifact includes the decision, the confidence level, and the complete reasoning chain that explains which factors influenced the classification.
Schema
{
"agent": "triage",
"version": "triage-v2.4.1",
"completed_at": "2026-03-24T14:22:03.412Z",
"classification": "needs_investigation",
"confidence": 0.94,
"reasoning_chain": [
{
"step": 1,
"factor": "ofac_proximity",
"observation": "Counterparty 0x4a2f...8b1c appears at 2-hop distance from OFAC-listed address 0x7d3e...1f9a.",
"weight": 0.35,
"direction": "escalate"
},
{
"step": 2,
"factor": "structuring_detection",
"observation": "7 transactions between $2,400 and $2,950 within 48-hour window. Mean: $2,714. Standard deviation: $187. Pattern consistent with threshold avoidance.",
"weight": 0.30,
"direction": "escalate"
},
{
"step": 3,
"factor": "mixer_exposure",
"observation": "34% of inbound volume traces to known mixer cluster (attribution confidence: 89%).",
"weight": 0.25,
"direction": "escalate"
},
{
"step": 4,
"factor": "account_age",
"observation": "Address active for 187 days. Established activity pattern prior to flagged transactions.",
"weight": 0.10,
"direction": "neutral"
}
],
"risk_factors": [
"ofac_proximity_2hop",
"structuring_pattern",
"mixer_exposure",
"cross_chain_activity"
],
"auto_resolution_eligible": false,
"feature_vector_summary": {
"dimensions_evaluated": 39,
"top_anomalous_features": [
{"feature": "velocity_24h", "z_score": 3.42},
{"feature": "mixer_ratio", "z_score": 4.11},
{"feature": "unique_counterparties_7d", "z_score": 2.87}
]
}
}
Field Reference
| Field | Type | Description |
|---|---|---|
classification | string | One of false_positive, needs_investigation, immediate_sar |
confidence | float | 0.0 to 1.0. Model confidence in the classification decision. |
reasoning_chain | array | Ordered list of reasoning steps. Each step identifies a factor, the observation, its weight in the decision, and whether it pushed toward escalation or de-escalation. |
risk_factors | array | Enumerated risk factor codes detected during triage. |
auto_resolution_eligible | boolean | True when classification is false_positive and confidence exceeds 0.95. |
feature_vector_summary | object | Summary of the 39-dimension feature evaluation, highlighting the most anomalous features by z-score. |
Evidence Package (Investigation Artifact)
The Investigation Agent produces a comprehensive evidence package after analyzing 90 days of transaction history, counterparty relationships, sanctions screening results, and related case precedents.
Endpoint
GET https://api.useveris.finance/v1/evidence/{resolution_id}
Schema
{
"agent": "investigation",
"version": "investigation-v3.1.0",
"completed_at": "2026-03-24T14:22:21.534Z",
"resolution_id": "res_7f3b2c9d",
"summary": "Investigation identified high-risk activity involving mixer exposure, cross-chain layering, and structuring patterns. 14 counterparties analyzed across a 90-day window. 4 counterparties have direct mixer interaction.",
"reasoning": "The combination of structuring below reporting thresholds, mixer usage exceeding 30% of volume, and cross-chain movement through a bridge with no legitimate business rationale constitutes a pattern consistent with money laundering typologies 004 and 030.",
"evidence_items": [
{
"id": "ev_001",
"type": "transaction_pattern",
"description": "Structuring pattern detected across 7 transactions",
"significance": "high",
"data": {
"transaction_count": 7,
"amount_range_usd": [2400, 2950],
"time_window_hours": 48,
"mean_amount_usd": 2714.29,
"threshold_proximity": 0.98
},
"reasoning": "Transaction amounts cluster tightly below the $3,000 reporting threshold. The narrow standard deviation ($187) and compressed time window are inconsistent with organic spending behavior."
},
{
"id": "ev_002",
"type": "mixer_exposure",
"description": "Inbound funds from known mixer cluster",
"significance": "high",
"data": {
"mixer_volume_pct": 34.2,
"attribution_confidence": 0.89,
"mixer_cluster_id": "mc_tornado_eth_2024",
"traced_amount_usd": 63184.50
},
"reasoning": "34.2% of total inbound volume traces to a mixer cluster with 89% attribution confidence."
},
{
"id": "ev_003",
"type": "cross_chain_activity",
"description": "Arbitrum bridge usage with intermediate wallets",
"significance": "medium",
"data": {
"bridge": "Arbitrum L1-L2",
"intermediate_wallets": 3,
"total_bridged_usd": 47200.00,
"destination_chain": "Arbitrum One"
},
"reasoning": "Funds were bridged through 3 intermediate wallets before consolidation."
}
],
"timeline": [
{"timestamp": "2026-01-15T08:12:00Z", "event": "First transaction from subject address", "amount_usd": 2800.00, "tx_hash": "0xabc1..."},
{"timestamp": "2026-01-15T14:45:00Z", "event": "Second transaction, same counterparty", "amount_usd": 2650.00, "tx_hash": "0xabc2..."},
{"timestamp": "2026-02-20T11:30:00Z", "event": "Bridge to Arbitrum via intermediate wallet", "amount_usd": 47200.00, "tx_hash": "0xdef1..."},
{"timestamp": "2026-03-24T14:20:00Z", "event": "Alert triggered by velocity anomaly detector", "amount_usd": null, "tx_hash": null}
],
"counterparty_analysis": {
"total_analyzed": 14,
"high_risk": 4,
"medium_risk": 3,
"low_risk": 7,
"notable_counterparties": [
{
"address": "0x4a2f...8b1c",
"risk_level": "high",
"labels": ["mixer_user", "ofac_2hop"],
"interaction_count": 5,
"total_volume_usd": 63184.50
}
]
},
"recommendation": "file_sar",
"recommendation_reasoning": "Three independent risk indicators converge on the same subject address. Each indicator alone would warrant enhanced review. Together, they establish a pattern that meets the SAR filing threshold under 31 CFR 1020.320.",
"sources_consulted": [
"transaction_graph_90d",
"entity_attribution_db",
"sanctions_screening",
"related_alerts",
"vector_store_precedents"
]
}
Evidence Item Types
| Type | Description |
|---|---|
transaction_pattern | Statistical anomaly in transaction behavior (structuring, velocity, timing) |
mixer_exposure | Funds traced to mixing services or privacy protocols |
cross_chain_activity | Bridge usage, multi-chain movement patterns |
sanctions_proximity | Relationship to sanctioned addresses (direct or n-hop) |
entity_attribution | Known entity labels applied to addresses in the transaction graph |
behavioral_deviation | Departure from established baseline activity for the address |
dormant_reactivation | Previously inactive address resuming activity |
SAR Artifact
The SAR Agent produces two outputs: a human-readable narrative and a machine-readable BSA XML filing. Both are generated from the same underlying evidence and reasoning.
Narrative Endpoint
GET https://api.useveris.finance/v1/sar/{resolution_id}/narrative
Returns the SAR narrative as plain text formatted for direct inclusion in a FinCEN filing.
XML Endpoint
GET https://api.useveris.finance/v1/sar/{resolution_id}.xml
Returns schema-validated BSA XML conforming to the FinCEN E-Filing specification.
SAR Metadata Schema
{
"agent": "sar",
"version": "sar-v2.2.0",
"completed_at": "2026-03-24T14:22:33.012Z",
"resolution_id": "res_7f3b2c9d",
"narrative_url": "https://api.useveris.finance/v1/sar/res_7f3b2c9d/narrative",
"xml_url": "https://api.useveris.finance/v1/sar/res_7f3b2c9d.xml",
"typology_codes": ["004", "030"],
"typology_justifications": {
"004": "Structuring: 7 transactions between $2,400 and $2,950 within a 48-hour period. Amounts cluster within 2% of the $3,000 reporting threshold.",
"030": "Layering through virtual currency: Funds moved from Ethereum mainnet through 3 intermediate wallets to an Arbitrum bridge, then consolidated at a single destination."
},
"amount_involved_usd": 184750.00,
"date_range": {
"start": "2026-01-15",
"end": "2026-03-24"
},
"filing_type": "initial",
"subject_addresses": [
"0x8a3f...2c9d",
"0x4a2f...8b1c"
],
"xsd_validation": {
"valid": true,
"schema_version": "2.0",
"validated_at": "2026-03-24T14:22:32.988Z"
}
}
Audit Trail
Every resolution produces an immutable, hash-chained audit trail. Each entry records a discrete event in the resolution lifecycle and is linked to the previous entry by SHA-256 hash, creating a tamper-evident log.
Endpoint
GET https://api.useveris.finance/v1/resolutions/{resolution_id}/audit
Schema
{
"resolution_id": "res_7f3b2c9d",
"chain_valid": true,
"entries": [
{
"sequence": 1,
"timestamp": "2026-03-24T14:22:01.002Z",
"event": "resolution_initiated",
"actor": "api",
"detail": "Resolution created from tx_hash 0x8a3f...2c9d",
"hash": "sha256:0000...0000",
"previous_hash": null
},
{
"sequence": 2,
"timestamp": "2026-03-24T14:22:01.015Z",
"event": "triage_dispatched",
"actor": "orchestrator",
"detail": "Triage Agent invoked with 39-dimension feature vector",
"hash": "sha256:a1b2...f0a1b2",
"previous_hash": "sha256:0000...0000"
},
{
"sequence": 3,
"timestamp": "2026-03-24T14:22:03.412Z",
"event": "triage_completed",
"actor": "triage-v2.4.1",
"detail": "Classification: needs_investigation (confidence: 0.94)",
"hash": "sha256:b2c3...a1b2c3",
"previous_hash": "sha256:a1b2...f0a1b2"
}
]
}
Audit Entry Fields
| Field | Type | Description |
|---|---|---|
sequence | integer | Monotonically increasing event counter within the resolution |
timestamp | string | ISO 8601 timestamp with millisecond precision |
event | string | Event type identifier |
actor | string | System component or agent version that produced the event |
detail | string | Human-readable description of the event |
hash | string | SHA-256 hash of the current entry (includes all fields plus previous_hash) |
previous_hash | string | SHA-256 hash of the preceding entry. Null for the first entry. |
Chain Verification
The chain_valid field in the audit response indicates whether the system has verified the integrity of the hash chain. Clients can independently verify by computing the SHA-256 hash of each entry and confirming it matches the hash field, and that each entry's previous_hash matches the preceding entry's hash.
Download Endpoints
| Artifact | Method | Endpoint | Content-Type |
|---|---|---|---|
| Complete Resolution | GET | /v1/resolutions/{resolution_id} | application/json |
| Evidence Package | GET | /v1/evidence/{resolution_id} | application/json |
| SAR Narrative | GET | /v1/sar/{resolution_id}/narrative | text/plain |
| BSA XML | GET | /v1/sar/{resolution_id}.xml | application/xml |
| Audit Trail | GET | /v1/resolutions/{resolution_id}/audit | application/json |
| Evidence Item | GET | /v1/evidence/{resolution_id}/items/{item_id} | application/json |
All download endpoints require authentication via Bearer token with resolutions:read scope.
curl Examples
# Download evidence package
curl -s https://api.useveris.finance/v1/evidence/res_7f3b2c9d \
-H "Authorization: Bearer vrs_live_a1b2c3d4e5f6..." \
-o evidence.json
# Download SAR narrative
curl -s https://api.useveris.finance/v1/sar/res_7f3b2c9d/narrative \
-H "Authorization: Bearer vrs_live_a1b2c3d4e5f6..." \
-o sar_narrative.txt
# Download BSA XML
curl -s https://api.useveris.finance/v1/sar/res_7f3b2c9d.xml \
-H "Authorization: Bearer vrs_live_a1b2c3d4e5f6..." \
-o sar_filing.xml
# Download audit trail
curl -s https://api.useveris.finance/v1/resolutions/res_7f3b2c9d/audit \
-H "Authorization: Bearer vrs_live_a1b2c3d4e5f6..." \
-o audit_trail.json
Retention Policy
| Artifact | Retention Period | Basis |
|---|---|---|
| Resolution (complete) | 7 years | BSA record retention (31 CFR 1010.430) |
| Evidence Package | 7 years | BSA record retention |
| SAR Narrative | 7 years | FinCEN SAR retention requirements |
| BSA XML | 7 years | FinCEN SAR retention requirements |
| Audit Trail | 7 years | SOC 2 and BSA compliance |
| Triage Artifact | 7 years | Part of resolution record |
All artifacts are encrypted at rest using AES-256-GCM. Access to artifacts is logged in the audit trail. Deletion before the retention period requires compliance officer authorization and produces an audit entry documenting the reason and approver.
Triage Agent
Classifies every alert within 2 to 3 seconds.
The Triage Agent classifies every alert generated by the Veris platform within 2 to 3 seconds. It determines whether an alert is a false positive, requires human investigation, or warrants immediate SAR filing. Every alert passes through this agent before reaching an analyst queue.
Classification Pipeline
The agent operates in two stages: a deterministic pre-filter and an AI classification layer.
Stage 1: Deterministic Pre-Filter
Before invoking the language model, the agent applies rule-based checks that produce instant classifications with zero ambiguity.
| Condition | Result | Rationale |
|---|---|---|
| OFAC SDN list match (exact or fuzzy above 95%) | immediate_sar | Federal law requires immediate filing. No model discretion permitted. |
| Known mixer or tumbler entity | immediate_sar | Mixer interaction is a standalone typology under FinCEN Advisory FIN-2019-A003. |
| Sanctioned jurisdiction (DPRK, Iran, Syria, Cuba, Crimea) | immediate_sar | OFAC comprehensive sanctions programs require blocking and reporting. |
When the pre-filter triggers, the agent bypasses the LLM entirely and returns a deterministic result with confidence: 1.0 and a citation to the matching rule.
Stage 2: AI Classification
Alerts that pass the pre-filter enter the AI classification layer. The model receives a structured context payload and returns one of three classifications.
Input Context:
- Alert metadata (ID, type, severity, creation timestamp)
- Transfer details (from/to address, amount, token, chain, block number)
- Entity information (labels, risk scores, historical transaction patterns)
- Behavioral profile (velocity metrics, dormancy indicators, peer group deviation)
- EVM state analysis (contract interactions, approval patterns, token holdings)
Classifications:
| Classification | Definition | Typical Analyst Action |
|---|---|---|
false_positive | Alert does not represent suspicious activity. Noise from legitimate operations. | Auto-close with audit trail. |
needs_investigation | Alert contains indicators that require human review and deeper analysis. | Route to analyst queue for case creation. |
immediate_sar | Alert contains clear indicators of illicit activity requiring regulatory filing. | Escalate to SAR preparation pipeline. |
Output Schema
Every triage result conforms to the following structure. Pydantic validation enforces type correctness and value constraints on every response before it is persisted.
{
"classification": "needs_investigation",
"severity": "high",
"confidence": 0.92,
"reasoning_chain": [
"Address 0xabc...def received 847,000 USDC from a mixer-adjacent address within 3 blocks.",
"Recipient address was dormant for 186 days before this transfer.",
"Amount exceeds the 99th percentile for this entity's historical volume."
],
"risk_factors": [
{
"factor": "mixer_proximity",
"weight": 0.35,
"detail": "1-hop distance from Tornado Cash deposit address"
},
{
"factor": "dormancy_reactivation",
"weight": 0.25,
"detail": "186-day dormancy period followed by high-value inflow"
},
{
"factor": "volume_anomaly",
"weight": 0.30,
"detail": "Transfer amount 12.4x above entity 30-day average"
}
],
"recommended_action": "escalate_to_case"
}
Field Reference
| Field | Type | Constraints |
|---|---|---|
classification | string | One of: false_positive, needs_investigation, immediate_sar |
severity | string | One of: low, medium, high, critical |
confidence | float | Range: 0.0 to 1.0. Values below 0.6 trigger mandatory human review. |
reasoning_chain | array[string] | Ordered list of reasoning steps. Minimum 1 entry. |
risk_factors | array[object] | Each entry includes factor, weight (0.0 to 1.0), and detail. |
recommended_action | string | One of: auto_close, escalate_to_case, escalate_to_sar |
Model Configuration
| Parameter | Value |
|---|---|
| Model | Gemini Flash |
| Output format | Unstructured text with JSON extraction |
| Max tokens | 2048 |
| Temperature | 0.1 |
The agent does not use response_mime_type="application/json". Instead, the model returns text that may include markdown fences, and a dedicated _extract_json() parser handles extraction. This avoids internal JSON parsing failures observed with forced structured output modes.
Anti-Hallucination Controls
- Pydantic validation: Every output field is validated against strict type and range constraints before persistence.
- Confidence floor: Classifications with confidence below 0.6 are flagged for mandatory human review regardless of the model's recommendation.
- Deterministic override: Rule-based pre-filter results always supersede model outputs when both fire on the same alert.
- Audit trail: Every triage result is stored in the
ai_audit_loghypertable with full input context for post-hoc review.
Latency Profile
| Metric | Value |
|---|---|
| P50 | 1.8 seconds |
| P95 | 2.9 seconds |
| P99 | 3.4 seconds |
| Deterministic pre-filter | < 50 milliseconds |
Investigation Agent
Deep compliance analysis in 15 to 25 seconds.
The Investigation Agent performs deep compliance analysis on escalated alerts, producing structured evidence packages in 15 to 25 seconds. It executes parallel data gathering across multiple subsystems, applies PII redaction before LLM submission, and augments its analysis with historical investigation context retrieved via vector search.
Architecture
The agent uses a 2-phase data gathering architecture built on ThreadPoolExecutor(max_workers=8). Phase A executes independent queries in parallel. Phase B executes dependent queries that require Phase A results as input.
Phase A: Independent Queries (25-second timeout)
All 8 queries execute concurrently. If any query exceeds the 25-second timeout, the agent proceeds with partial results and notes the data gap in its output.
| Query | Source | Data Retrieved |
|---|---|---|
| Transfer history | TimescaleDB | 90-day transfer events for the subject address |
| Entity profile | TimescaleDB | Labels, risk scores, registration metadata |
| Sanctions check | OFAC + EU + UK lists | Real-time screening against consolidated watchlists |
| Behavioral baseline | Redis + TimescaleDB | Velocity metrics, dormancy state, peer group statistics |
| Related alerts | TimescaleDB | Alerts sharing addresses, entities, or transaction patterns |
| On-chain graph | TimescaleDB | 2-hop counterparty network with edge weights |
| Contract analysis | EVM state | Token approvals, contract interactions, proxy patterns |
| Past investigations | Qdrant | Vector similarity search over prior investigation reports |
Phase B: Dependent Queries (10-second timeout)
Phase B queries use Phase A results to perform targeted lookups.
| Query | Depends On | Data Retrieved |
|---|---|---|
| Counterparty risk profiles | On-chain graph | Risk scores for top 10 counterparties by volume |
| Cross-chain correlation | Transfer history | Matching patterns across Ethereum, Base, Polygon, Arbitrum |
| Typology matching | Behavioral baseline + transfer history | Best-fit typology codes from the FinCEN registry |
RAG: Historical Investigation Context
The agent queries the Qdrant vector store using Gemini text-embedding-004 embeddings to retrieve relevant past investigations. This provides the model with institutional memory: how similar patterns were analyzed, what conclusions were reached, and what regulatory actions followed.
Retrieved documents are ranked by cosine similarity. The top 5 results above a 0.72 similarity threshold are included in the prompt context.
PII Redaction
Before submitting any data to the language model, the agent applies PII redaction to all text fields. The following categories are redacted:
| PII Category | Redaction Method |
|---|---|
| Personal names | Replaced with [PERSON_1], [PERSON_2], etc. |
| Email addresses | Replaced with [EMAIL_REDACTED] |
| Phone numbers | Replaced with [PHONE_REDACTED] |
| Physical addresses | Replaced with [ADDRESS_REDACTED] |
| Government IDs (SSN, passport) | Replaced with [GOV_ID_REDACTED] |
Blockchain addresses, transaction hashes, and token amounts are not redacted. They are required for accurate analysis and do not constitute PII under applicable regulations.
A mapping table is retained server-side so that the final output can be re-hydrated with original values before delivery to the requesting analyst.
Output Schema
{
"summary": "Address 0xabc...def received 2.1M USDC through a layering pattern involving 14 intermediate wallets over 72 hours. The originating funds trace to a Tornado Cash withdrawal. Behavioral analysis indicates coordinated activity with 3 previously flagged entities.",
"evidence_items": [
{
"type": "transaction",
"tx_hash": "0x789...012",
"relevance": "Initial Tornado Cash withdrawal funding the layering chain",
"amount_usd": 500000.00,
"timestamp": "2026-03-18T14:22:31Z"
},
{
"type": "entity_link",
"entity_id": "ent_abc123",
"relevance": "Shared counterparty with 2 prior SAR filings",
"risk_score": 0.91
}
],
"timeline": [
{"timestamp": "2026-03-18T14:22:31Z", "event": "Tornado Cash withdrawal of 500,000 USDC to 0x111...aaa"},
{"timestamp": "2026-03-18T15:04:12Z", "event": "Split into 5 transfers of 100,000 USDC across intermediate wallets"},
{"timestamp": "2026-03-20T09:31:45Z", "event": "Consolidation into 0xabc...def with final balance of 2.1M USDC"}
],
"counterparty_analysis": {
"total_counterparties": 14,
"high_risk_counterparties": 3,
"sanctioned_counterparties": 0,
"network_density": 0.23
},
"recommendation": "file_sar",
"typology_codes": ["BSA-2019-TC-03", "BSA-2019-TC-07"],
"confidence": 0.94
}
Field Reference
| Field | Type | Description |
|---|---|---|
summary | string | Analyst-ready narrative summarizing findings. Maximum 500 words. |
evidence_items | array[object] | Discrete pieces of evidence with type, relevance, and supporting data. |
timeline | array[object] | Chronologically ordered events relevant to the investigation. |
counterparty_analysis | object | Aggregate statistics on the counterparty network. |
recommendation | string | One of: close_no_action, monitor, file_sar. |
typology_codes | array[string] | Applicable FinCEN typology codes. |
confidence | float | Range: 0.0 to 1.0. |
Model Configuration
| Parameter | Value |
|---|---|
| Model | Claude (Anthropic) |
| Max tokens | 4096 |
| Temperature | 0.2 |
| Context window utilization | Up to 80K tokens for complex cases |
Case Integration
When the Investigation Agent completes its analysis, the result is written to the ai_investigation_result JSONB column on the associated case record. If a case does not yet exist for the alert, the agent creates one automatically and links all related alerts.
If the recommendation is file_sar, the case status transitions to pending_sar, making it eligible for SAR Generation Agent processing.
Latency Profile
| Metric | Value |
|---|---|
| P50 | 16 seconds |
| P95 | 23 seconds |
| P99 | 28 seconds |
| Phase A (parallel gather) | 3 to 8 seconds |
| Phase B (dependent gather) | 1 to 4 seconds |
| LLM inference | 10 to 18 seconds |
SAR Generation Agent
Regulatory filing narratives and structured XML in 10 to 20 seconds.
The SAR Generation Agent produces regulatory filing narratives and structured XML documents in 10 to 20 seconds. It supports three jurisdictions (US, EU, UK), three languages (EN, DE, FR), and includes a hallucination gate that verifies every factual claim against source data before output delivery.
Jurisdiction Support
| Jurisdiction | Regulatory Body | Filing Format | Narrative Standard |
|---|---|---|---|
| United States | FinCEN | BSA XML (FinCEN SAR) | Part V narrative, 5W structure |
| European Union | AMLA (MiCA framework) | goAML XML | STR narrative per national FIU requirements |
| United Kingdom | FCA / NCA | SAR Online (NCA format) | DAML or consent SAR narrative |
Narrative Structure: FinCEN 5W Framework
For US filings, the narrative follows the FinCEN-mandated 5W structure. Each section is generated independently and assembled into the final Part V narrative.
| Section | Content |
|---|---|
| Who | Subject identification. Entity type, known aliases, wallet addresses, risk profile summary. |
| What | Suspicious activity description. Transaction patterns, typology classification, deviation from baseline behavior. |
| Where | Geographic and chain context. Blockchain networks involved, jurisdiction of known counterparties, IP geolocation data (when available). |
| When | Temporal scope. Date range of suspicious activity, key timestamps, sequence of events. |
| Why | Analyst reasoning. Why the activity is suspicious, what legitimate explanations were considered and excluded, regulatory basis for filing. |
Language Support
| Language | Jurisdictions | Notes |
|---|---|---|
| English | US, UK, EU | Default for all jurisdictions |
| German | EU | Required by BaFin for German-supervised entities |
| French | EU | Required by ACPR for French-supervised entities |
The language is selected per filing. The agent generates the narrative in the target language natively rather than translating from English, ensuring idiomatic regulatory terminology.
Hallucination Gate
Every SAR narrative passes through a fact verification layer before delivery. The gate checks every verifiable claim in the generated text against the source data that was provided to the model.
Verified Fields
| Field Type | Verification Method |
|---|---|
| Blockchain addresses | Exact string match against case transfer records |
| Transaction hashes | Exact string match against case transfer records |
| USD amounts | Numeric match within 0.01 tolerance against source amounts |
| Timestamps | ISO 8601 match against source event timestamps |
| Entity names | Exact match against entity profile records |
| Typology codes | Validated against the FinCEN typology registry |
Verification Process
- The agent extracts all factual claims from the generated narrative using structured parsing.
- Each claim is compared against the investigation evidence payload.
- Claims that fail verification are flagged with the specific discrepancy.
- If any claim fails, the narrative is regenerated with explicit correction instructions.
- The retry loop executes up to 3 times. If verification still fails after 3 attempts, the output is delivered with a
verification_warningsarray detailing unresolved discrepancies for human review.
Typology Code Selection
The agent selects typology codes from the FinCEN advisory registry based on the investigation evidence. Each selected code includes a justification explaining why the observed activity matches that typology.
{
"typology_codes": ["BSA-2019-TC-03", "BSA-2019-TC-07"],
"typology_justifications": [
{
"code": "BSA-2019-TC-03",
"name": "Layering through multiple wallets",
"justification": "Funds moved through 14 intermediate wallets within 72 hours with no economic purpose. Each hop involved a different chain to obscure the trail."
},
{
"code": "BSA-2019-TC-07",
"name": "Use of mixing services",
"justification": "Originating funds trace to a Tornado Cash withdrawal. 500,000 USDC deposited to the mixer 4 hours before the first layering transfer."
}
]
}
Output Schema
{
"narrative": "Part V narrative text (typically 800-2000 words)...",
"narrative_language": "en",
"jurisdiction": "us",
"typology_codes": ["BSA-2019-TC-03", "BSA-2019-TC-07"],
"typology_justifications": [],
"amount_usd": 2147500.00,
"date_range": {
"start": "2026-03-18T00:00:00Z",
"end": "2026-03-20T23:59:59Z"
},
"filing_type": "initial",
"subject_count": 3,
"verification_status": "passed",
"verification_warnings": [],
"xml_document": "base64-encoded BSA XML document"
}
Field Reference
| Field | Type | Description |
|---|---|---|
narrative | string | Complete filing narrative in the specified language. |
narrative_language | string | ISO 639-1 code: en, de, or fr. |
jurisdiction | string | One of: us, eu, uk. |
typology_codes | array[string] | Selected FinCEN typology codes. |
typology_justifications | array[object] | Per-code justification with name and reasoning. |
amount_usd | float | Total suspicious activity amount in USD. |
date_range | object | Start and end timestamps of the activity period. |
filing_type | string | One of: initial, continuing, joint. |
subject_count | integer | Number of subjects identified in the filing. |
verification_status | string | One of: passed, passed_with_warnings, failed. |
verification_warnings | array[string] | Discrepancies that could not be resolved after retry. |
xml_document | string | Base64-encoded regulatory XML (BSA, goAML, or NCA format). |
XML Validation
Generated XML documents are validated against the official XSD schemas before delivery:
| Jurisdiction | Schema |
|---|---|
| US (FinCEN) | FinCEN SAR XML schema (current version) |
| EU (goAML) | UNODC goAML 4.x XSD |
| UK (NCA) | NCA SAR Online schema |
Documents that fail XSD validation are regenerated. Validation errors are logged to the ai_audit_log for compliance review.
Storage and Encryption
Filed SAR documents are encrypted at rest using AES-256 and stored in the compliance archive. Access requires sar:read permission and generates an audit log entry for every retrieval.
Latency Profile
| Metric | Value |
|---|---|
| P50 | 12 seconds |
| P95 | 18 seconds |
| P99 | 24 seconds |
| Hallucination gate | 1 to 3 seconds |
| XML generation and validation | < 500 milliseconds |
Reserve Monitor Agent
Proof of Reserves verification for GENIUS Act compliance.
The Reserve Monitor Agent verifies stablecoin reserve attestations against on-chain circulating supply on a daily schedule. It produces audit-ready Proof of Reserves reports formatted for GENIUS Act compliance and triggers alerts when reserve shortfalls exceed configurable thresholds.
Schedule
The agent executes daily at 07:00 UTC. This timing ensures reports are available at the start of business hours across US and European time zones. Ad-hoc executions can be triggered via the API for on-demand verification.
Function
The agent performs the following operations in sequence:
- Query circulating supply: Retrieve total supply for each monitored stablecoin by querying on-chain token contracts across all indexed chains (Ethereum, Base, Polygon, Arbitrum).
- Retrieve reserve attestations: Pull the latest reserve attestation data from issuer feeds and on-chain proof mechanisms.
- Compute reserve ratio: Calculate
reserve_value / circulating_supplyfor each token. - Evaluate thresholds: Compare the reserve ratio against configured alert thresholds.
- Generate report: Produce a structured Proof of Reserves report.
Monitored Stablecoins
| Token | Issuer | Reserve Type |
|---|---|---|
| USDC | Circle | Monthly attestation (Grant Thornton) + real-time on-chain |
| USDT | Tether | Quarterly reserve report + on-chain |
| DAI | MakerDAO | On-chain collateral (fully transparent) |
| FRAX | Frax Finance | On-chain AMO + off-chain reserves |
Additional tokens can be added through the configuration API without code changes.
Shortfall Detection
Thresholds are configurable per token and per client. Default thresholds:
| Threshold | Reserve Ratio | Action |
|---|---|---|
| Healthy | >= 1.00 | No action. Report filed as clean attestation. |
| Warning | 0.98 to 0.9999 | reserve.warning webhook fired. Report flagged for review. |
| Critical | 0.95 to 0.9799 | reserve.critical webhook fired. Analyst notification. |
| Emergency | < 0.95 | reserve.emergency webhook fired. All pending transactions for the affected token are held for manual review. |
Output: Proof of Reserves Report
The report conforms to the disclosure requirements outlined in the GENIUS Act (Guiding and Establishing National Innovation for U.S. Stablecoins).
{
"report_id": "por_2026032407",
"generated_at": "2026-03-24T07:00:14Z",
"reporting_period": {
"start": "2026-03-23T00:00:00Z",
"end": "2026-03-23T23:59:59Z"
},
"tokens": [
{
"symbol": "USDC",
"circulating_supply": 43217890456.12,
"reserve_value": 43298100000.00,
"reserve_ratio": 1.00186,
"status": "healthy",
"chains": [
{"chain": "ethereum", "supply": 28450000000.00},
{"chain": "base", "supply": 8120000000.00},
{"chain": "polygon", "supply": 4230000000.00},
{"chain": "arbitrum", "supply": 2417890456.12}
],
"attestation_source": "Grant Thornton monthly attestation (2026-02-28)",
"attestation_lag_days": 24
}
],
"aggregate": {
"total_circulating": 87654321000.00,
"total_reserves": 87890100000.00,
"aggregate_ratio": 1.00269,
"status": "healthy"
},
"genius_act_compliance": {
"daily_disclosure": true,
"reserve_composition_disclosed": true,
"third_party_attestation": true,
"redemption_capability_confirmed": true
}
}
Field Reference
| Field | Type | Description |
|---|---|---|
report_id | string | Unique identifier for the report. Format: por_YYYYMMDDHH. |
generated_at | string | ISO 8601 timestamp of report generation. |
reporting_period | object | 24-hour window covered by the report. |
tokens | array[object] | Per-token reserve verification results. |
tokens[].reserve_ratio | float | Reserve value divided by circulating supply. |
tokens[].attestation_lag_days | integer | Days since the most recent third-party attestation. |
aggregate | object | Cross-token aggregate reserve metrics. |
genius_act_compliance | object | Boolean checklist for GENIUS Act disclosure requirements. |
Webhook Notifications
When a threshold is breached, the agent fires a webhook to all configured endpoints.
{
"event": "reserve.warning",
"token": "USDT",
"reserve_ratio": 0.9923,
"circulating_supply": 44100000000.00,
"reserve_value": 43760430000.00,
"shortfall_usd": 339570000.00,
"timestamp": "2026-03-24T07:00:14Z",
"report_id": "por_2026032407"
}
Data Retention
Proof of Reserves reports are retained for 7 years in compliance with BSA record-keeping requirements. Reports are stored in the reserve_attestations hypertable with 30-day compression.
Latency Profile
| Metric | Value |
|---|---|
| Full report generation | 8 to 15 seconds |
| Per-chain supply query | 1 to 3 seconds |
| Attestation retrieval | 2 to 5 seconds |
Travel Rule Agent
FATF Recommendation 16 compliance for every qualifying transfer.
The Travel Rule Agent captures originator and beneficiary personally identifiable information (PII) for every transaction exceeding the $3,000 threshold, ensuring compliance with FATF Recommendation 16 and its national implementations.
Trigger Condition
The agent activates on every transfer event where amount_usd >= 3000.00. This threshold aligns with:
- FATF Recommendation 16: $1,000 USD/EUR threshold for virtual asset transfers (Veris applies the stricter US threshold by default)
- FinCEN Travel Rule (31 CFR 103.33(g)): $3,000 threshold for funds transfers
- EU Transfer of Funds Regulation (2023/1113): EUR 0 threshold (all transfers require originator/beneficiary data under MiCA)
The threshold is configurable per client to accommodate jurisdiction-specific requirements. EU-supervised clients typically set this to 0 to comply with TFR requirements.
Function
For each qualifying transfer, the agent:
- Identifies the originator VASP: Resolves the sending address to its hosting Virtual Asset Service Provider using the Veris address intelligence database.
- Identifies the beneficiary VASP: Resolves the receiving address to its hosting VASP.
- Retrieves originator PII: Queries the originator VASP via the IVMS 101 messaging protocol for required data fields.
- Retrieves beneficiary PII: Queries the beneficiary VASP for required data fields.
- Validates completeness: Ensures all mandatory fields are present per the applicable jurisdiction's requirements.
- Files the record: Persists the structured PII record linked to the transfer event.
Required Data Fields (FATF Recommendation 16)
Originator Information
| Field | Required | IVMS 101 Path |
|---|---|---|
| Full name | Yes | originator.naturalPerson.name |
| Account number (wallet address) | Yes | originator.accountNumber |
| Physical address OR national ID OR date/place of birth | Yes (at least one) | originator.geographicAddress / originator.nationalIdentification / originator.dateAndPlaceOfBirth |
Beneficiary Information
| Field | Required | IVMS 101 Path |
|---|---|---|
| Full name | Yes | beneficiary.naturalPerson.name |
| Account number (wallet address) | Yes | beneficiary.accountNumber |
IVMS 101 Compliance
All PII records conform to the InterVASP Messaging Standard (IVMS 101), the industry standard data model for travel rule information exchange. The agent supports the following transport protocols for inter-VASP communication:
| Protocol | Status | Notes |
|---|---|---|
| TRISA | Supported | Travel Rule Information Sharing Architecture |
| TRP (OpenVASP) | Supported | Travel Rule Protocol by OpenVASP Association |
| Sygna Bridge | Supported | Used primarily in APAC jurisdictions |
Output Schema
{
"transfer_id": "txn_abc123def456",
"tx_hash": "0x789...012",
"amount_usd": 47500.00,
"threshold_applied": 3000.00,
"jurisdiction": "us",
"originator": {
"vasp_name": "Exchange Alpha",
"vasp_lei": "529900T8BM49AURSDO55",
"natural_person": {
"name": "[REDACTED]",
"account_number": "0xabc...def",
"geographic_address": "[REDACTED]"
},
"data_complete": true
},
"beneficiary": {
"vasp_name": "Exchange Beta",
"vasp_lei": "213800WSGIIZCXF1P572",
"natural_person": {
"name": "[REDACTED]",
"account_number": "0x111...222",
"geographic_address": "[REDACTED]"
},
"data_complete": true
},
"compliance_status": "complete",
"filed_at": "2026-03-24T14:22:31Z",
"ivms_version": "101.2020"
}
Note: PII fields are redacted in API responses. Full PII is accessible only through the compliance portal with travel_rule:read permission and generates an audit log entry on every access.
Compliance Status Values
| Status | Meaning |
|---|---|
complete | All required PII fields present for both originator and beneficiary. |
partial | One or more required fields missing. Counterparty VASP has been notified. 72-hour remediation window active. |
failed | Counterparty VASP did not respond within the remediation window. Alert generated for compliance team. |
exempt | Transfer is exempt from travel rule (self-hosted wallet below jurisdiction threshold). |
Unhosted Wallet Handling
When the originator or beneficiary address resolves to an unhosted (self-custody) wallet, the agent applies jurisdiction-specific rules:
| Jurisdiction | Unhosted Wallet Rule |
|---|---|
| US (FinCEN) | No additional requirements below $3,000. Above $3,000, VASP must collect and retain counterparty information. |
| EU (TFR) | Above EUR 1,000, VASP must verify ownership of the unhosted wallet. Below EUR 1,000, simplified due diligence. |
| UK (FCA) | VASP must collect beneficiary information regardless of wallet type for transfers above GBP 1,000. |
Data Retention
Travel rule records are retained for 5 years from the date of the transfer, per FATF Recommendation 11 and BSA record-keeping requirements. PII is encrypted at rest using AES-256.
Latency Profile
| Metric | Value |
|---|---|
| VASP identification | < 500 milliseconds |
| PII retrieval (cooperative VASP) | 1 to 5 seconds |
| PII retrieval (slow VASP) | Up to 30 seconds (async completion) |
| Record filing | < 200 milliseconds |
Regulatory Change Agent
8 regulatory feeds. Daily monitoring. Actionable impact analysis.
The Regulatory Change Agent monitors 8 regulatory feeds daily, detects rule changes that affect compliance operations, maps the impact to Veris configuration parameters, and fires webhooks with actionable payloads. It enables institutions to respond to regulatory shifts within hours rather than weeks.
Schedule
The agent executes daily at 08:00 UTC. This runs 1 hour after the Reserve Monitor Agent, ensuring that reserve-related regulatory changes are evaluated against the most recent Proof of Reserves data.
Ad-hoc executions can be triggered via the API when a breaking regulatory announcement requires immediate assessment.
Monitored Feeds
| Feed | Jurisdiction | Content Type |
|---|---|---|
| FinCEN | United States | Advisories, rules, interpretive guidance, SAR filing updates |
| OFAC | United States | SDN list updates, sectoral sanctions, general licenses, FAQs |
| FATF | International | Recommendations, mutual evaluation reports, high-risk jurisdiction lists |
| EU AMLA | European Union | Anti-Money Laundering Authority directives, MiCA implementing measures |
| FCA | United Kingdom | Policy statements, consultation papers, supervisory notices |
| MAS | Singapore | Notices, guidelines, consultation papers |
| SFC | Hong Kong | Circulars, codes of conduct, licensing updates |
| JFSA | Japan | Cabinet orders, supervisory guidelines, no-action letters |
Detection Process
The agent performs the following steps for each feed:
- Fetch: Retrieve the latest publications since the last successful check. Each feed has a dedicated parser tuned to its publication format (RSS, HTML scraping, API, or PDF extraction).
- Classify: Determine whether each publication constitutes a rule change, guidance update, enforcement action, or informational notice.
- Extract parameters: Identify specific regulatory parameters that changed (thresholds, reporting deadlines, entity definitions, prohibited activities).
- Map impact: Correlate changed parameters to Veris configuration values, screening rules, and filing templates.
- Score urgency: Assign an urgency level based on effective date proximity and operational impact.
- Notify: Fire webhooks to all configured endpoints with the change payload.
Urgency Levels
| Level | Definition | Expected Response Time |
|---|---|---|
critical | Rule takes effect within 7 days or has retroactive application. Immediate configuration change required. | Same business day |
high | Rule takes effect within 30 days. Configuration change required before effective date. | Within 5 business days |
medium | Rule takes effect within 90 days. Planning and implementation required. | Within 30 days |
low | Guidance or interpretive update. No immediate configuration change, but policy review recommended. | Next quarterly review |
Output: Webhook Payload
When the agent detects a rule change, it fires a policy.update_required webhook with the following payload:
{
"event": "policy.update_required",
"change_id": "rc_20260324_fincen_001",
"source": "fincen",
"jurisdiction": "us",
"urgency": "high",
"publication": {
"title": "FinCEN Issues Final Rule on Convertible Virtual Currency Mixing",
"url": "https://www.fincen.gov/news/news-releases/...",
"published_at": "2026-03-23T16:00:00Z",
"document_type": "final_rule"
},
"impact": {
"summary": "New reporting requirements for transactions involving CVC mixing services. Threshold lowered from $10,000 to $1,000 for mixer-related transactions.",
"affected_parameters": [
{
"parameter": "mixer_reporting_threshold_usd",
"current_value": 10000.00,
"required_value": 1000.00,
"effective_date": "2026-04-22T00:00:00Z"
}
],
"affected_modules": [
"triage_agent.deterministic_prefilter",
"investigation_agent.typology_matching",
"sar_generation_agent.filing_templates"
]
},
"recommended_actions": [
"Update mixer reporting threshold from $10,000 to $1,000 in screening configuration.",
"Review and update SAR narrative templates for mixer-related filings.",
"Retrain triage model with updated threshold boundary."
],
"timestamp": "2026-03-24T08:00:32Z"
}
Field Reference
| Field | Type | Description |
|---|---|---|
event | string | Always policy.update_required. |
change_id | string | Unique identifier. Format: rc_YYYYMMDD_source_seq. |
source | string | Regulatory feed identifier. |
jurisdiction | string | ISO 3166-1 jurisdiction code or international for FATF. |
urgency | string | One of: critical, high, medium, low. |
publication | object | Source document metadata with URL and publication date. |
impact.summary | string | Plain-language description of the change and its operational impact. |
impact.affected_parameters | array[object] | Specific configuration values that require update, with current and required values. |
impact.affected_modules | array[string] | Veris modules affected by the change. |
recommended_actions | array[string] | Ordered list of steps to achieve compliance with the new rule. |
OFAC SDN List Updates
OFAC SDN list changes receive special handling due to their immediate legal effect. When the agent detects an SDN list update:
- The updated list is ingested and indexed within 15 minutes of publication.
- A retroactive sweep executes against all monitored addresses to identify new matches.
- New matches trigger immediate alert generation with
immediate_sarclassification. - A
policy.update_requiredwebhook fires withurgency: critical.
Feed Health Monitoring
Each feed is monitored for availability and freshness. If a feed fails to return new data for longer than its expected publication cadence, the agent fires a feed.health_warning webhook.
| Feed | Expected Cadence | Alert After |
|---|---|---|
| OFAC | Weekly (with ad-hoc updates) | 14 days |
| FinCEN | Monthly | 45 days |
| FATF | Quarterly | 120 days |
| EU AMLA | Monthly | 45 days |
| FCA | Weekly | 21 days |
| MAS | Monthly | 45 days |
| SFC | Monthly | 45 days |
| JFSA | Monthly | 45 days |
Data Retention
Regulatory change records are retained indefinitely. They form the compliance audit trail demonstrating that the institution was aware of and responded to regulatory changes in a timely manner.
Standard Authentication (API Key + JWT)
Standard authentication combines a long-lived API key for service identification with short-lived JWT tokens for session-scoped access. This method is appropriate for Tier 3 (Emerging) integrations and automated compliance pipelines where mutual TLS is not required.
API Key Format
Veris API keys follow a deterministic format that encodes environment context:
vrs_live_<32-256 alphanumeric characters>
vrs_test_<32-256 alphanumeric characters>
Keys prefixed with vrs_live_ operate against production data. Keys prefixed with vrs_test_ operate against the sandbox environment. All API requests must include the key in the X-API-Key header.
| Property | Value |
|---|---|
| Header name | X-API-Key |
| Key length | 32 to 256 characters after prefix |
| Character set | Alphanumeric (a-z, A-Z, 0-9) |
| Environment separation | vrs_live_ (production), vrs_test_ (sandbox) |
| Revocation | Immediate via API or dashboard |
API keys authenticate the calling organization. They do not expire automatically but can be rotated or revoked at any time through the Veris dashboard or the key management API.
JWT Bearer Tokens
For session-scoped operations, Veris issues JWT tokens signed with RS256. Tokens are obtained by authenticating against the login endpoint with valid credentials and a valid API key.
Token Properties
| Property | Value |
|---|---|
| Algorithm | RS256 |
| Expiry | 60 minutes |
| Issuer | api.useveris.finance |
| Header | Authorization: Bearer <token> |
Obtaining a Token
POST /api/v1/auth/login
Request body:
{
"email": "compliance-team@example.com",
"password": "your-password"
}
Required header:
X-API-Key: vrs_live_abc123...
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "vrs_rt_d8f2a1..."
}
Token Refresh
Refresh tokens before expiry to maintain uninterrupted access. The refresh endpoint issues a new access token and rotates the refresh token. Previous refresh tokens are invalidated immediately upon use.
POST /api/v1/auth/refresh
Request body:
{
"refresh_token": "vrs_rt_d8f2a1..."
}
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "vrs_rt_e9a3b2..."
}
Code Samples
curl
Authenticate and obtain a JWT:
# Step 1: Obtain JWT token
TOKEN=$(curl -s -X POST https://api.useveris.finance/api/v1/auth/login \
-H "Content-Type: application/json" \
-H "X-API-Key: vrs_live_abc123def456ghi789jkl012mno345" \
-d '{"email": "compliance-team@example.com", "password": "your-password"}' \
| jq -r '.access_token')
# Step 2: Use JWT for subsequent requests
curl -s https://api.useveris.finance/api/v1/alerts \
-H "Authorization: Bearer $TOKEN" \
-H "X-API-Key: vrs_live_abc123def456ghi789jkl012mno345"
API key only (for endpoints that do not require JWT):
curl -s https://api.useveris.finance/api/v1/health \
-H "X-API-Key: vrs_live_abc123def456ghi789jkl012mno345"
Python
import httpx
API_KEY = "vrs_live_abc123def456ghi789jkl012mno345"
BASE_URL = "https://api.useveris.finance/api/v1"
def authenticate(email: str, password: str) -> dict:
"""Obtain JWT and refresh tokens."""
response = httpx.post(
f"{BASE_URL}/auth/login",
headers={"X-API-Key": API_KEY},
json={"email": email, "password": password},
)
response.raise_for_status()
return response.json()
def refresh_token(refresh_token: str) -> dict:
"""Rotate the refresh token and obtain a new access token."""
response = httpx.post(
f"{BASE_URL}/auth/refresh",
headers={"X-API-Key": API_KEY},
json={"refresh_token": refresh_token},
)
response.raise_for_status()
return response.json()
# Usage
tokens = authenticate("compliance-team@example.com", "your-password")
access_token = tokens["access_token"]
# Authenticated request
response = httpx.get(
f"{BASE_URL}/alerts",
headers={
"Authorization": f"Bearer {access_token}",
"X-API-Key": API_KEY,
},
)
alerts = response.json()
Node.js
const API_KEY = "vrs_live_abc123def456ghi789jkl012mno345";
const BASE_URL = "https://api.useveris.finance/api/v1";
async function authenticate(email, password) {
const response = await fetch(`${BASE_URL}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
},
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error(`Authentication failed: ${response.status}`);
}
return response.json();
}
async function refreshToken(refreshToken) {
const response = await fetch(`${BASE_URL}/auth/refresh`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": API_KEY,
},
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
return response.json();
}
// Usage
const tokens = await authenticate(
"compliance-team@example.com",
"your-password"
);
const alertsResponse = await fetch(`${BASE_URL}/alerts`, {
headers: {
Authorization: `Bearer ${tokens.access_token}`,
"X-API-Key": API_KEY,
},
});
const alerts = await alertsResponse.json();
When to Use Standard Authentication
Standard authentication is appropriate for the following scenarios:
| Scenario | Recommendation |
|---|---|
| Tier 3 (Emerging) integrations | Required minimum |
| Automated compliance pipelines | Recommended with payload signatures |
| Internal tooling and dashboards | Sufficient when combined with IP whitelisting |
| Development and sandbox testing | Use vrs_test_ keys |
For Tier 2 (Enterprise) and Tier 1 (Sovereign) integrations, use mTLS authentication instead. Standard authentication can be combined with payload signatures and IP whitelisting for additional security layers.
Error Responses
| HTTP Status | Error Code | Description |
|---|---|---|
| 401 | invalid_api_key | The API key is malformed, revoked, or does not exist |
| 401 | token_expired | The JWT has expired. Obtain a new token via refresh |
| 401 | invalid_token | The JWT signature verification failed |
| 403 | insufficient_scope | The API key does not have permission for this endpoint |
| 429 | rate_limit_exceeded | Too many authentication attempts. Retry after the period specified in Retry-After |
Security Considerations
- Store API keys in a secrets manager (HashiCorp Vault, AWS Secrets Manager, or equivalent). Never embed keys in source code or client-side applications.
- Rotate API keys on a regular schedule. Veris supports multiple active keys per organization to enable zero-downtime rotation.
- Use the narrowest possible scope when generating API keys. Assign read-only keys to monitoring systems and full-access keys only to systems that require write operations.
- Monitor the API key usage dashboard for anomalous request patterns. Veris flags unusual geographic origins and request volume spikes automatically.
Enterprise Authentication (mTLS)
Mutual TLS authentication provides cryptographic verification of both client and server identity at the transport layer. Veris requires mTLS for Tier 2 (Enterprise) and Tier 1 (Sovereign) integrations. All financial-grade API operations at these tiers must authenticate over a mutually authenticated TLS channel.
Overview
In standard TLS, only the server presents a certificate. With mTLS, the client also presents a certificate that the server validates against a trusted certificate authority. This establishes bidirectional trust before any application-layer data is exchanged.
Client Veris API
| |
|--- ClientHello ------------------------->|
|<-- ServerHello + Server Certificate ------|
|<-- CertificateRequest -------------------|
|--- Client Certificate ------------------->|
|--- CertificateVerify -------------------->|
|<-- Finished ------------------------------|
|--- Application Data (encrypted) -------->|
Certificate Requirements
Client certificates must meet the following specifications:
| Property | Requirement |
|---|---|
| Format | X.509 v3 |
| Key algorithm | RSA 2048-bit minimum, or ECDSA P-256 |
| Signature algorithm | SHA-256 or stronger |
| Validity period | Maximum 1 year |
| Key usage | Digital Signature, Key Encipherment |
| Extended key usage | TLS Client Authentication (OID 1.3.6.1.5.5.7.3.2) |
| Subject DN | Must include O (Organization) matching your Veris account name |
| SAN | At least one DNS name or URI identifying the client system |
Veris does not accept self-signed certificates. Certificates must be issued by a publicly trusted CA or by your organization's private CA after you upload the CA certificate to Veris.
Configuration Steps
Step 1: Generate a Private Key and CSR
Generate a private key and certificate signing request. The CSR must include your organization name as registered in your Veris account.
RSA 2048-bit:
# Generate private key
openssl genrsa -out veris-client.key 2048
# Generate CSR
openssl req -new -key veris-client.key -out veris-client.csr \
-subj "/C=DE/ST=Berlin/L=Berlin/O=Your Organization/CN=veris-client.your-org.com"
ECDSA P-256:
# Generate private key
openssl ecparam -genkey -name prime256v1 -out veris-client.key
# Generate CSR
openssl req -new -key veris-client.key -out veris-client.csr \
-subj "/C=DE/ST=Berlin/L=Berlin/O=Your Organization/CN=veris-client.your-org.com"
Step 2: Obtain a Signed Certificate
Submit the CSR to your certificate authority. The resulting certificate must satisfy the requirements listed above. If you use a private CA, proceed to Step 3 before uploading the client certificate.
Step 3: Upload Certificates to Veris
Upload your client certificate (and optionally your CA certificate) through the Veris API:
# Upload CA certificate (required for private CAs)
curl -X POST https://api.useveris.finance/api/v1/security/ca-certificates \
-H "X-API-Key: vrs_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"name": "your-org-internal-ca",
"certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
}'
# Upload client certificate
curl -X POST https://api.useveris.finance/api/v1/security/client-certificates \
-H "X-API-Key: vrs_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"name": "production-compliance-service",
"certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"environment": "production"
}'
Step 4: Bind Certificate to API Endpoints
After uploading, bind the certificate to the mTLS-protected endpoint:
curl -X POST https://api.useveris.finance/api/v1/security/mtls-bindings \
-H "X-API-Key: vrs_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"certificate_id": "cert_a1b2c3d4",
"endpoint_pattern": "/api/v1/*",
"enforce": true
}'
Once enforce is set to true, all requests to matching endpoints that do not present a valid client certificate will be rejected with HTTP 403.
Making Authenticated Requests
curl
curl -s https://mtls.api.useveris.finance/api/v1/alerts \
--cert /path/to/veris-client.crt \
--key /path/to/veris-client.key \
--cacert /path/to/veris-ca-bundle.crt \
-H "X-API-Key: vrs_live_abc123..."
Python (httpx)
import httpx
client = httpx.Client(
base_url="https://mtls.api.useveris.finance/api/v1",
cert=("/path/to/veris-client.crt", "/path/to/veris-client.key"),
verify="/path/to/veris-ca-bundle.crt",
headers={"X-API-Key": "vrs_live_abc123..."},
)
response = client.get("/alerts")
alerts = response.json()
Node.js
const https = require("node:https");
const fs = require("node:fs");
const agent = new https.Agent({
cert: fs.readFileSync("/path/to/veris-client.crt"),
key: fs.readFileSync("/path/to/veris-client.key"),
ca: fs.readFileSync("/path/to/veris-ca-bundle.crt"),
});
const response = await fetch(
"https://mtls.api.useveris.finance/api/v1/alerts",
{
agent,
headers: {
"X-API-Key": "vrs_live_abc123...",
},
}
);
const alerts = await response.json();
When to Use mTLS
| Integration Tier | mTLS Requirement |
|---|---|
| Tier 1 (Sovereign) | Mandatory |
| Tier 2 (Enterprise) | Mandatory |
| Tier 3 (Emerging) | Optional (recommended for production) |
mTLS is required when:
- Processing transaction volumes above 100,000 per month.
- Accessing PII or SAR-related endpoints.
- Operating under regulatory frameworks that mandate transport-layer client authentication (MiCAR, BSA/AML for MSBs).
Certificate Rotation
Certificates must be rotated before expiry. Veris supports overlapping certificate validity to enable zero-downtime rotation.
Rotation Procedure
- Generate a new private key and CSR. Do not reuse the previous private key.
- Obtain a signed certificate from your CA.
- Upload the new certificate to Veris.
- Bind the new certificate to your endpoints. Both old and new certificates remain valid during the overlap window.
- Update your client systems to use the new certificate and key.
- Verify connectivity with the new certificate.
- Revoke the old certificate through the Veris API.
# Upload replacement certificate
curl -X POST https://api.useveris.finance/api/v1/security/client-certificates \
-H "X-API-Key: vrs_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"name": "production-compliance-service-v2",
"certificate_pem": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"environment": "production",
"replaces": "cert_a1b2c3d4"
}'
# After confirming the new certificate works, revoke the old one
curl -X DELETE https://api.useveris.finance/api/v1/security/client-certificates/cert_a1b2c3d4 \
-H "X-API-Key: vrs_live_abc123..."
Rotation Schedule
| Certificate Type | Recommended Rotation |
|---|---|
| Client certificates | Every 90 days |
| CA certificates | Every 2 years |
| Private keys | Every rotation (never reuse) |
Veris sends webhook notifications 30 days, 14 days, and 3 days before a client certificate expires. Configure your alerting systems to act on the security.certificate_expiring event.
Error Responses
| HTTP Status | Error Code | Description |
|---|---|---|
| 403 | mtls_required | The endpoint requires mTLS but no client certificate was presented |
| 403 | certificate_rejected | The client certificate is not trusted, expired, or revoked |
| 403 | certificate_mismatch | The certificate subject does not match the registered organization |
| 403 | certificate_expired | The client certificate has passed its validity period |
Security Considerations
- Store private keys in hardware security modules (HSMs) or equivalent tamper-resistant storage. Never store private keys on disk in production systems without filesystem encryption.
- Use separate certificates for each environment (sandbox, staging, production). Cross-environment certificate use is rejected by default.
- Implement automated certificate rotation with monitoring. Manual rotation processes are a reliability risk at scale.
- Combine mTLS with IP whitelisting for defense-in-depth. Transport-layer authentication and network-layer restrictions together satisfy the strictest regulatory requirements.
Payload Signatures
Payload signatures provide request integrity verification independent of transport-layer security. Every mutating API request (POST, PUT, PATCH, DELETE) must include an HMAC-SHA256 signature computed over the request timestamp and body. This prevents tampering, ensures non-repudiation, and blocks replay attacks.
Signature Scheme
Veris uses HMAC-SHA256 with a shared signing secret. The signing secret is issued alongside your API key and is distinct from the API key itself. The signing secret never leaves your infrastructure and is never transmitted in API requests.
| Component | Value |
|---|---|
| Algorithm | HMAC-SHA256 |
| Signature header | X-Veris-Signature |
| Timestamp header | X-Veris-Timestamp |
| Nonce header | X-Veris-Nonce |
| Timestamp tolerance | 30 seconds |
| Signing secret format | vrs_sig_<64 hex characters> |
Signing Algorithm
The signature is computed as follows:
signature = HMAC-SHA256(signing_secret, timestamp + "." + request_body)
Where:
signing_secretis your Veris signing secret.timestampis the current Unix epoch in seconds, as a string.request_bodyis the raw request body as a UTF-8 string. For requests with no body, use an empty string.- The timestamp and body are concatenated with a literal period (
.) separator.
The resulting signature is hex-encoded and placed in the X-Veris-Signature header.
Required Headers
Every signed request must include all three headers:
| Header | Format | Description |
|---|---|---|
X-Veris-Signature | 64-character hex string | HMAC-SHA256 signature |
X-Veris-Timestamp | Unix epoch (seconds) | Request creation time |
X-Veris-Nonce | UUID v4 or 32+ character unique string | One-time request identifier |
Timestamp Validation
Veris rejects requests where the timestamp differs from server time by more than 30 seconds. This window accounts for reasonable clock drift while preventing replay attacks with captured requests.
Ensure your client systems synchronize time via NTP. A clock skew beyond 30 seconds will cause all signed requests to fail.
Replay Attack Prevention
The X-Veris-Nonce header provides replay protection. Veris stores each nonce for 30 seconds. Any request that reuses a previously seen nonce within this window is rejected with HTTP 409.
Nonce values must be unique per request. UUID v4 is the recommended format. Sequential or predictable nonce values are accepted but provide weaker security guarantees.
Code Samples
Python
import hashlib
import hmac
import time
import uuid
import httpx
API_KEY = "vrs_live_abc123..."
SIGNING_SECRET = "vrs_sig_a1b2c3d4e5f6..."
BASE_URL = "https://api.useveris.finance/api/v1"
def sign_request(body: str) -> dict:
"""Generate signature headers for a request body."""
timestamp = str(int(time.time()))
nonce = str(uuid.uuid4())
message = f"{timestamp}.{body}"
signature = hmac.new(
SIGNING_SECRET.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return {
"X-Veris-Signature": signature,
"X-Veris-Timestamp": timestamp,
"X-Veris-Nonce": nonce,
}
# Example: Create a screening request
body = '{"address": "0x1234...", "chain": "ethereum"}'
signature_headers = sign_request(body)
response = httpx.post(
f"{BASE_URL}/screening",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json",
**signature_headers,
},
content=body,
)
Node.js
const crypto = require("node:crypto");
const API_KEY = "vrs_live_abc123...";
const SIGNING_SECRET = "vrs_sig_a1b2c3d4e5f6...";
const BASE_URL = "https://api.useveris.finance/api/v1";
function signRequest(body) {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomUUID();
const message = `${timestamp}.${body}`;
const signature = crypto
.createHmac("sha256", SIGNING_SECRET)
.update(message)
.digest("hex");
return {
"X-Veris-Signature": signature,
"X-Veris-Timestamp": timestamp,
"X-Veris-Nonce": nonce,
};
}
// Example: Create a screening request
const body = JSON.stringify({ address: "0x1234...", chain: "ethereum" });
const signatureHeaders = signRequest(body);
const response = await fetch(`${BASE_URL}/screening`, {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
...signatureHeaders,
},
body,
});
Verification (Server-Side Reference)
The following demonstrates how Veris verifies incoming signatures. Use this logic if you implement signature verification for Veris webhooks delivered to your systems.
Python
import hashlib
import hmac
import time
def verify_signature(
signing_secret: str,
signature: str,
timestamp: str,
body: str,
tolerance_seconds: int = 30,
) -> bool:
"""Verify an HMAC-SHA256 payload signature.
Returns True if the signature is valid and the timestamp
is within the tolerance window.
"""
# Check timestamp freshness
request_time = int(timestamp)
current_time = int(time.time())
if abs(current_time - request_time) > tolerance_seconds:
return False
# Compute expected signature
message = f"{timestamp}.{body}"
expected = hmac.new(
signing_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected, signature)
Node.js
const crypto = require("node:crypto");
function verifySignature(signingSecret, signature, timestamp, body, toleranceSeconds = 30) {
// Check timestamp freshness
const requestTime = parseInt(timestamp, 10);
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - requestTime) > toleranceSeconds) {
return false;
}
// Compute expected signature
const message = `${timestamp}.${body}`;
const expected = crypto
.createHmac("sha256", signingSecret)
.update(message)
.digest("hex");
// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}
Error Responses
| HTTP Status | Error Code | Description |
|---|---|---|
| 401 | signature_missing | The X-Veris-Signature header is absent |
| 401 | signature_invalid | The computed signature does not match the provided signature |
| 401 | timestamp_missing | The X-Veris-Timestamp header is absent |
| 401 | timestamp_expired | The timestamp is outside the 30-second tolerance window |
| 409 | nonce_reused | The X-Veris-Nonce value was already used within the replay window |
Security Considerations
- Store signing secrets with the same rigor as private keys. Use a secrets manager. Never log signing secrets or include them in error messages.
- Always use constant-time comparison functions (
hmac.compare_digestin Python,crypto.timingSafeEqualin Node.js) when verifying signatures. Standard string equality allows timing side-channel attacks. - Compute signatures over the raw request body, not a re-serialized version. JSON serialization is not deterministic across libraries. Changing key order or whitespace will invalidate the signature.
- Combine payload signatures with mTLS for Tier 1 and Tier 2 integrations. Payload signatures protect against application-layer tampering. mTLS protects against transport-layer attacks. Neither substitutes for the other.
IP Whitelisting
IP whitelisting restricts API access to requests originating from pre-approved network addresses. Veris evaluates the source IP of every inbound request against your organization's whitelist before processing any authentication or application logic. Requests from non-whitelisted addresses are rejected at the network edge.
Configuration
Adding IP Addresses
Register allowed IP addresses or CIDR ranges through the Veris API. Each entry requires a descriptive label for audit trail purposes.
POST /api/v1/security/ip-whitelist
Request body:
{
"entries": [
{
"cidr": "203.0.113.0/24",
"label": "Production datacenter - Frankfurt",
"environment": "production"
},
{
"cidr": "198.51.100.42/32",
"label": "CI/CD runner - GitHub Actions",
"environment": "production"
},
{
"cidr": "10.0.0.0/8",
"label": "Internal VPN",
"environment": "sandbox"
}
]
}
Response:
{
"entries": [
{
"id": "ipw_a1b2c3d4",
"cidr": "203.0.113.0/24",
"label": "Production datacenter - Frankfurt",
"environment": "production",
"created_at": "2026-03-24T10:00:00Z",
"status": "active"
}
],
"total_active": 3
}
CIDR Notation
Veris accepts both individual addresses and CIDR ranges:
| Format | Example | Description |
|---|---|---|
| Single IPv4 | 198.51.100.42/32 | One specific address |
| IPv4 subnet | 203.0.113.0/24 | 256 addresses (203.0.113.0 through 203.0.113.255) |
| Single IPv6 | 2001:db8::1/128 | One specific address |
| IPv6 subnet | 2001:db8::/48 | IPv6 prefix block |
The minimum prefix length is /16 for IPv4 and /48 for IPv6. Broader ranges are rejected to prevent accidental over-permissioning.
Listing Whitelisted Entries
GET /api/v1/security/ip-whitelist
Response:
{
"entries": [
{
"id": "ipw_a1b2c3d4",
"cidr": "203.0.113.0/24",
"label": "Production datacenter - Frankfurt",
"environment": "production",
"created_at": "2026-03-24T10:00:00Z",
"status": "active",
"last_seen": "2026-03-24T14:32:17Z"
}
],
"total_active": 3
}
The last_seen field records the most recent request from an address within the CIDR range. Use this to identify stale entries during periodic reviews.
Removing Entries
DELETE /api/v1/security/ip-whitelist/{entry_id}
curl -X DELETE https://api.useveris.finance/api/v1/security/ip-whitelist/ipw_a1b2c3d4 \
-H "X-API-Key: vrs_live_abc123..."
Removal takes effect within 60 seconds. During propagation, requests from the removed range may still be accepted. Plan removal operations accordingly.
Enforcement Behavior
When IP whitelisting is active for your organization:
| Condition | HTTP Status | Error Code | Behavior |
|---|---|---|---|
| Source IP matches a whitelist entry | N/A | N/A | Request proceeds to authentication |
| Source IP does not match any entry | 403 | ip_not_whitelisted | Request rejected before authentication |
| Whitelist is empty (no entries) | N/A | N/A | All IPs accepted (whitelisting disabled) |
The rejection response includes the source IP observed by Veris, which is useful for diagnosing NAT or proxy misconfigurations:
{
"error": {
"code": "ip_not_whitelisted",
"message": "Request origin 192.0.2.50 is not in the allowed IP list.",
"source_ip": "192.0.2.50"
}
}
Environment Separation
Whitelist entries are scoped to environments. A production whitelist entry does not grant access to the sandbox API, and vice versa. Configure entries for each environment independently.
| Environment | API Base URL | Whitelist Scope |
|---|---|---|
| Production | api.useveris.finance | production entries only |
| Sandbox | sandbox.api.useveris.finance | sandbox entries only |
Combining with mTLS
For Tier 1 (Sovereign) and Tier 2 (Enterprise) integrations, combine IP whitelisting with mTLS authentication to establish defense-in-depth:
- Network layer: IP whitelisting restricts which networks can reach the API.
- Transport layer: mTLS verifies the cryptographic identity of the client.
- Application layer: API key and payload signatures authenticate the request and verify integrity.
This layered approach satisfies regulatory expectations under MiCAR Article 45 (operational resilience) and aligns with NIST 800-53 SC-7 (boundary protection).
Request Flow:
[Client] --> [IP Whitelist Check] --> [mTLS Handshake] --> [API Key Auth] --> [Signature Verify] --> [Application]
Network Edge TLS Termination App Layer App Layer Business Logic
Operational Notes
Proxy and Load Balancer Considerations
If your requests pass through a proxy, CDN, or load balancer, the source IP seen by Veris is the address of the last hop, not your origin server. Whitelist the egress IP of your proxy infrastructure. Veris respects X-Forwarded-For headers only from trusted proxy networks configured during onboarding.
Dynamic IP Addresses
Cloud environments with dynamic IP allocation (autoscaling groups, serverless functions) require a stable egress point. Route API traffic through a NAT gateway or static IP proxy to maintain a predictable source address.
Audit and Compliance
All whitelist modifications are recorded in the Veris audit log with the following details:
- Timestamp of the change.
- Identity of the user or API key that performed the action.
- Previous and new state of the entry.
- Source IP of the administrative request.
Retrieve audit records via the /api/v1/audit/ip-whitelist endpoint for SOC 2 and regulatory evidence collection.
Error Responses
| HTTP Status | Error Code | Description |
|---|---|---|
| 403 | ip_not_whitelisted | The request source IP is not in the allowed list |
| 400 | invalid_cidr | The CIDR notation is malformed |
| 400 | cidr_too_broad | The prefix length is below the minimum (/16 IPv4, /48 IPv6) |
| 404 | whitelist_entry_not_found | The specified entry ID does not exist |
| 409 | duplicate_entry | A whitelist entry with the same CIDR already exists |
Event Catalog
Veris delivers structured webhook events to your registered endpoints when compliance-relevant state changes occur. Each event carries a typed payload with full context, enabling downstream automation without polling.
This catalog documents every event type, its trigger condition, and payload structure.
Event Format
All webhook events share a common envelope:
{
"id": "evt_a1b2c3d4e5f6",
"type": "alert.resolved",
"api_version": "2026-03-01",
"created_at": "2026-03-24T14:32:17.000Z",
"organization_id": "org_x1y2z3",
"data": { }
}
| Field | Type | Description |
|---|---|---|
id | string | Unique event identifier. Use for idempotency. |
type | string | Event type from this catalog |
api_version | string | API version that generated the event |
created_at | string (ISO 8601) | Timestamp of event creation |
organization_id | string | Your organization identifier |
data | object | Event-specific payload (documented below) |
Resolution Events
Events triggered by alert lifecycle transitions and compliance workflow completions.
alert.resolved
Fired when an alert reaches a terminal state (closed, dismissed, or escalated to a case).
Trigger: Alert status changes to resolved, dismissed, or escalated.
{
"id": "evt_r001",
"type": "alert.resolved",
"api_version": "2026-03-01",
"created_at": "2026-03-24T14:32:17.000Z",
"organization_id": "org_x1y2z3",
"data": {
"alert_id": "alt_5f8a2b",
"resolution": "escalated",
"case_id": "cas_9d4e1c",
"risk_score": 87,
"address": "0x1a2b3c...",
"chain": "ethereum",
"resolved_by": "ai_triage",
"resolved_at": "2026-03-24T14:32:15.000Z"
}
}
investigation.complete
Fired when an AI investigation finishes processing a case.
Trigger: Investigation agent completes analysis and writes results to the case record.
{
"id": "evt_r002",
"type": "investigation.complete",
"api_version": "2026-03-01",
"created_at": "2026-03-24T14:33:42.000Z",
"organization_id": "org_x1y2z3",
"data": {
"case_id": "cas_9d4e1c",
"alert_id": "alt_5f8a2b",
"recommendation": "file_sar",
"confidence": 0.94,
"risk_indicators": ["layering", "rapid_consolidation", "mixer_interaction"],
"investigation_duration_seconds": 18,
"completed_at": "2026-03-24T14:33:40.000Z"
}
}
sar.ready
Fired when a SAR narrative has been generated and is awaiting human review.
Trigger: SAR agent completes narrative generation for a case.
{
"id": "evt_r003",
"type": "sar.ready",
"api_version": "2026-03-01",
"created_at": "2026-03-24T15:01:22.000Z",
"organization_id": "org_x1y2z3",
"data": {
"case_id": "cas_9d4e1c",
"sar_id": "sar_7b3f2a",
"filing_type": "initial",
"subject_count": 2,
"transaction_count": 47,
"total_amount_usd": 284500.00,
"generated_at": "2026-03-24T15:01:20.000Z"
}
}
sar.filed
Fired when a SAR has been filed with the regulatory authority (after human approval).
Trigger: SAR status transitions to filed following compliance officer approval and submission.
{
"id": "evt_r004",
"type": "sar.filed",
"api_version": "2026-03-01",
"created_at": "2026-03-24T16:45:00.000Z",
"organization_id": "org_x1y2z3",
"data": {
"case_id": "cas_9d4e1c",
"sar_id": "sar_7b3f2a",
"filing_type": "initial",
"bsa_id": "31000012345678",
"filed_at": "2026-03-24T16:44:58.000Z",
"filing_deadline": "2026-04-23T00:00:00.000Z"
}
}
Risk Events
Events triggered by changes in address or entity risk assessments. These events support continuous KYT (Know Your Transaction) monitoring.
risk.score_changed
Continuous monitoring. When an address risk score changes after initial screening, Veris delivers a risk.score_changed event to your webhook endpoint. This enables real-time response to evolving risk without re-screening.
Trigger: An address risk score changes by more than 5 points, or crosses a tier boundary (low/medium/high/critical).
{
"id": "evt_k001",
"type": "risk.score_changed",
"api_version": "2026-03-01",
"created_at": "2026-03-24T14:50:00.000Z",
"organization_id": "org_x1y2z3",
"data": {
"address": "0x1a2b3c...",
"chain": "ethereum",
"previous_score": 32,
"current_score": 78,
"previous_tier": "low",
"current_tier": "high",
"contributing_factors": [
{
"factor": "mixer_interaction",
"weight": 0.35,
"detail": "Received 12.5 ETH from Tornado Cash pool in 3 transactions"
},
{
"factor": "rapid_movement",
"weight": 0.20,
"detail": "Funds moved through 4 intermediaries within 45 minutes"
}
],
"monitored_since": "2026-01-15T09:00:00.000Z",
"evaluated_at": "2026-03-24T14:49:58.000Z"
}
}
sanctions.match_detected
Fired when a monitored address is linked to a newly listed sanctions target. This event requires immediate action under OFAC, EU, and UN sanctions frameworks.
Trigger: An address in your monitoring set matches a new or updated entry on OFAC SDN, EU Consolidated List, or UN Security Council sanctions lists.
{
"id": "evt_k002",
"type": "sanctions.match_detected",
"api_version": "2026-03-01",
"created_at": "2026-03-24T08:15:00.000Z",
"organization_id": "org_x1y2z3",
"data": {
"address": "0x4d5e6f...",
"chain": "ethereum",
"match_type": "exact",
"match_confidence": 1.0,
"sanctions_list": "OFAC_SDN",
"list_entry_id": "SDN-36892",
"listed_entity": "Example Sanctioned Entity LLC",
"listing_date": "2026-03-24",
"source_url": "https://sanctionssearch.ofac.treas.gov/Details.aspx?id=36892",
"recommended_action": "freeze_and_report",
"detected_at": "2026-03-24T08:14:55.000Z"
}
}
Regulatory Events
Events related to regulatory framework changes and sanctions list updates that may require policy adjustments.
policy.update_required
Fired when Veris detects a regulatory change that affects your configured compliance policies.
Trigger: A regulatory framework update (MiCAR, BSA/AML, Travel Rule) requires changes to your screening thresholds, reporting timelines, or risk parameters.
{
"id": "evt_g001",
"type": "policy.update_required",
"api_version": "2026-03-01",
"created_at": "2026-03-24T06:00:00.000Z",
"organization_id": "org_x1y2z3",
"data": {
"regulation": "MiCAR",
"article": "Article 76(3)",
"jurisdiction": "EU",
"effective_date": "2026-06-30",
"summary": "Travel Rule threshold reduced from EUR 1000 to EUR 0 for crypto-asset transfers.",
"affected_policies": ["travel_rule_threshold", "screening_minimum_amount"],
"recommended_changes": [
{
"policy": "travel_rule_threshold",
"current_value": "1000.00",
"recommended_value": "0.00",
"currency": "EUR"
}
],
"documentation_url": "https://docs.useveris.finance/regulatory/micar-2026-06"
}
}
sanctions.list_updated
Fired when Veris ingests an updated sanctions list. This event is informational. If any of your monitored addresses match new entries, separate sanctions.match_detected events are delivered.
Trigger: Veris completes ingestion of an updated sanctions list from any supported source.
{
"id": "evt_g002",
"type": "sanctions.list_updated",
"api_version": "2026-03-01",
"created_at": "2026-03-24T04:30:00.000Z",
"organization_id": "org_x1y2z3",
"data": {
"list_name": "OFAC_SDN",
"list_version": "2026-03-24",
"entries_added": 12,
"entries_removed": 3,
"entries_modified": 7,
"total_entries": 14892,
"crypto_addresses_added": 4,
"ingested_at": "2026-03-24T04:29:45.000Z"
}
}
System Events
Events related to platform operations, testing, and generated reports.
webhook.test
A test event for verifying webhook endpoint connectivity and signature validation. Carries no compliance data.
Trigger: Manual invocation via the API or the Veris dashboard.
{
"id": "evt_s001",
"type": "webhook.test",
"api_version": "2026-03-01",
"created_at": "2026-03-24T10:00:00.000Z",
"organization_id": "org_x1y2z3",
"data": {
"message": "Webhook endpoint verified successfully."
}
}
report.generated
Fired when a scheduled or on-demand compliance report has been generated and is available for download.
Trigger: Report generation completes (transaction monitoring summary, risk assessment report, regulatory filing summary).
{
"id": "evt_s002",
"type": "report.generated",
"api_version": "2026-03-01",
"created_at": "2026-03-24T02:00:00.000Z",
"organization_id": "org_x1y2z3",
"data": {
"report_id": "rpt_8c2d4f",
"report_type": "transaction_monitoring_summary",
"period_start": "2026-03-01T00:00:00.000Z",
"period_end": "2026-03-31T23:59:59.000Z",
"format": "pdf",
"size_bytes": 2847561,
"download_url": "https://api.useveris.finance/api/v1/reports/rpt_8c2d4f/download",
"download_expires_at": "2026-03-31T02:00:00.000Z",
"generated_at": "2026-03-24T01:59:48.000Z"
}
}
reserve.attestation
Fired when a stablecoin reserve attestation is published or when reserve composition changes materially.
Trigger: New reserve attestation data is recorded, or reserve ratio deviates by more than 0.5% from the previous attestation.
{
"id": "evt_s003",
"type": "reserve.attestation",
"api_version": "2026-03-01",
"created_at": "2026-03-24T12:00:00.000Z",
"organization_id": "org_x1y2z3",
"data": {
"issuer": "USDC",
"attestation_date": "2026-03-24",
"total_supply": 52400000000.00,
"total_reserves": 52480000000.00,
"reserve_ratio": 1.0015,
"reserve_composition": {
"us_treasuries": 0.78,
"cash_deposits": 0.15,
"reverse_repo": 0.07
},
"auditor": "Deloitte",
"attestation_url": "https://transparency.centre.io/2026-03-24",
"recorded_at": "2026-03-24T11:59:50.000Z"
}
}
Event Summary Table
| Event | Category | Trigger |
|---|---|---|
alert.resolved | Resolution | Alert reaches terminal state |
investigation.complete | Resolution | AI investigation finishes |
sar.ready | Resolution | SAR narrative generated |
sar.filed | Resolution | SAR submitted to regulator |
risk.score_changed | Risk (KYT) | Address risk score changes materially |
sanctions.match_detected | Risk (KYT) | Monitored address matches sanctions entry |
policy.update_required | Regulatory | Regulation change affects your policies |
sanctions.list_updated | Regulatory | Sanctions list ingestion completed |
webhook.test | System | Manual connectivity test |
report.generated | System | Compliance report ready for download |
reserve.attestation | System | Reserve attestation published |
Subscribing to Events
Configure which events your webhook endpoint receives through the Webhook Configuration API. You can subscribe to individual event types, entire categories, or all events using the wildcard *.
Webhook Configuration
Veris delivers real-time event notifications to HTTPS endpoints you control. This document covers endpoint registration, signature verification, retry behavior, and failure handling.
Registering a Webhook Endpoint
Register an endpoint to receive events from the Event Catalog.
POST /api/v1/webhooks/endpoints
Request body:
{
"url": "https://compliance.your-org.com/webhooks/veris",
"description": "Production compliance event handler",
"events": [
"alert.resolved",
"investigation.complete",
"sar.ready",
"sar.filed",
"risk.score_changed",
"sanctions.match_detected"
],
"environment": "production",
"enabled": true
}
Response:
{
"id": "wh_a1b2c3d4",
"url": "https://compliance.your-org.com/webhooks/veris",
"description": "Production compliance event handler",
"events": [
"alert.resolved",
"investigation.complete",
"sar.ready",
"sar.filed",
"risk.score_changed",
"sanctions.match_detected"
],
"environment": "production",
"enabled": true,
"signing_secret": "whsec_k9m2n4p6q8r0s2t4u6v8w0x2y4z6a8b0",
"created_at": "2026-03-24T10:00:00.000Z"
}
The signing_secret is returned only once, at creation time. Store it securely. If lost, rotate the secret through the endpoint update API.
Event Subscriptions
Subscribe to specific event types, categories, or all events:
| Pattern | Description |
|---|---|
alert.resolved | Single event type |
risk.* | All events in the Risk category |
* | All events |
Managing Endpoints
List endpoints:
GET /api/v1/webhooks/endpoints
Update an endpoint:
PATCH /api/v1/webhooks/endpoints/{endpoint_id}
{
"events": ["alert.resolved", "risk.*"],
"enabled": false
}
Delete an endpoint:
DELETE /api/v1/webhooks/endpoints/{endpoint_id}
Rotate signing secret:
POST /api/v1/webhooks/endpoints/{endpoint_id}/rotate-secret
This invalidates the previous secret immediately. Update your verification logic before calling this endpoint.
Delivery Headers
Every webhook delivery includes the following headers:
| Header | Description |
|---|---|
X-Veris-Webhook-ID | Unique delivery identifier (for idempotency and support reference) |
X-Veris-Webhook-Timestamp | Unix epoch (seconds) when the delivery was initiated |
X-Veris-Webhook-Signature | HMAC-SHA256 signature of the delivery payload |
Content-Type | application/json |
User-Agent | Veris-Webhooks/1.0 |
Signature Verification
Veris signs every webhook delivery using HMAC-SHA256. Verify the signature before processing any event data to confirm the delivery originates from Veris and has not been modified in transit.
Signature Algorithm
The signature is computed as:
signature = HMAC-SHA256(signing_secret, webhook_id + "." + timestamp + "." + body)
Where:
signing_secretis the secret returned when the endpoint was created.webhook_idis the value ofX-Veris-Webhook-ID.timestampis the value ofX-Veris-Webhook-Timestamp.bodyis the raw HTTP request body.- Components are concatenated with literal period (
.) separators.
Verification: Python
import hashlib
import hmac
import time
def verify_webhook(
signing_secret: str,
webhook_id: str,
timestamp: str,
body: str,
signature: str,
tolerance_seconds: int = 300,
) -> bool:
"""Verify a Veris webhook signature.
Args:
signing_secret: The endpoint signing secret (whsec_...).
webhook_id: Value of X-Veris-Webhook-ID header.
timestamp: Value of X-Veris-Webhook-Timestamp header.
body: Raw request body as a string.
signature: Value of X-Veris-Webhook-Signature header.
tolerance_seconds: Maximum allowed age of the delivery.
Returns:
True if the signature is valid and the timestamp is fresh.
"""
# Reject stale deliveries
delivery_time = int(timestamp)
current_time = int(time.time())
if abs(current_time - delivery_time) > tolerance_seconds:
return False
# Compute expected signature
message = f"{webhook_id}.{timestamp}.{body}"
expected = hmac.new(
signing_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected, signature)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_k9m2n4p6q8r0s2t4u6v8w0x2y4z6a8b0"
@app.route("/webhooks/veris", methods=["POST"])
def handle_webhook():
is_valid = verify_webhook(
signing_secret=WEBHOOK_SECRET,
webhook_id=request.headers.get("X-Veris-Webhook-ID", ""),
timestamp=request.headers.get("X-Veris-Webhook-Timestamp", ""),
body=request.get_data(as_text=True),
signature=request.headers.get("X-Veris-Webhook-Signature", ""),
)
if not is_valid:
abort(401)
event = request.get_json()
if event["type"] == "sanctions.match_detected":
# Immediate action required
handle_sanctions_match(event["data"])
elif event["type"] == "risk.score_changed":
handle_risk_change(event["data"])
return "", 200
Verification: Node.js
const crypto = require("node:crypto");
const WEBHOOK_SECRET = "whsec_k9m2n4p6q8r0s2t4u6v8w0x2y4z6a8b0";
function verifyWebhook(signingSecret, webhookId, timestamp, body, signature, toleranceSeconds = 300) {
// Reject stale deliveries
const deliveryTime = parseInt(timestamp, 10);
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - deliveryTime) > toleranceSeconds) {
return false;
}
// Compute expected signature
const message = `${webhookId}.${timestamp}.${body}`;
const expected = crypto
.createHmac("sha256", signingSecret)
.update(message)
.digest("hex");
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}
// Express example
const express = require("express");
const app = express();
app.post(
"/webhooks/veris",
express.raw({ type: "application/json" }),
(req, res) => {
const isValid = verifyWebhook(
WEBHOOK_SECRET,
req.headers["x-veris-webhook-id"],
req.headers["x-veris-webhook-timestamp"],
req.body.toString("utf-8"),
req.headers["x-veris-webhook-signature"]
);
if (!isValid) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
switch (event.type) {
case "sanctions.match_detected":
handleSanctionsMatch(event.data);
break;
case "risk.score_changed":
handleRiskChange(event.data);
break;
}
res.status(200).send();
}
);
Retry Policy
When your endpoint returns a non-2xx HTTP status or fails to respond within 30 seconds, Veris retries the delivery with exponential backoff.
| Attempt | Delay After Previous Attempt | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 1 minute | 1 minute |
| 3 | 10 minutes | 11 minutes |
| 4 | 1 hour | 1 hour 11 minutes |
| 5 | 6 hours | 7 hours 11 minutes |
After 5 failed attempts (spanning approximately 7 hours), the delivery is marked as failed. The total retry window does not exceed 24 hours. Failed deliveries are retained for 30 days and can be replayed manually through the API.
Idempotency
Your endpoint may receive the same event more than once due to retries or network conditions. Use the X-Veris-Webhook-ID header as an idempotency key. Store processed delivery IDs and skip duplicates.
Expected Response
Veris considers a delivery successful when your endpoint returns any 2xx HTTP status code within 30 seconds. The response body is ignored. Return 200 OK with an empty body for the fastest acknowledgment.
Testing Webhooks
Send a test event to verify your endpoint configuration and signature validation.
POST /api/v1/webhooks/endpoints/{endpoint_id}/test
Response:
{
"delivery_id": "dlv_t1u2v3w4",
"event_type": "webhook.test",
"status": "delivered",
"response_status": 200,
"response_time_ms": 142,
"delivered_at": "2026-03-24T10:05:00.000Z"
}
If the test delivery fails, the response includes diagnostic information:
{
"delivery_id": "dlv_t1u2v3w4",
"event_type": "webhook.test",
"status": "failed",
"error": "Connection refused",
"response_status": null,
"response_time_ms": null,
"delivered_at": null
}
Failure Handling and Alerting
Automatic Disabling
If an endpoint fails to accept deliveries for 7 consecutive days, Veris automatically disables the endpoint and sends a notification to the organization's administrative contacts. Re-enable the endpoint after resolving the underlying issue:
PATCH /api/v1/webhooks/endpoints/{endpoint_id}
{
"enabled": true
}
Veris replays all missed events from the retention window (30 days) upon re-enablement if you request a backfill.
Delivery Logs
Inspect recent deliveries for an endpoint:
GET /api/v1/webhooks/endpoints/{endpoint_id}/deliveries
Response:
{
"deliveries": [
{
"id": "dlv_a1b2c3d4",
"event_type": "risk.score_changed",
"event_id": "evt_k001",
"status": "delivered",
"attempts": 1,
"response_status": 200,
"response_time_ms": 87,
"first_attempted_at": "2026-03-24T14:50:01.000Z",
"delivered_at": "2026-03-24T14:50:01.000Z"
},
{
"id": "dlv_e5f6g7h8",
"event_type": "alert.resolved",
"event_id": "evt_r001",
"status": "failed",
"attempts": 5,
"response_status": 503,
"response_time_ms": 30000,
"first_attempted_at": "2026-03-24T10:00:00.000Z",
"last_attempted_at": "2026-03-24T17:11:00.000Z",
"next_attempt_at": null,
"error": "Service Unavailable"
}
],
"pagination": {
"total": 156,
"page": 1,
"per_page": 20
}
}
Manual Replay
Replay a specific failed delivery:
POST /api/v1/webhooks/deliveries/{delivery_id}/replay
Replay a time range of events:
POST /api/v1/webhooks/endpoints/{endpoint_id}/replay
{
"from": "2026-03-24T00:00:00.000Z",
"to": "2026-03-24T23:59:59.000Z",
"event_types": ["risk.score_changed", "sanctions.match_detected"]
}
Endpoint Requirements
Your webhook endpoint must satisfy the following:
| Requirement | Detail |
|---|---|
| Protocol | HTTPS only. HTTP endpoints are rejected. |
| TLS version | TLS 1.2 or 1.3 |
| Certificate | Valid certificate from a publicly trusted CA |
| Response time | Under 30 seconds |
| Response code | 2xx for successful receipt |
| Availability | 99.9% uptime recommended |
| Idempotency | Handle duplicate deliveries gracefully |
Error Responses
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | invalid_url | The webhook URL is not a valid HTTPS endpoint |
| 400 | invalid_events | One or more event types are not recognized |
| 404 | endpoint_not_found | The specified endpoint ID does not exist |
| 409 | duplicate_url | An endpoint with this URL already exists for this environment |
| 422 | endpoint_disabled | The endpoint is disabled and must be re-enabled before testing |
Idempotency
Veris supports idempotent requests to prevent duplicate processing of compliance-critical operations. Any mutating endpoint can accept an Idempotency-Key header. Endpoints that create regulatory filings or resolve entities require it.
How It Works
- The client generates a UUID v4 and sends it in the
Idempotency-Keyheader. - Veris stores the request fingerprint (method, path, body hash) alongside the key.
- If a request arrives with a previously seen key, Veris returns the cached response without re-executing the operation.
- Keys are retained for 24 hours, then automatically purged.
Required Endpoints
The following endpoints reject requests that omit the Idempotency-Key header with a 400 Bad Request response.
| Endpoint | Reason |
|---|---|
POST /v1/resolve | Entity resolution must not produce duplicate records for the same input. |
POST /v1/regulatory/report | SAR and STR filings must never be submitted twice for the same case. |
POST /v1/cases | Case creation from the same alert set must be deduplicated. |
POST /v1/screening/batch | Batch screening jobs must not be duplicated on retry. |
All other mutating endpoints (POST, PUT, PATCH) accept the header optionally. GET, DELETE, and HEAD requests ignore the header.
Request Format
POST /v1/regulatory/report HTTP/1.1
Host: api.useveris.finance
Authorization: Bearer vrs_live_...
Content-Type: application/json
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{
"case_id": "case_abc123",
"jurisdiction": "us",
"filing_type": "initial"
}
Behavior on Duplicate Request
When a duplicate key is received, Veris checks three conditions:
| Condition | Behavior |
|---|---|
| Same key, same request body | Return the cached response with X-Idempotent-Replayed: true header. HTTP status matches the original response. |
| Same key, different request body | Return 409 Conflict. The key has already been used with a different payload. |
| Same key, original request still processing | Return 409 Conflict with retry_after field. The original request has not yet completed. |
Response Headers
| Header | Value | Present When |
|---|---|---|
X-Idempotent-Replayed | true | Response is served from cache (duplicate key with matching body). |
X-Idempotency-Key | The submitted UUID | Every response to a request that included the header. |
Key Retention
| Parameter | Value |
|---|---|
| Retention period | 24 hours from first use |
| Storage | Redis with TTL |
| Key format | UUID v4 (RFC 4122). Non-UUID values are rejected with 400 Bad Request. |
Code Examples
Python
import uuid
import requests
idempotency_key = str(uuid.uuid4())
response = requests.post(
"https://api.useveris.finance/v1/regulatory/report",
headers={
"Authorization": "Bearer vrs_live_...",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json={
"case_id": "case_abc123",
"jurisdiction": "us",
"filing_type": "initial",
},
)
# Safe to retry with the same key on network failure
if response.status_code in (502, 503, 504):
response = requests.post(
"https://api.useveris.finance/v1/regulatory/report",
headers={
"Authorization": "Bearer vrs_live_...",
"Content-Type": "application/json",
"Idempotency-Key": idempotency_key,
},
json={
"case_id": "case_abc123",
"jurisdiction": "us",
"filing_type": "initial",
},
)
cURL
curl -X POST https://api.useveris.finance/v1/regulatory/report \
-H "Authorization: Bearer vrs_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"case_id": "case_abc123",
"jurisdiction": "us",
"filing_type": "initial"
}'
TypeScript
const idempotencyKey = crypto.randomUUID();
const response = await fetch(
"https://api.useveris.finance/v1/regulatory/report",
{
method: "POST",
headers: {
Authorization: "Bearer vrs_live_...",
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify({
case_id: "case_abc123",
jurisdiction: "us",
filing_type: "initial",
}),
}
);
Error Responses
Missing Required Key
{
"error": {
"code": "IDEMPOTENCY_KEY_REQUIRED",
"message": "This endpoint requires an Idempotency-Key header. Provide a UUID v4 value.",
"request_id": "req_xyz789"
}
}
Key Conflict
{
"error": {
"code": "IDEMPOTENCY_KEY_CONFLICT",
"message": "This idempotency key has already been used with a different request body.",
"request_id": "req_xyz790"
}
}
Invalid Key Format
{
"error": {
"code": "IDEMPOTENCY_KEY_INVALID",
"message": "Idempotency-Key must be a valid UUID v4.",
"request_id": "req_xyz791"
}
}
Rate Limits
Veris enforces rate limits to ensure platform stability and fair resource allocation across all clients. Limits are applied per API key and vary by subscription tier.
Tier-Based Limits
| Tier | Requests per Minute | Requests per Hour | Burst Allowance |
|---|---|---|---|
| Emerging | 100 | 1,000 | 20 requests in 1 second |
| Enterprise | 1,000 | 10,000 | 100 requests in 1 second |
| Sovereign | Custom | Custom | Custom |
Burst allowance defines the maximum number of requests accepted within a 1-second window. Sustained request rates must remain within the per-minute limit. Burst capacity is designed for batch operations that send multiple requests in rapid succession, not for sustained throughput above the per-minute rate.
Sovereign tier limits are negotiated during onboarding and documented in the service agreement.
Rate Limit Headers
Every API response includes rate limit headers. These headers reflect the per-minute window.
| Header | Type | Description |
|---|---|---|
X-RateLimit-Limit | integer | Maximum requests allowed in the current window. |
X-RateLimit-Remaining | integer | Requests remaining in the current window. |
X-RateLimit-Reset | integer | Unix timestamp (seconds) when the current window resets. |
Example Response Headers
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1711267200
Content-Type: application/json
429 Too Many Requests
When a client exceeds its rate limit, Veris returns a 429 Too Many Requests response. The response body includes a retry_after field indicating how many seconds to wait before retrying.
Response
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1711267200
Retry-After: 34
Content-Type: application/json
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Retry after 34 seconds.",
"request_id": "req_abc123",
"details": {
"limit": 100,
"window": "1m",
"retry_after": 34
}
}
}
Recommended Client Behavior
Exponential Backoff
Clients should implement exponential backoff when receiving 429 responses. The recommended strategy:
- Wait for the duration specified in the
Retry-Afterheader. - If the retry also receives a 429, wait
2 * Retry-After. - Continue doubling the wait time up to a maximum of 60 seconds.
- After 5 consecutive 429 responses, pause all requests for 120 seconds.
Proactive Throttling
Monitor the X-RateLimit-Remaining header. When remaining requests drop below 10% of the limit, reduce request frequency proactively rather than waiting for a 429 response.
remaining = int(response.headers["X-RateLimit-Remaining"])
limit = int(response.headers["X-RateLimit-Limit"])
if remaining < limit * 0.10:
time.sleep(1) # Slow down before hitting the wall
Endpoint-Specific Limits
Certain endpoints have additional per-endpoint limits that apply independently of the global tier limits.
| Endpoint | Limit | Rationale |
|---|---|---|
POST /v1/regulatory/report | 10 per minute | SAR generation is compute-intensive. |
POST /v1/screening/batch | 5 per minute | Batch screening jobs consume significant database resources. |
GET /v1/alerts (with export=true) | 2 per minute | Full alert export generates large result sets. |
When an endpoint-specific limit is exceeded, the 429 response includes the endpoint path in the details object:
{
"error": {
"code": "ENDPOINT_RATE_LIMIT_EXCEEDED",
"message": "Endpoint-specific rate limit exceeded for POST /v1/regulatory/report.",
"request_id": "req_def456",
"details": {
"endpoint": "POST /v1/regulatory/report",
"limit": 10,
"window": "1m",
"retry_after": 42
}
}
}
Rate Limit Scope
| Scope | Key | Description |
|---|---|---|
| API key | Authorization header value | All limits are tracked per API key. Multiple keys under the same organization have independent limits. |
| IP address | Client IP | Not used for primary rate limiting. Used only for abuse detection on unauthenticated endpoints. |
Monitoring
Current rate limit usage is available through the Veris dashboard under Settings > API Usage. Historical usage data is retained for 90 days.
For programmatic monitoring, query the /v1/usage/rate-limits endpoint to retrieve current consumption against all applicable limits.
{
"global": {
"limit": 1000,
"used": 153,
"remaining": 847,
"resets_at": "2026-03-24T14:01:00Z"
},
"endpoints": {
"POST /v1/regulatory/report": {
"limit": 10,
"used": 2,
"remaining": 8,
"resets_at": "2026-03-24T14:01:00Z"
}
}
}
Error Codes
Veris uses conventional HTTP status codes and returns structured error responses with machine-readable error codes, human-readable messages, and request tracing identifiers. Every error response follows the same format regardless of the endpoint or failure type.
Error Response Format
{
"error": {
"code": "ENTITY_NOT_FOUND",
"message": "No entity found with ID ent_abc123.",
"request_id": "req_7f3a2b1c",
"details": {
"entity_id": "ent_abc123"
}
}
}
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code. Stable across API versions. Use this for programmatic error handling. |
message | string | Human-readable description. May change between versions. Do not parse this field programmatically. |
request_id | string | Unique identifier for the request. Include this in support inquiries. Format: req_ prefix followed by 8 hex characters. |
details | object | Additional context specific to the error. Structure varies by error code. May be omitted when no additional context is available. |
HTTP Status Codes
400 Bad Request
The request is malformed, missing required fields, or contains invalid values.
| Error Code | Description | Resolution |
|---|---|---|
INVALID_REQUEST_BODY | Request body failed JSON parsing. | Verify the request body is valid JSON. |
MISSING_REQUIRED_FIELD | A required field is absent. details.field identifies the missing field. | Include the specified field in the request body. |
INVALID_FIELD_VALUE | A field value does not meet validation constraints. | Check field constraints in the endpoint documentation. |
IDEMPOTENCY_KEY_REQUIRED | A required Idempotency-Key header is missing. | Provide a UUID v4 in the Idempotency-Key header. |
IDEMPOTENCY_KEY_INVALID | The Idempotency-Key value is not a valid UUID v4. | Use a properly formatted UUID v4. |
INVALID_DATE_RANGE | The specified date range is invalid (start after end, or exceeds maximum span). | Correct the date range parameters. |
401 Unauthorized
Authentication credentials are missing or invalid.
| Error Code | Description | Resolution |
|---|---|---|
AUTHENTICATION_REQUIRED | No Authorization header provided. | Include a valid bearer token or API key. |
INVALID_API_KEY | The provided API key does not exist or has been revoked. | Verify the API key in the Veris dashboard. |
TOKEN_EXPIRED | The bearer token has expired. | Obtain a new token via the authentication endpoint. |
INVALID_SIGNATURE | HMAC signature verification failed (Oris SDK). | Verify the signing key and canonical request format. |
403 Forbidden
The authenticated identity lacks permission for the requested operation.
| Error Code | Description | Resolution |
|---|---|---|
INSUFFICIENT_PERMISSIONS | The API key does not have the required scope. details.required_scope identifies the needed permission. | Request the required scope through the Veris dashboard. |
TIER_RESTRICTION | The requested feature is not available on the current subscription tier. | Contact sales to upgrade the subscription tier. |
IP_NOT_ALLOWLISTED | The request originates from an IP address not on the allowlist. | Add the IP address to the allowlist in the dashboard. |
404 Not Found
The requested resource does not exist.
| Error Code | Description | Resolution |
|---|---|---|
ENTITY_NOT_FOUND | No entity matches the provided identifier. | Verify the entity ID. |
ALERT_NOT_FOUND | No alert matches the provided identifier. | Verify the alert ID. |
CASE_NOT_FOUND | No case matches the provided identifier. | Verify the case ID. |
ENDPOINT_NOT_FOUND | The requested API endpoint does not exist. | Check the API reference for valid endpoints. |
409 Conflict
The request conflicts with the current state of the resource.
| Error Code | Description | Resolution |
|---|---|---|
IDEMPOTENCY_KEY_CONFLICT | The idempotency key was already used with a different request body. | Generate a new idempotency key for the new request. |
CASE_STATUS_CONFLICT | The requested status transition is not valid from the current state. details.current_status and details.requested_status identify the conflict. | Review the case status state machine. |
DUPLICATE_ENTITY | An entity with the same identifying attributes already exists. | Use the existing entity or update it. |
422 Unprocessable Entity
The request is syntactically valid but semantically incorrect.
| Error Code | Description | Resolution |
|---|---|---|
INVALID_ADDRESS_FORMAT | The provided blockchain address is not a valid format for the specified chain. | Verify the address format (e.g., 0x-prefixed, 42 characters for EVM). |
UNSUPPORTED_CHAIN | The specified blockchain is not supported. | Check the list of supported chains. |
INVALID_JURISDICTION | The specified jurisdiction code is not recognized. | Use a valid ISO 3166-1 alpha-2 code. |
AMOUNT_OUT_OF_RANGE | The specified amount exceeds allowable bounds. | Check the endpoint documentation for valid ranges. |
429 Too Many Requests
Rate limit exceeded. See the Rate Limits section for detailed guidance.
| Error Code | Description | Resolution |
|---|---|---|
RATE_LIMIT_EXCEEDED | Global rate limit exceeded. | Wait for the duration in the Retry-After header, then retry. |
ENDPOINT_RATE_LIMIT_EXCEEDED | Endpoint-specific rate limit exceeded. | Wait for the duration in the Retry-After header, then retry. |
500 Internal Server Error
An unexpected error occurred on the server. These errors are automatically reported to the Veris engineering team.
| Error Code | Description | Resolution |
|---|---|---|
INTERNAL_ERROR | An unexpected server error occurred. | Retry the request. If the error persists, contact support with the request_id. |
503 Service Unavailable
A downstream dependency is unavailable. The circuit breaker has engaged to protect system stability.
| Error Code | Description | Resolution |
|---|---|---|
AGENT_UNAVAILABLE | The requested AI agent (triage, investigation, or SAR generation) is temporarily unavailable. | Retry after 30 seconds. The circuit breaker will re-test the downstream dependency automatically. |
DATABASE_UNAVAILABLE | The database is temporarily unreachable. | Retry after 10 seconds. |
SERVICE_MAINTENANCE | The platform is undergoing scheduled maintenance. details.estimated_completion provides the expected completion time. | Wait until the maintenance window closes. |
Circuit Breaker Behavior
Veris implements circuit breakers on all downstream dependencies (AI models, database, vector store, sanctions list providers). When a dependency fails repeatedly, the circuit opens and subsequent requests receive a 503 response immediately rather than waiting for a timeout.
| State | Behavior |
|---|---|
| Closed | Requests flow normally. Failures are counted. |
| Open | All requests return 503 immediately. The breaker re-tests the dependency every 30 seconds. |
| Half-Open | A single test request is sent to the dependency. If it succeeds, the breaker closes. If it fails, the breaker re-opens. |
The circuit breaker opens after 5 consecutive failures or a failure rate exceeding 50% within a 60-second window.
Retry Guidance Summary
| Status Code | Retryable | Strategy |
|---|---|---|
| 400 | No | Fix the request. |
| 401 | No | Fix authentication. |
| 403 | No | Fix permissions. |
| 404 | No | Fix the resource identifier. |
| 409 | No | Resolve the conflict. |
| 422 | No | Fix the request payload. |
| 429 | Yes | Exponential backoff with Retry-After header. |
| 500 | Yes | Retry up to 3 times with exponential backoff (1s, 2s, 4s). |
| 503 | Yes | Retry after 30 seconds. Check the status page if errors persist beyond 5 minutes. |
SLA and Uptime
Veris provides tiered Service Level Agreements that define uptime commitments, resolution latency guarantees, and incident response obligations. All SLA metrics are measured over calendar month windows and reported through the public status page.
Uptime SLA by Tier
| Tier | Monthly Uptime | Maximum Downtime per Month | Credit Threshold |
|---|---|---|---|
| Emerging | 99.5% | 3 hours 39 minutes | Below 99.0% |
| Enterprise | 99.9% | 43 minutes | Below 99.5% |
| Sovereign | 99.99% | 4 minutes 19 seconds | Below 99.9% |
Uptime is calculated as:
Uptime % = ((Total Minutes in Month - Downtime Minutes) / Total Minutes in Month) * 100
Downtime is defined as any period during which the Veris API returns 5xx responses to more than 5% of requests within a 1-minute measurement window. Scheduled maintenance windows announced at least 72 hours in advance are excluded from downtime calculations.
Resolution Latency SLA
Resolution latency measures the time from alert creation to completed triage output. This is the primary performance metric for the Veris real-time pipeline.
| Metric | Target | Measurement |
|---|---|---|
| P50 resolution latency | < 5 seconds | Median across all alerts in the billing period |
| P95 resolution latency | < 40 seconds | 95th percentile across all alerts in the billing period |
| P99 resolution latency | < 120 seconds | 99th percentile across all alerts in the billing period |
Resolution latency covers the full pipeline: alert ingestion, ML pre-triage scoring, deterministic pre-filter evaluation, AI triage classification, and result persistence. It does not include investigation or SAR generation, which are triggered asynchronously after triage.
Agent-Specific Latency Targets
| Agent | P95 Target |
|---|---|
| Triage Agent | < 3 seconds |
| Investigation Agent | < 25 seconds |
| SAR Generation Agent | < 20 seconds |
| Reserve Monitor Agent | < 15 seconds |
API Response Time SLA
Independent of agent processing, the API itself has response time commitments for synchronous endpoints.
| Endpoint Category | P95 Target |
|---|---|
Read operations (GET) | < 200 milliseconds |
Write operations (POST, PUT, PATCH) | < 500 milliseconds |
| List operations with pagination | < 400 milliseconds |
| Search and filter operations | < 600 milliseconds |
Monitoring Infrastructure
Prometheus
All Veris services export metrics to Prometheus, which scrapes at 15-second intervals. Key metrics:
| Metric | Type | Description |
|---|---|---|
veris_api_request_duration_seconds | histogram | API response time by endpoint and status code |
veris_triage_duration_seconds | histogram | Triage agent processing time |
veris_investigation_duration_seconds | histogram | Investigation agent processing time |
veris_alert_queue_depth | gauge | Number of alerts awaiting triage |
veris_circuit_breaker_state | gauge | Circuit breaker state (0=closed, 1=half-open, 2=open) |
Grafana
Grafana dashboards provide real-time visibility into all SLA metrics. Dashboards are organized by domain:
| Dashboard | Contents |
|---|---|
| API Performance | Request rate, latency percentiles, error rate, status code distribution |
| Agent Pipeline | Triage/Investigation/SAR latency, queue depth, success rate |
| Infrastructure | CPU, memory, disk, network across all containers |
| SLA Compliance | Monthly uptime tracking, latency SLA adherence, trend analysis |
Alerting
Prometheus Alertmanager fires alerts when metrics approach SLA thresholds:
| Alert | Condition | Notification |
|---|---|---|
HighErrorRate | 5xx rate exceeds 1% for 5 minutes | PagerDuty + Slack |
HighLatency | P95 response time exceeds 180ms for 10 minutes | Slack |
AgentQueueBacklog | Alert queue depth exceeds 100 for 5 minutes | PagerDuty + Slack |
CircuitBreakerOpen | Any circuit breaker in open state for 2 minutes | PagerDuty |
Incident Response
Severity Levels
| Severity | Definition | Response Time | Resolution Target |
|---|---|---|---|
| SEV-1 | Complete service outage or data integrity issue | 15 minutes | 1 hour |
| SEV-2 | Degraded performance affecting more than 10% of requests | 30 minutes | 4 hours |
| SEV-3 | Partial functionality impaired, workaround available | 2 hours | 24 hours |
| SEV-4 | Minor issue, no customer impact | 24 hours | 5 business days |
Incident Communication
| Channel | Update Frequency | Content |
|---|---|---|
| Status page | Every 15 minutes during SEV-1/SEV-2 | Current state, impact scope, estimated resolution |
| Email notification | At incident start, major updates, and resolution | Summary for affected customers |
| Post-incident report | Within 48 hours of resolution | Root cause, timeline, corrective actions |
Enterprise and Sovereign tier customers receive direct communication from their assigned technical account manager during SEV-1 and SEV-2 incidents.
Status Page
The Veris status page is available at:
https://status.useveris.finance
The status page displays:
- Current operational status for all API endpoints
- Active incidents with real-time updates
- Scheduled maintenance windows
- 90-day uptime history per service component
- Historical incident reports
Sovereign tier customers receive a dedicated status page endpoint that reflects only the components relevant to their deployment.
SLA Credits
When the monthly uptime falls below the committed SLA, customers are eligible for service credits applied to the next billing cycle.
| Uptime Achieved | Credit (% of Monthly Fee) |
|---|---|
| 99.0% to SLA commitment | 10% |
| 95.0% to 98.99% | 25% |
| Below 95.0% | 50% |
Credit requests must be submitted within 30 days of the month in which the SLA was not met. Credits are capped at 50% of the monthly fee and do not apply to one-time fees or professional services.
Exclusions
The following are excluded from SLA calculations:
- Scheduled maintenance announced at least 72 hours in advance
- Force majeure events (natural disasters, war, government actions)
- Client-side issues (network connectivity, misconfigured API keys, rate limit violations)
- Beta features explicitly marked as non-SLA
- Third-party sanctions list provider outages (OFAC, EU, UK list feeds)