Introduction

The Router Auditor API runs 60 security detectors against an LLM API router endpoint, probing for model substitution, parameter fraud, billing manipulation, supply-chain attacks, and more.

Base URL:

https://safe.bua.sh/api/v1

All responses are JSON. The API follows REST conventions with standard HTTP status codes.

Authentication

All endpoints except /health and /detectors require a Bearer token.

Authorization: Bearer YOUR_API_KEY
Set via AUDITOR_API_KEY environment variable on the server.

Error Handling

Errors return a JSON body with a detail field:

{
  "detail": "Not found"
}
StatusMeaning
400Bad request (invalid parameters, unknown detector IDs)
401Missing or invalid API key
404Task not found
409Conflict (cancel non-running, delete running task)
503Server at capacity (max 3 concurrent tasks)

Create Test

POST/api/v1/tests

Queue a new audit run. Returns immediately with a task_id and WebSocket URL for real-time progress.

Request Body

FieldTypeRequiredDescription
router_endpointstring*Base URL of the router under test
api_keystring*API key for the router
claimed_modelstringModel name claimed by router. Default: "gpt-4o"
claimed_providerstringopenai | anthropic | gemini | any. Default: "any"
capabilitiesstring[]text vision pdf audio tool_calling task_model. Default: ["text"]
auth_methodstringbearer | x-api-key | query. Default: "bearer"
api_formatstringopenai | anthropic | auto. Default: "openai"
timeoutnumberPer-request timeout in seconds (5–120). Default: 30
onlystring[]Run only these detector IDs, e.g. ["D4a", "D11"]
direct_endpointstringDirect provider URL for baseline comparison
direct_api_keystringAPI key for direct provider
callback_urlstringWebhook URL called on task completion
extra_headersobjectAdditional headers to send with each probe

Response 201

{
  "task_id": "a1b2c3d4e5f6",
  "status": "pending",
  "message": "Test created and queued",
  "ws_url": "/api/v1/tests/a1b2c3d4e5f6/ws"
}

Example

curl -X POST https://safe.bua.sh/api/v1/tests \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "router_endpoint": "https://api.example.com/v1",
    "api_key": "sk-router-key",
    "claimed_model": "claude-opus-4-6",
    "claimed_provider": "anthropic",
    "capabilities": ["text", "tool_calling"],
    "timeout": 60
  }'

List Tests

GET/api/v1/tests

List all audit tasks, with optional filtering.

Query Parameters

ParamTypeDescription
limitintMax results. Default: 20
offsetintSkip N results. Default: 0
statusstringFilter: pending | running | completed | failed | cancelled
endpointstringFilter by router endpoint (substring match)

Response 200

[
  {
    "task_id": "a1b2c3d4e5f6",
    "status": "completed",
    "created_at": "2026-04-15T10:30:00Z",
    "completed_at": "2026-04-15T10:32:15Z",
    "router_endpoint": "https://api.example.com/v1",
    "claimed_model": "claude-opus-4-6",
    "tier_assignment": "BLACKLIST",
    "overall_verdict": "fail",
    "progress": "60/60"
  }
]

Get Test Detail

GET/api/v1/tests/{task_id}

Get full details of a test including sanitized config and complete report.

Response 200

Returns TaskDetail — extends TaskSummary with config (API keys masked), report (full JSON), and error (if failed).

Download Report

GET/api/v1/tests/{task_id}/report

Download the full JSON report as a file. Only available for completed tasks.

Response 200

Returns application/json with Content-Disposition: attachment.

{
  "router_endpoint": "...",
  "overall_verdict": "fail",
  "tier_assignment": "BLACKLIST",
  "total_detectors": 60,
  "passed": 23,
  "failed": 15,
  "results": [
    {
      "detector_id": "D31",
      "verdict": "fail",
      "confidence": 1.0,
      "evidence": { ... },
      "latency_ms": 8324
    }
  ]
}

Download JUnit XML

GET/api/v1/tests/{task_id}/junit

Download the report as JUnit XML for CI/CD integration. Only available for completed tasks.

Cancel Test

POST/api/v1/tests/{task_id}/cancel

Cancel a running test. Returns 409 if the task is not currently running.

Delete Test

DEL/api/v1/tests/{task_id}

Delete a test and its results. Returns 409 if still running (cancel first).

WebSocket Progress Stream

WS/api/v1/tests/{task_id}/ws

Real-time bidirectional stream of audit progress. No authentication required (task_id acts as capability token).

Event Types

