API Reference
The Vouch Scan API lets you trigger a full security scan of any GitHub repository from your own scripts, CI pipelines, or integrations — no ZIP uploads, no manual steps. The same pipeline that powers our GitHub App and dashboard runs behind every API call: static scanners (Semgrep, Gitleaks, npm-audit, pip-audit), an Endpoint-Index, the AI Hunter, RAG, the AI Validator, and the Formatter.
Base URL: https://api.vouch-secure.com
Auth: X-API-Key header
Format: JSON in / JSON out
TL;DR
curl -X POST https://api.vouch-secure.com/scan-repo-url \
-H "Content-Type: application/json" \
-H "X-API-Key: $VOUCH_API_KEY" \
-d '{
"repo_url": "https://github.com/owner/repo",
"ref": "main",
"callback_url": "https://my-app.com/vouch-webhook",
"callback_secret": "shared-secret-for-hmac"
}'You receive a scan_id immediately. Then either:
- Poll
GET /scans/{scan_id}untilstatus = "completed", or - Receive a webhook on
callback_url(HMAC-signed if you setcallback_secret).
Authentication
Every request must include an X-API-Key header. Generate one in your
Developer Portal — one
key per account.
X-API-Key: vouch_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxTreat the key like a password. It grants full access to your scan quota and returns scan results that may include sensitive code excerpts. If leaked, regenerate it immediately from the dashboard — the old key is revoked the moment a new one is issued.
POST /scan-repo-url
Trigger a full repository scan by URL.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
repo_url | string | ✓ | GitHub URL. Accepts: https://github.com/owner/repo, https://github.com/owner/repo.git, https://github.com/owner/repo/tree/branch, git@github.com:owner/repo.git |
ref | string | – | Branch, tag, or commit SHA. Default: repo’s default branch |
language | string | – | python, javascript, typescript, go, java, ruby, php, or auto (default — Modal auto-detects) |
github_token | string | – | GitHub PAT for private repos. Optional if the Vouch GitHub App is installed on the repo and the calling user is linked |
callback_url | string | – | Vouch POSTs the final scan result here. Must be http:// or https://. SSRF-protected: localhost, RFC1918 ranges, link-local, and cloud metadata IPs are rejected |
callback_secret | string | – | HMAC-SHA256 secret. When set, callback body is signed with header X-Vouch-Signature: sha256=<hex> |
Response (immediate, 202 Accepted)
{
"scan_id": "0910bbaa-74e4-4770-af7f-4fc77e7bc944",
"status": "processing",
"message": "Scan started. Poll GET /scans/{scan_id} for results, or wait for callback if callback_url was set."
}Examples
curl
curl -X POST https://api.vouch-secure.com/scan-repo-url \
-H "Content-Type: application/json" \
-H "X-API-Key: $VOUCH_API_KEY" \
-d '{"repo_url": "https://github.com/juice-shop/juice-shop"}'Limits
- Rate limit: 5 requests / minute / IP
- Quota: 2 Core-Scan credits per call (matches large
/scan-repouploads)
GET /scans/{scan_id}
Poll for scan results. Use this when you don’t set callback_url, or as a
fallback if your webhook endpoint missed the delivery.
Response
While processing:
{ "scan_id": "...", "status": "processing", "score": 0, "issues": [], "filtered_findings": [] }When complete:
{
"scan_id": "0910bbaa-74e4-4770-af7f-4fc77e7bc944",
"status": "completed",
"score": 23,
"summary": "A significant number of critical and high-severity vulnerabilities were identified...",
"issues": [
{
"title": "SQL Injection in Login Endpoint",
"severity": "CRITICAL",
"file": "juice-shop-juice-shop-53a4c42/data/static/codefixes/loginAdminChallenge_1.ts",
"line": 6,
"source": "ai_hunter",
"description": "User-supplied email/password are concatenated directly into a raw SQL query, allowing an attacker to bypass authentication...",
"how_to_fix": "Use parameterized queries via Sequelize's `where` operator instead of string interpolation.",
"fixed_code_snippet": "..."
}
],
"filtered_findings": [
{
"title": "Possible insecure regex",
"severity": "LOW",
"file": "src/utils/parser.ts",
"line": 84,
"rule_id": "javascript.lang.security.audit.detect-non-literal-regexp",
"reason": "Pattern is constant at runtime — no user input"
}
],
"credits_used": 1,
"github_owner": "juice-shop",
"github_repo": "juice-shop"
}Polling pattern
Trigger the scan
POST to /scan-repo-url, capture scan_id.
Poll every 15–30 seconds
GET /scans/{scan_id}. A typical full scan takes 3–6 minutes
depending on repo size. Don’t poll faster than every 5s — you’ll just hit
the rate limit on this endpoint too.
Stop on terminal state
status reaches completed (success) or failed (with summary
explaining why).
Issue schema
Every entry in the issues[] array follows the same shape:
| Field | Type | Description |
|---|---|---|
title | string | Short, human-readable title |
severity | string | CRITICAL, HIGH, MEDIUM, or LOW |
file | string | Path relative to repo root |
line | int | null | Source line of the finding (0 if synthetic, e.g. dependency-level) |
source | string | static (Semgrep / Gitleaks / npm-audit / pip-audit) or ai_hunter (LLM-found) |
description | string | 1–2 sentences explaining the vulnerability and exploit path |
how_to_fix | string | Concrete remediation steps |
fixed_code_snippet | string | null | Suggested patch (when available) |
filtered_findings[] uses the same shape plus a reason field explaining why
the AI Validator dismissed the finding as a false positive — useful for
audit trails.
Webhook callbacks
When you set callback_url on the scan request, Vouch POSTs the final result
to that URL once the scan completes.
Request format
POST <callback_url>
Content-Type: application/json
User-Agent: Vouch-Webhook/1.0
X-Vouch-Signature: sha256=<hex> # only if callback_secret was set
{
"event": "scan.completed",
"scan_id": "0910bbaa-74e4-4770-af7f-4fc77e7bc944",
"score": 23,
"summary": "...",
"issues": [ { "title": "...", "severity": "...", "file": "...", "line": 6, ... } ],
"language": "javascript",
"repo_name": "juice-shop/juice-shop"
}Verify the signature
Python
import hmac, hashlib
def verify_vouch_signature(body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)
# In your webhook handler:
raw_body = request.get_data() # raw bytes, NOT parsed JSON
sig = request.headers.get("X-Vouch-Signature", "")
if not verify_vouch_signature(raw_body, sig, SECRET):
return "invalid signature", 403Delivery guarantees
At-most-once delivery. Vouch attempts the POST exactly once with a 10-second
timeout. There is no retry mechanism in v1. If your endpoint is unreachable,
the scan result is still available via GET /scans/{scan_id} indefinitely —
treat the webhook as a convenience, not a guarantee.
GitHub token resolution
When the target repo is private, Vouch tries these in order:
Explicit github_token in the request
A PAT you supply in the request body wins over everything else. Useful for ad-hoc scans of private repos owned by accounts that haven’t installed the Vouch GitHub App.
Vouch GitHub App installation token
If you’ve installed the Vouch GitHub App on the target repo and the calling API key belongs to a user linked to that installation, Vouch automatically obtains a 1-hour-valid installation token. Recommended for production — no token storage on your side.
Anonymous (public repos only)
For public repositories, no token is needed. GitHub’s anonymous rate limit applies (60 requests/hour per IP).
Why no PAT storage? Vouch deliberately doesn’t persist GitHub tokens in
the user account. A PAT with repo scope grants full read access to all your
private code — storing it without enterprise-grade encryption-at-rest would be
reckless. The GitHub App pattern (1-hour scoped tokens, fetched on demand) is
the secure default.
Errors
| Status | Meaning |
|---|---|
401 Unauthorized | Missing or invalid X-API-Key |
402 Payment Required | Monthly Core-Scan quota exhausted. Upgrade plan or buy credits in the dashboard |
422 Unprocessable Entity | Invalid repo_url, malformed callback_url, or schema mismatch |
429 Too Many Requests | Rate limit (5/min) hit. Back off |
500 Internal Server Error | Server-side failure. Contact support with your scan_id |
When a scan fails during processing (not at submission), status becomes
"failed" and the summary field contains the error message. Common causes:
GitHub returned 404 (bad ref) or 401 (private repo without token), Modal
container hit its memory ceiling, or the AI provider was rate-limited.
Self-test cheat-sheet
# 1. Get an API key in the dashboard
export VOUCH_KEY="vouch_..."
export VOUCH_URL="https://api.vouch-secure.com"
# 2. Public repo (no github_token needed)
curl -s -X POST "$VOUCH_URL/scan-repo-url" \
-H "X-API-Key: $VOUCH_KEY" \
-H "Content-Type: application/json" \
-d '{"repo_url": "https://github.com/juice-shop/juice-shop"}'
# → {"scan_id": "abc-123", "status": "processing", ...}
# 3. Poll
SCAN_ID="abc-123"
watch -n 15 "curl -s '$VOUCH_URL/scans/$SCAN_ID' \
-H 'X-API-Key: $VOUCH_KEY' | jq '.status, .score, (.issues | length)'"
# 4. With webhook callback
curl -s -X POST "$VOUCH_URL/scan-repo-url" \
-H "X-API-Key: $VOUCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"repo_url": "https://github.com/owner/repo",
"callback_url": "https://my-app.com/vouch",
"callback_secret": "supersecret123"
}'
# 5. Private repo via PAT
curl -s -X POST "$VOUCH_URL/scan-repo-url" \
-H "X-API-Key: $VOUCH_KEY" \
-H "Content-Type: application/json" \
-d '{
"repo_url": "https://github.com/owner/private-repo",
"github_token": "ghp_..."
}'OpenAPI / Swagger
FastAPI exposes auto-generated interactive docs:
- Swagger UI:
/docs - ReDoc:
/redoc - OpenAPI JSON:
/openapi.json
Use these to discover all endpoints (including admin/diagnostic ones not covered here) and to generate client SDKs for any language.
Known limits (v1)
These are documented honestly so you know what to plan around:
- No webhook retries. One POST attempt, 10s timeout. Polling is the safety net.
- No idempotency keys. A duplicate POST = a duplicate scan = double credits charged.
- No pagination on
GET /scans(the list endpoint). Heavy users will see large payloads. - No first-party SDKs for Python / JS / Go yet. Use raw HTTP or generate from OpenAPI.
If any of these blocks your use-case, open an issue or reach out — Phase 3 priorities are driven by real user pain.