Skip to Content
API Reference

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} until status = "completed", or
  • Receive a webhook on callback_url (HMAC-signed if you set callback_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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
⚠️

Treat 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

FieldTypeRequiredDescription
repo_urlstringGitHub 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
refstringBranch, tag, or commit SHA. Default: repo’s default branch
languagestringpython, javascript, typescript, go, java, ruby, php, or auto (default — Modal auto-detects)
github_tokenstringGitHub PAT for private repos. Optional if the Vouch GitHub App is installed on the repo and the calling user is linked
callback_urlstringVouch 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_secretstringHMAC-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 -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-repo uploads)

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:

FieldTypeDescription
titlestringShort, human-readable title
severitystringCRITICAL, HIGH, MEDIUM, or LOW
filestringPath relative to repo root
lineint | nullSource line of the finding (0 if synthetic, e.g. dependency-level)
sourcestringstatic (Semgrep / Gitleaks / npm-audit / pip-audit) or ai_hunter (LLM-found)
descriptionstring1–2 sentences explaining the vulnerability and exploit path
how_to_fixstringConcrete remediation steps
fixed_code_snippetstring | nullSuggested 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

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", 403

Delivery 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

StatusMeaning
401 UnauthorizedMissing or invalid X-API-Key
402 Payment RequiredMonthly Core-Scan quota exhausted. Upgrade plan or buy credits in the dashboard
422 Unprocessable EntityInvalid repo_url, malformed callback_url, or schema mismatch
429 Too Many RequestsRate limit (5/min) hit. Back off
500 Internal Server ErrorServer-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:

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:

  1. No webhook retries. One POST attempt, 10s timeout. Polling is the safety net.
  2. No idempotency keys. A duplicate POST = a duplicate scan = double credits charged.
  3. No pagination on GET /scans (the list endpoint). Heavy users will see large payloads.
  4. 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.

Last updated on