TypeWhenData
statusOn connect{ status, progress }
stage_startStage begins{ name, count }
detector_startDetector begins{ id, name }
detector_endDetector finishes{ id, verdict, latency_ms }
stage_endStage ends{ name, results[] }
task_endTask complete{ status, tier }
pingEvery 30sKeepalive

Example

const ws = new WebSocket("wss://safe.bua.sh/api/v1/tests/abc123/ws");
ws.onmessage = (e) => {
  const event = JSON.parse(e.data);
  if (event.type === "detector_end") {
    console.log(`${event.data.id}: ${event.data.verdict} (${event.data.latency_ms}ms)`);
  }
  if (event.type === "task_end") {
    console.log(`Done: ${event.data.tier}`);
    ws.close();
  }
};

List Detectors

GET/api/v1/detectors

List all registered detectors with metadata. No authentication required.

Response 200

[
  {
    "detector_id": "D4a",
    "detector_name": "TokenizerFingerprint",
    "priority": "P0",
    "judge_mode": "once",
    "request_count": 1,
    "required_capabilities": ["text"],
    "required_provider": "any",
    "requires_direct": false,
    "requires_single_route_claim": false,
    "description": "Detect model substitution via tokenizer..."
  }
]

Health Check

GET/api/v1/health

Health check endpoint. No authentication required.

Response 200

{
  "status": "ok",
  "version": "0.1.0",
  "active_tasks": 1,
  "total_completed": 42
}

Quick Start

Run a full audit in 3 steps:

1. Create a test

TASK=$(curl -s -X POST https://safe.bua.sh/api/v1/tests \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "router_endpoint": "https://api.router.com/v1",
    "api_key": "sk-router-key",
    "claimed_model": "gpt-4o",
    "claimed_provider": "openai"
  }' | jq -r .task_id)

echo "Task ID: $TASK"

2. Poll for completion

while true; do
  STATUS=$(curl -s https://safe.bua.sh/api/v1/tests/$TASK \
    -H "Authorization: Bearer $API_KEY" | jq -r .status)
  echo "Status: $STATUS"
  [ "$STATUS" = "completed" ] && break
  sleep 5
done

3. Download the report

curl -s https://safe.bua.sh/api/v1/tests/$TASK/report \
  -H "Authorization: Bearer $API_KEY" | jq .tier_assignment
# → "BLACKLIST" or "TIER_1"

Webhooks

If callback_url is provided when creating a test, the server sends a POST request on completion:

POST https://your-server.com/webhook
Content-Type: application/json

{
  "task_id": "a1b2c3d4e5f6",
  "status": "completed",
  "overall_verdict": "fail",
  "tier_assignment": "BLACKLIST",
  "passed": 23,
  "failed": 15,
  "report_url": "/api/v1/tests/a1b2c3d4e5f6/report"
}

Webhook timeout is 10 seconds. Failures are logged but do not affect the task result.

WebSocket Guide

For real-time progress tracking, connect to the WebSocket endpoint immediately after creating a test:

Python Example

import asyncio, json, httpx, websockets

async def audit(endpoint, api_key):
    async with httpx.AsyncClient() as client:
        r = await client.post(
            "https://safe.bua.sh/api/v1/tests",
            headers={"Authorization": "Bearer YOUR_AUDITOR_KEY"},
            json={
                "router_endpoint": endpoint,
                "api_key": api_key,
                "claimed_model": "gpt-4o",
            },
        )
        task_id = r.json()["task_id"]

    async with websockets.connect(
        f"wss://safe.bua.sh/api/v1/tests/{task_id}/ws"
    ) as ws:
        async for msg in ws:
            ev = json.loads(msg)
            if ev["type"] == "detector_end":
                d = ev["data"]
                print(f"  {d['id']}: {d['verdict']}")
            if ev["type"] == "task_end":
                print(f"Result: {ev['data']['tier']}")
                break

asyncio.run(audit("https://api.router.com/v1", "sk-key"))

Connection Lifecycle

Client                          Server
  │                               │
  ├── connect ──────────────────► │
  │                               ├── status {pending}
  │                               ├── stage_start {pre_screen}
  │                               ├── detector_start {D31}
  │                               ├── detector_end {D31, fail}
  │                               ├── stage_end {pre_screen}
  │                               ├── stage_start {s0}
  │                               ├── ...
  │                               ├── ping (every 30s)
  │                               ├── ...
  │                               ├── task_end {completed, BLACKLIST}
  │ ◄─────────────────── close ──┤
  │                               │

Router Auditor v2.0 — 85 detectors — Back to app