API Usage Guide
The VirtueRed API is a REST interface for running AI safety scans programmatically. The primary endpoint is Scan Start (POST /v1/scan/start), which launches safety evaluations against any OpenAI-compatible model endpoint.
Prerequisites
To use the VirtueRed API, you'll need:
- A base URL — Each organization receives a dedicated deployment URL (e.g.,
https://<your-org>.virtueai.io). In all examples below, replace<your-base-url>with this URL. - A VirtueRed account — An account provisioned for your organization.
- An API key — Used to authenticate every request via the
X-API-Keyheader.
VirtueRed is currently available on-demand. Contact VirtueAI to receive your organization's deployment URL, account, and API key.
Quickstart
1) Start a scan
- cURL
- Python
- TypeScript
curl -X POST "https://<your-base-url>/v1/scan/start" \
-H "X-API-Key: <your-virtuered-api-key>" \
-H "Content-Type: application/json" \
-d '{
"scan_name": "quickstart-scan",
"model": {
"base_url": "https://api.openai.com/v1",
"api_key": "<your-model-api-key>",
"model_name": "gpt-4o"
},
"datasets": [
{"name": "Healthcare Risks"}
]
}'
import requests
url = "https://<your-base-url>/v1/scan/start"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {
"scan_name": "quickstart-scan",
"model": {
"base_url": "https://api.openai.com/v1",
"api_key": "<your-model-api-key>",
"model_name": "gpt-4o",
},
"datasets": [{"name": "Healthcare Risks"}],
}
resp = requests.post(url, headers=headers, json=payload, timeout=60)
print(resp.status_code)
print(resp.json())
const response = await fetch("https://<your-base-url>/v1/scan/start", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
scan_name: "quickstart-scan",
model: {
base_url: "https://api.openai.com/v1",
api_key: "<your-model-api-key>",
model_name: "gpt-4o",
},
datasets: [{ name: "Healthcare Risks" }],
}),
});
const data = await response.json();
console.log(response.status, data);
Expected response:
{
"scan_id": "scan-12345",
"status": 200
}
Save the returned scan_id for subsequent requests.
2) Check progress
- cURL
- Python
- TypeScript
curl -X POST "https://<your-base-url>/v1/scan/progress" \
-H "X-API-Key: <your-virtuered-api-key>" \
-H "Content-Type: application/json" \
-d '{"scan_id": "scan-12345"}'
import requests
url = "https://<your-base-url>/v1/scan/progress"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {"scan_id": "scan-12345"}
resp = requests.post(url, headers=headers, json=payload, timeout=60)
print(resp.status_code)
print(resp.json())
const response = await fetch("https://<your-base-url>/v1/scan/progress", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ scan_id: "scan-12345" }),
});
const data = await response.json();
console.log(response.status, data);
3) Get summary
Run this after scan status is Finished.
- cURL
- Python
- TypeScript
curl -X POST "https://<your-base-url>/v1/scan/summary" \
-H "X-API-Key: <your-virtuered-api-key>" \
-H "Content-Type: application/json" \
-d '{"scan_id": "scan-12345"}'
import requests
url = "https://<your-base-url>/v1/scan/summary"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {"scan_id": "scan-12345"}
resp = requests.post(url, headers=headers, json=payload, timeout=60)
print(resp.status_code)
print(resp.json())
const response = await fetch("https://<your-base-url>/v1/scan/summary", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ scan_id: "scan-12345" }),
});
const data = await response.json();
console.log(response.status, data);
4) Download full artifacts
This step is optional.
- cURL
- Python
- TypeScript
curl -X POST "https://<your-base-url>/v1/scan/download" \
-H "X-API-Key: <your-virtuered-api-key>" \
-H "Content-Type: application/json" \
-d '{"scan_id": "scan-12345"}' \
-o scan-results.zip
import requests
url = "https://<your-base-url>/v1/scan/download"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {"scan_id": "scan-12345"}
resp = requests.post(url, headers=headers, json=payload, timeout=120)
resp.raise_for_status()
with open("scan-results.zip", "wb") as f:
f.write(resp.content)
print("saved scan-results.zip")
import { writeFileSync } from "node:fs";
const response = await fetch("https://<your-base-url>/v1/scan/download", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ scan_id: "scan-12345" }),
});
if (!response.ok) {
throw new Error(`Download failed: ${response.status}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
writeFileSync("scan-results.zip", buffer);
console.log("saved scan-results.zip");
Scan Lifecycle
| Step | Endpoint | When to use |
|---|---|---|
| Start | POST /v1/scan/start | Launch a standard scan with explicit datasets |
| Start (Sandbox) | POST /v1/sandbox/scan/start | Quick connection validation with pre-configured tests |
| Monitor | POST /v1/scan/progress | Poll execution progress/status or list all scans (optional since) |
| Summarize | POST /v1/scan/summary | Retrieve aggregate scores and failures when finished |
| Download | POST /v1/scan/download | Export ZIP artifacts (results.jsonl, summary.json, report.pdf) |
| Pause | POST /v1/scan/pause | Pause an active scan |
| Retry | POST /v1/scan/retry | Resume from checkpoint when status is Paused, Failed, or Error |
| Usage | POST /v1/scan/usage | Retrieve scan count for the current billing cycle, all time, or a custom range |
Scan status reference
| Status | Meaning | Next action |
|---|---|---|
Initializing | Setup in progress | Keep polling progress |
Scanning | Running tests | Keep polling progress |
Paused | Stopped by user | Retry when ready |
Finished | Completed successfully | Get summary and download artifacts |
Failed | Runtime failure | Retry from checkpoint |
Error | Unexpected system error | Retry; if repeated, contact support |
Endpoint Reference
Start a Scan
POST /v1/scan/start
Launch a safety scan with your model and dataset configuration.
Request body (JSON)
Content-Type: application/json.
| Field | Type | Required | Description |
|---|---|---|---|
scan_name | string | Yes | Unique scan name |
model | object | Yes | Model provider configuration (see model object) |
datasets | array | Yes | Datasets to evaluate (see datasets[] item) |
algorithms | array | No | Additional attack algorithms (see Available Additional Algorithms) |
input_modalities | array | No | Input modalities (default: ["text"]) |
output_modalities | array | No | Output modalities (default: ["text"]) |
qpm | number | No | Queries-per-minute limit against the target model |
concurrency | number | No | Maximum concurrent requests to the target model |
Rate-limit note: If
qpmis not set, no explicit QPM cap is applied. Defaultconcurrencyis8. Tuneqpmandconcurrencybased on your target endpoint capacity and provider limits.
model object
| Field | Type | Required | Description |
|---|---|---|---|
base_url | string | Yes | OpenAI-compatible API base URL |
api_key | string | Yes | Model provider API key |
model_name | string | Yes | Model identifier (e.g., gpt-4o) |
temperature | number | No | Sampling temperature (default 0.7) |
max_tokens | number | No | Maximum output tokens (default 2048) |
extra_headers | object | No | Additional headers forwarded to the upstream model endpoint |
Custom header note: For self-hosted or custom inference endpoints, set provider-specific headers in
model.extra_headers. See Example: Custom endpoint with extra headers.
datasets[] item
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Dataset name |
Available Datasets
| Type | Risk Categories |
|---|---|
| Regulation-based Risks | EU Artificial Intelligence Act, AI Company Policies, General Data Protection Regulation, OWASP LLM Top 10, NIST AI RMF, MITRE ATLAS, FINRA |
| Use-Case-Driven Risks | Bias, Over Cautiousness, Hallucination, Societal Harmfulness, Privacy, Robustness, Finance Brand Risk, Education Brand Risk, Health Care Brand Risk |
| Domain-specific Risks | Healthcare Risks, Finance Risks, Retail Policy Compliance, IT/Tech Policy Compliance |
Full datasets payload
"datasets": [
{"name": "Healthcare Risks"},
{"name": "Finance Risks"},
{"name": "Retail Policy Compliance"},
{"name": "IT/Tech Policy Compliance"},
{"name": "EU Artificial Intelligence Act"},
{"name": "AI Company Policies"},
{"name": "General Data Protection Regulation"},
{"name": "FINRA"},
{"name": "MITRE ATLAS"},
{"name": "NIST AI RMF"},
{"name": "OWASP LLM Top 10"},
{"name": "Bias"},
{"name": "Over Cautiousness"},
{"name": "Hallucination"},
{"name": "Societal Harmfulness"},
{"name": "Privacy"},
{"name": "Robustness"},
{"name": "Finance Brand Risk"},
{"name": "Education Brand Risk"},
{"name": "Health Care Brand Risk"}
]
Available Additional Algorithms
By default, every scan runs our curated set of red teaming tests combining multiple attack algorithms. You can optionally select additional attack algorithms to apply together with the defaults.
| Algorithm | Description |
|---|---|
| DarkCite | Authority citation-driven jailbreak |
| Bijection Learning | Learns custom encoding to bypass filters |
| Crescendo Attack | Multi-turn progressive jailbreak |
| Flip Attack | Text reversal/denoising jailbreak |
| Language Game | Encodes harmful prompts with transformations |
| BoN Attack | Best-of-N augmented prompt search |
| Humor Attack | Harmful requests hidden in playful context |
| Emoji Attack | Harmful token substitution with emojis |
All algorithms payload
"algorithms": [
"DarkCite",
"Bijection Learning",
"Crescendo Attack",
"Flip Attack",
"Language Game",
"BoN Attack",
"Humor Attack",
"Emoji Attack"
]
Request body (TOML)
Content-Type: application/toml.
Send TOML as the raw request body. scan_name must be present in TOML (top-level or under [scan]).
See detailed keys and examples in TOML Configuration Reference.
Download a ready-to-use template: virtuered-scan-template.toml
- cURL
- Python
- TypeScript
curl -X POST "$BASE_URL/v1/scan/start" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/toml" \
--data-binary @/path/to/scan-config.toml
import requests
from pathlib import Path
url = "https://<your-base-url>/v1/scan/start"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/toml",
}
toml_body = Path("/path/to/scan-config.toml").read_text(encoding="utf-8")
resp = requests.post(url, headers=headers, data=toml_body, timeout=60)
print(resp.status_code)
print(resp.json())
import { readFileSync } from "node:fs";
const tomlBody = readFileSync("/path/to/scan-config.toml", "utf8");
const response = await fetch("https://<your-base-url>/v1/scan/start", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/toml",
},
body: tomlBody,
});
const data = await response.json();
console.log(response.status, data);
Example request
- cURL
- Python
- TypeScript
curl -X POST "$BASE_URL/v1/scan/start" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scan_name": "my-safety-scan",
"model": {
"base_url": "https://api.openai.com/v1",
"api_key": "sk-...",
"model_name": "gpt-4o",
"temperature": 0.7,
"max_tokens": 2048
},
"datasets": [{"name": "Healthcare Risks"}],
"input_modalities": ["text"],
"output_modalities": ["text"],
"qpm": 60,
"concurrency": 16
}'
import requests
url = "https://<your-base-url>/v1/scan/start"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {
"scan_name": "my-safety-scan",
"model": {
"base_url": "https://api.openai.com/v1",
"api_key": "<your-model-api-key>",
"model_name": "gpt-4o",
"temperature": 0.7,
"max_tokens": 2048,
},
"datasets": [{"name": "Healthcare Risks"}],
"input_modalities": ["text"],
"output_modalities": ["text"],
"qpm": 10,
"concurrency": 2,
}
resp = requests.post(url, headers=headers, json=payload, timeout=60)
print(resp.status_code)
print(resp.json())
const response = await fetch("https://<your-base-url>/v1/scan/start", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
scan_name: "my-safety-scan",
model: {
base_url: "https://api.openai.com/v1",
api_key: "<your-model-api-key>",
model_name: "gpt-4o",
temperature: 0.7,
max_tokens: 2048,
},
datasets: [{ name: "Healthcare Risks" }],
input_modalities: ["text"],
output_modalities: ["text"],
qpm: 10,
concurrency: 2,
}),
});
const data = await response.json();
console.log(response.status, data);
Example: Custom endpoint with extra headers
Use model.extra_headers when your upstream endpoint requires additional request headers.
- cURL
- Python
- TypeScript
curl -X POST "https://<your-base-url>/v1/scan/start" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scan_name": "custom-endpoint-scan",
"model": {
"base_url": "https://your-inference-api.example.com/openai/v1",
"api_key": "your-model-api-key",
"model_name": "meta-llama/Llama-3.1-8B-Instruct",
"temperature": 1.0,
"max_tokens": 256,
"extra_headers": {
"setup": "lora-adapter",
"X-Request-Context": "{\"service-name\": \"my-service\"}"
}
},
"datasets": [
{"name": "Bias"}
],
"input_modalities": ["text"],
"output_modalities": ["text"],
"qpm": 60,
"concurrency": 16
}'
import requests
url = "https://<your-base-url>/v1/scan/start"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {
"scan_name": "custom-endpoint-scan",
"model": {
"base_url": "https://your-inference-api.example.com/openai/v1",
"api_key": "<your-model-api-key>",
"model_name": "meta-llama/Llama-3.1-8B-Instruct",
"temperature": 1.0,
"max_tokens": 256,
"extra_headers": {
"setup": "lora-adapter",
"X-Request-Context": "{\"service-name\": \"my-service\"}",
},
},
"datasets": [{"name": "Bias"}],
"input_modalities": ["text"],
"output_modalities": ["text"],
"qpm": 60,
"concurrency": 16,
}
resp = requests.post(url, headers=headers, json=payload, timeout=60)
print(resp.status_code)
print(resp.json())
const response = await fetch("https://<your-base-url>/v1/scan/start", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
scan_name: "custom-endpoint-scan",
model: {
base_url: "https://your-inference-api.example.com/openai/v1",
api_key: "<your-model-api-key>",
model_name: "meta-llama/Llama-3.1-8B-Instruct",
temperature: 1.0,
max_tokens: 256,
extra_headers: {
setup: "lora-adapter",
"X-Request-Context": "{\"service-name\": \"my-service\"}",
},
},
datasets: [{ name: "Bias" }],
input_modalities: ["text"],
output_modalities: ["text"],
qpm: 60,
concurrency: 16,
}),
});
const data = await response.json();
console.log(response.status, data);
Example response
{
"scan_id": "scan-12345",
"status": 200
}
| Field | Type | Description |
|---|---|---|
scan_id | string | Unique scan identifier |
status | number | HTTP status code (200 on success) |
Start a Sandbox Scan
POST /v1/sandbox/scan/start
Launch a quick validation scan with pre-configured safety tests. Use this to verify your model connection before running a full scan.
Minimum requirements
Only scan_name and model are required. Inside model, include base_url, api_key, and model_name.
Custom header note: Sandbox scans support
model.extra_headersfor provider-specific upstream headers.
Example request
- cURL
- Python
- TypeScript
curl -X POST "$BASE_URL/v1/sandbox/scan/start" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scan_name": "my-sandbox-test",
"model": {
"base_url": "https://api.example.com/v1",
"api_key": "sk-...",
"model_name": "my-model",
"extra_headers": {
"X-Provider-Routing": "sandbox"
}
}
}'
import requests
url = "https://<your-base-url>/v1/sandbox/scan/start"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {
"scan_name": "my-sandbox-test",
"model": {
"base_url": "https://api.example.com/v1",
"api_key": "<your-model-api-key>",
"model_name": "my-model",
"extra_headers": {
"X-Provider-Routing": "sandbox",
},
},
}
resp = requests.post(url, headers=headers, json=payload, timeout=60)
print(resp.status_code)
print(resp.json())
const response = await fetch("https://<your-base-url>/v1/sandbox/scan/start", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
scan_name: "my-sandbox-test",
model: {
base_url: "https://api.example.com/v1",
api_key: "<your-model-api-key>",
model_name: "my-model",
extra_headers: {
"X-Provider-Routing": "sandbox",
},
},
}),
});
const data = await response.json();
console.log(response.status, data);
Example response
{
"scan_id": "scan-12345",
"status": 200
}
Check Scan Progress
POST /v1/scan/progress
Returns progress for a specific scan, or for all scans the authenticated user can access (both ongoing and finished).
- Without
scan_id— Returns all scans, sorted bycreated_atascending. Use the optionalsincefilter to narrow results by creation time. - With
scan_id— Returns progress for that specific scan.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
scan_id | string | No | Scan identifier. If omitted, returns all scans |
since | string | No | ISO 8601 datetime (e.g., 2024-01-15T00:00:00Z). When scan_id is omitted, excludes scans created before this time |
Example request (single scan)
- cURL
- Python
- TypeScript
curl -X POST "$BASE_URL/v1/scan/progress" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scan_id": "scan-12345"}'
import requests
url = "https://<your-base-url>/v1/scan/progress"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {"scan_id": "scan-12345"}
resp = requests.post(url, headers=headers, json=payload, timeout=60)
print(resp.status_code)
print(resp.json())
const response = await fetch("https://<your-base-url>/v1/scan/progress", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ scan_id: "scan-12345" }),
});
const data = await response.json();
console.log(response.status, data);
Example request (all scans, optional since)
- cURL
- Python
- TypeScript
# All scans
curl -X POST "$BASE_URL/v1/scan/progress" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
# All scans since a given time
curl -X POST "$BASE_URL/v1/scan/progress" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"since": "2024-01-15T00:00:00Z"}'
import requests
url = "https://<your-base-url>/v1/scan/progress"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
# All scans
resp = requests.post(url, headers=headers, json={}, timeout=60)
print(resp.json()) # list of progress objects
# All scans since a given time
resp = requests.post(url, headers=headers, json={"since": "2024-01-15T00:00:00Z"}, timeout=60)
print(resp.json())
// All scans
const response = await fetch("https://<your-base-url>/v1/scan/progress", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
const data = await response.json(); // array of progress objects
// All scans since a given time
const response2 = await fetch("https://<your-base-url>/v1/scan/progress", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ since: "2024-01-15T00:00:00Z" }),
});
const data2 = await response2.json();
Example response (single scan)
{
"scan_id": "scan-12345",
"scan_time": "2024-01-15T10:30:00Z",
"created_at": "2024-01-15T10:30:00Z",
"status": "Scanning",
"passed_line": 91,
"remaining_time": 120,
"percentage": 45.5
}
Example response (all scans)
Returns an array of the same shape, sorted by created_at ascending:
[
{
"scan_id": "scan-111",
"scan_time": "2024-01-14T08:00:00Z",
"created_at": "2024-01-14T08:00:00Z",
"status": "Finished",
"passed_line": 200,
"remaining_time": 0,
"percentage": 100
},
{
"scan_id": "scan-12345",
"scan_time": "2024-01-15T10:30:00Z",
"created_at": "2024-01-15T10:30:00Z",
"status": "Scanning",
"passed_line": 91,
"remaining_time": 120,
"percentage": 45.5
}
]
Response fields
| Field | Type | Description |
|---|---|---|
scan_id | string | Scan identifier |
scan_time | string | Scan start timestamp (ISO 8601) |
created_at | string | When the scan was created (ISO 8601) |
status | string | Lifecycle status (Initializing, Scanning, Paused, Finished, Failed, Error) |
passed_line | number | Number of completed test cases |
remaining_time | number | Estimated seconds remaining |
percentage | number | Completion percentage (0–100) |
Get Scan Summary
POST /v1/scan/summary
Retrieves aggregate scores, failure examples, and risk-level metadata for a completed scan.
Note: The scan must have a
Finishedstatus; otherwise returns HTTP400.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
scan_id | string | Yes | Scan identifier |
Example request
- cURL
- Python
- TypeScript
curl -X POST "$BASE_URL/v1/scan/summary" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scan_id": "scan-12345"}'
import requests
url = "https://<your-base-url>/v1/scan/summary"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {"scan_id": "scan-12345"}
resp = requests.post(url, headers=headers, json=payload, timeout=60)
print(resp.status_code)
print(resp.json())
const response = await fetch("https://<your-base-url>/v1/scan/summary", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ scan_id: "scan-12345" }),
});
const data = await response.json();
console.log(response.status, data);
Key response fields
| Field | Type | Description |
|---|---|---|
averages | object | Average score per dataset |
averages_sub | object | Average score per dataset subcategory |
averages_attack | object | Average score by attack type |
failures | object | Failed test cases grouped by dataset |
scores | object | Counts by risk category |
risk_level | string | Overall risk level (Low, Medium, High) |
avg_score | number | Overall average safety score |
Download Scan Results
POST /v1/scan/download
Downloads the complete scan artifacts as a ZIP archive.
Note: The scan must have a
Finishedstatus; otherwise returns HTTP400.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
scan_id | string | Yes | Scan identifier |
Example request
- cURL
- Python
- TypeScript
curl -X POST "$BASE_URL/v1/scan/download" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scan_id": "scan-12345"}' \
-o scan-results.zip
import requests
url = "https://<your-base-url>/v1/scan/download"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {"scan_id": "scan-12345"}
resp = requests.post(url, headers=headers, json=payload, timeout=120)
resp.raise_for_status()
with open("scan-results.zip", "wb") as f:
f.write(resp.content)
print("saved scan-results.zip")
import { writeFileSync } from "node:fs";
const response = await fetch("https://<your-base-url>/v1/scan/download", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ scan_id: "scan-12345" }),
});
if (!response.ok) {
throw new Error(`Download failed: ${response.status}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
writeFileSync("scan-results.zip", buffer);
console.log("saved scan-results.zip");
Archive contents
| File | Description |
|---|---|
results.jsonl | Per-case test outputs |
summary.json | Aggregated summary |
report.pdf | Formatted report |
Pause a Scan
POST /v1/scan/pause
Pauses a running scan.
Only scans in
ScanningorInitializingstatus can be paused.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
scan_id | string | Yes | Scan identifier |
Example request
- cURL
- Python
- TypeScript
curl -X POST "$BASE_URL/v1/scan/pause" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scan_id": "scan-12345"}'
import requests
url = "https://<your-base-url>/v1/scan/pause"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {"scan_id": "scan-12345"}
resp = requests.post(url, headers=headers, json=payload, timeout=60)
print(resp.status_code)
print(resp.json())
const response = await fetch("https://<your-base-url>/v1/scan/pause", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ scan_id: "scan-12345" }),
});
const data = await response.json();
console.log(response.status, data);
Response
{
"status": "paused",
"scan_id": "scan-12345"
}
Retry a Scan
POST /v1/scan/retry
Resumes an interrupted scan from its last checkpoint.
Retry-eligible statuses
FailedErrorPaused
Scans in Finished, Scanning, or Initializing status cannot be retried.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
scan_id | string | Yes | Scan identifier |
Example request
- cURL
- Python
- TypeScript
curl -X POST "$BASE_URL/v1/scan/retry" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"scan_id": "scan-12345"}'
import requests
url = "https://<your-base-url>/v1/scan/retry"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
payload = {"scan_id": "scan-12345"}
resp = requests.post(url, headers=headers, json=payload, timeout=60)
print(resp.status_code)
print(resp.json())
const response = await fetch("https://<your-base-url>/v1/scan/retry", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ scan_id: "scan-12345" }),
});
const data = await response.json();
console.log(response.status, data);
Response
{
"scan_id": "scan-12345",
"status": 200
}
Get Scan Usage
POST /v1/scan/usage
Returns the number of scans consumed by the authenticated user within a specified time window. Use this endpoint to monitor usage against your plan quota. Only finished, non-sandbox scans contribute to the count — in-progress, paused, failed, and sandbox scans are excluded.
Note: The count returned by this endpoint is for informational purposes only. Actual billed amounts are determined by the official invoice issued at the close of each billing cycle.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
mode | string | Yes | One of: current_billing_cycle, all_time, custom |
start | string | No | ISO 8601 datetime. For custom mode, at least one of start or end is required |
end | string | No | ISO 8601 datetime. When both start and end are provided, end must be ≥ start |
Modes
current_billing_cycle— Counts scans in the current calendar month (UTC).periodreflects the billing window.all_time— Counts all scans.periodspans from the first scan to the last.custom— Counts scans within[start, end]. At least one ofstartorendis required; the other defaults to the first or last scan as applicable.
Example request
- cURL
- Python
- TypeScript
# Current billing cycle
curl -X POST "$BASE_URL/v1/scan/usage" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode": "current_billing_cycle"}'
# All time
curl -X POST "$BASE_URL/v1/scan/usage" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode": "all_time"}'
# Custom range
curl -X POST "$BASE_URL/v1/scan/usage" \
-H "X-API-Key: $VIRTUERED_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode": "custom", "start": "2024-01-01T00:00:00Z", "end": "2024-01-31T23:59:59Z"}'
import requests
url = "https://<your-base-url>/v1/scan/usage"
headers = {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
}
# Current billing cycle
resp = requests.post(url, headers=headers, json={"mode": "current_billing_cycle"}, timeout=60)
print(resp.json())
# All time
resp = requests.post(url, headers=headers, json={"mode": "all_time"}, timeout=60)
print(resp.json())
# Custom range
resp = requests.post(url, headers=headers, json={
"mode": "custom",
"start": "2024-01-01T00:00:00Z",
"end": "2024-01-31T23:59:59Z",
}, timeout=60)
print(resp.json())
// Current billing cycle
const response = await fetch("https://<your-base-url>/v1/scan/usage", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ mode: "current_billing_cycle" }),
});
const data = await response.json();
// All time
const response2 = await fetch("https://<your-base-url>/v1/scan/usage", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({ mode: "all_time" }),
});
const data2 = await response2.json();
// Custom range
const response3 = await fetch("https://<your-base-url>/v1/scan/usage", {
method: "POST",
headers: {
"X-API-Key": "<your-virtuered-api-key>",
"Content-Type": "application/json",
},
body: JSON.stringify({
mode: "custom",
start: "2024-01-01T00:00:00Z",
end: "2024-01-31T23:59:59Z",
}),
});
const data3 = await response3.json();
Example response
{
"user_id": "user-abc",
"mode": "current_billing_cycle",
"period": {
"start": "2024-01-01T00:00:00.000000Z",
"end": "2024-01-31T23:59:59.999999Z"
},
"total_scans": 12,
"as_of": "2024-01-18T14:30:00.123456Z"
}
Response fields
| Field | Type | Description |
|---|---|---|
user_id | string | User identifier |
mode | string | Requested mode |
period | object or null | { "start": "<ISO 8601>", "end": "<ISO 8601>" }; null when there are no scans |
total_scans | number | Number of finished, non-sandbox scans in the period |
as_of | string | ISO 8601 timestamp indicating when this usage snapshot was taken |
TOML Configuration Reference
/v1/scan/start and /v1/sandbox/scan/start accept TOML via Content-Type: application/toml.
Download a ready-to-use template: virtuered-scan-template.toml
Required keys
| Key | Location | Description |
|---|---|---|
scan_name | top-level or [scan] | Unique scan name |
base_url | [model] | OpenAI-compatible base URL |
api_key | [model] | Model provider API key |
model_name | [model] | Model identifier |
Optional keys
| Key | Location | Description |
|---|---|---|
temperature | [model] | Sampling temperature |
max_tokens | [model] | Maximum output tokens |
extra_headers | [model.extra_headers] | Additional upstream HTTP headers |
datasets | [scan] | Dataset configurations |
algorithms | [scan] | Additional attack algorithms |
input_modalities | [scan] | Input modalities |
output_modalities | [scan] | Output modalities |
qpm | [scan] | Queries per minute limit |
concurrency | [scan] | Concurrent request limit |
is_user_model | [scan] | Whether the model is user-provided |
application_id | [scan] | Application identifier |
continue_flag | [scan] | Resume from checkpoint |
Example TOML
[model]
base_url = "https://api.openai.com/v1"
api_key = "sk-..."
model_name = "gpt-4o"
temperature = 0.7
max_tokens = 2048
[model.extra_headers]
setup = "lora-adapter"
X-Request-Context = '{"service-name": "my-service"}'
[scan]
scan_name = "toml-config-scan"
datasets = [
{name = "Healthcare Risks"},
{name = "Bias"}
]
input_modalities = ["text"]
output_modalities = ["text"]
qpm = 10
concurrency = 2
Error Handling and Troubleshooting
Standard status codes
| Status Code | Description |
|---|---|
200 | Success |
400 | Bad request, invalid state, or operation attempted before scan completion |
401 | Missing or invalid API key |
404 | Unknown scan ID |
500 | Internal server error |
Common issues
- Missing
X-API-Keyheader - Missing required fields (
scan_name,model,scan_id) - Invalid state transitions (e.g., pausing a
Finishedscan) - Calling summary or download before the scan has completed
Debug checklist
- Confirm request headers (
X-API-Key,Content-Type) - Confirm scan status via
POST /v1/scan/progress - Verify the target model endpoint is healthy and reachable
- Verify the requested lifecycle transition is valid
- Retry from checkpoint for
Paused,Failed, orErrorscans
Support
For API access, tenant provisioning, or platform support, contact VirtueAI support.