API reference
Pepper exposes a JSON REST API rooted at /api. Endpoints accept
two authentication mechanisms: session cookies for the web UI, and bearer
API keys for everything else (CI, IDE assistants via MCP, pre-commit, webhooks).
Authentication
For machine-to-machine, pass an API key in the Authorization header:
Authorization: Bearer ppr_xxxxxxxxxxxxxxxxxxxxxxxx
Create keys in Settings → API Keys or via
POST /api/apikeys. Keys are shown once and stored hashed
(SHA-256). Revoke with DELETE /api/apikeys/<id>.
All organisation-scoped endpoints return 403 when the caller is
not a member of the org the resource belongs to.
DEVELOPER
permissions at most, whoever created them — enough to create scans, read
findings and download artifacts, but never enough to change team membership,
delete scans or edit policy. A key issued by a user with a lower role keeps
that lower role. Actions are attributed to the user who created the key, so
they appear under that name in the audit log.
Scans
Create a scan
POST /api/scans — multipart or JSON. The most common form is a
tarball upload:
curl -X POST $PEPPER_API_URL/api/scans \
-H "Authorization: Bearer $PEPPER_API_KEY" \
-F "source=@source.tar.gz" \
-F "scanType=FULL" \
-F "branch=main" \
-F "commitSha=$(git rev-parse HEAD)"
# → { "scanId": "cl…", "projectId": "cl…", "status": "QUEUED" }
Fields:
scanType—FULL,INCREMENTAL,SAST_ONLY,SCA_ONLY,SECRETS_ONLY,IAC_ONLY,ZERO_DAY_ONLY,CONTAINER_ONLY,K8S_ONLYrepoUrl— clone from a public Git HTTPS URL instead of uploadsvnUrl+svnRevision+svnUsername/svnPassword— Subversion sourceprojectId— attach to an existing project; if omitted, Pepper creates a project from the repo / archive namebranch,commitSha,baseSha— labels + base for INCREMENTAL diff
Read a scan
GET /api/scans/<scanId>
# → {
# "id": "…", "status": "RUNNING|COMPLETED|FAILED|…",
# "gateResult": "PASSED|FAILED|PENDING",
# "criticalCount": 0, "highCount": 3, ...,
# "scannerProgress": { "SAST_LLM": { "status": "RUNNING", "filesCompleted": 12, "filesTotal": 88 } }
# }
List scans
GET /api/scans?projectId=<id>&status=COMPLETED&limit=20
Lifecycle
| Method & path | Effect |
|---|---|
POST /api/scans/<id>/pause | Pause a running scan (worker honours it at safe points). |
POST /api/scans/<id>/resume | Resume a paused or stopped scan. |
POST /api/scans/<id>/stop | Stop early; partial findings are kept. |
POST /api/scans/<id>/cancel | Discard the scan (partial findings deleted). |
POST /api/scans/<id>/rescan | Re-queue the same source. |
Artifacts & SBOMs
Every scan can produce up to six artifact types stored in MinIO:
SARIF, SBOM_CYCLONEDX, SBOM_SPDX,
SCAN_LOG, CONTAINER_REPORT,
SIGNATURE.
GET /api/scans/<scanId>/artifacts
# → { "artifacts": [ { "type": "SBOM_CYCLONEDX", "size": 12453, "downloadPath": "..." }, … ] }
GET /api/scans/<scanId>/artifacts/cyclonedx # CycloneDX 1.5 JSON
GET /api/scans/<scanId>/artifacts/spdx # SPDX 2.3 JSON
GET /api/scans/<scanId>/artifacts/sarif # SARIF v2 (when written)
GET /api/scans/<scanId>/artifacts/container # Trivy raw report (when run)
GET /api/scans/<scanId>/artifacts/signature # cosign / RSA signature bundle
GET /api/scans/<scanId>/artifacts/log # JSON log lines
Findings
GET /api/scans/<scanId>/findings?severity=HIGH&status=OPEN&limit=100
# → { "findings": [ { "id": …, "severity": "HIGH", "title": …, "filePath": …, "startLine": … }, … ] }
PATCH /api/findings/<findingId>
{ "status": "RESOLVED" | "FALSE_POSITIVE" | "ACCEPTED_RISK" | "IN_PROGRESS" | "OPEN",
"statusNote": "fixed in #1234" }
POST /api/findings/bulk
{ "ids": ["…","…"], "status": "FALSE_POSITIVE" }
POST /api/scans/<scanId>/findings/<findingId>/suggest-fix
# → { "diff": "--- a/foo.ts\n+++ b/foo.ts\n@@ …" }
POST /api/scans/<scanId>/findings/<findingId>/open-pr
{ "branch": "pepper/fix-cl123" }
# → { "url": "https://github.com/acme/foo/pull/42" }
GET /api/scans/<scanId>/findings/export?format=csv | html | sarif
Projects
GET /api/projects
POST /api/projects # create
GET /api/projects/<projectId>
PATCH /api/projects/<projectId> # name, description, repoUrl,
# defaultBranch
DELETE /api/projects/<projectId>
POST /api/projects/<projectId>/schedule # nightly / weekly cron
Integrations
GET /api/integrations # list all integrations
POST /api/integrations # upsert
DELETE /api/integrations/<id>
POST /api/integrations/test # dry-run with provided config
# (Azure Boards: validates only, creates nothing)
GET /api/findings/<id>/integrations # configured destinations + linked work items
POST /api/findings/<id>/integrations # { "channel": "slack" | "jira" | "azure-boards" | "all" }
Body shape
// Slack
{ "kind": "SLACK", "name": "Security channel",
"config": { "webhookUrl": "https://hooks.slack.com/services/…",
"channel": "#security",
"notifyOn": ["scan_complete","gate_failed"] } }
// Jira
{ "kind": "JIRA", "name": "Security (SEC)",
"config": { "baseUrl": "https://acme.atlassian.net",
"email": "bot@acme.com", "apiToken": "…",
"projectKey": "SEC", "issueType": "Bug",
"openForSeverities": ["CRITICAL","HIGH"] } }
// Azure Boards (serverUrl only for Azure DevOps Server; pat optional when
// the org has an Azure DevOps connection; project blank = repo's own project)
{ "kind": "AZURE_BOARDS", "name": "Azure Boards (Payments)",
"config": { "serverUrl": "https://tfs.company.com/tfs",
"organization": "DefaultCollection", "project": "Payments",
"pat": "…", "workItemType": "Bug",
"areaPath": "Payments\\AppSec", "iterationPath": "Payments\\Sprint 12",
"assignedTo": "appsec@company.com", "tags": ["appsec"],
"autoCreateSeverities": ["CRITICAL","HIGH"],
"fixedState": "Resolved", "apiVersion": "6.0" } }
// SIEM
{ "kind": "SIEM", "name": "Splunk HEC",
"config": { "endpoint": "https://splunk.example.com:8088/services/collector/raw",
"format": "cef" | "leef" | "json",
"apiKey": "Splunk <token>" } }
// Syslog target also supported: "udp://siem.example.com:514"
API keys
GET /api/apikeys # list (no plaintext)
POST /api/apikeys # { "name": "github-actions-prod",
# "expiresAt": "2027-01-01T00:00:00Z" }
# → returns "plaintext" ONCE
DELETE /api/apikeys/<id>
Audit log
GET /api/audit-log?action=scan.queued&userId=…&limit=50&cursor=…
# → { "entries": [ { "id": …, "action": "scan.queued", "resource": "scan",
# "resourceId": "…", "user": { "id": …, "email": … },
# "ipAddress": "…", "details": {...}, "createdAt": "…" } ],
# "nextCursor": "…" }
# Filters: action, resource, userId, from, to (ISO timestamp or YYYY-MM-DD;
# a date-only "to" includes the whole day). Security or Admin role.
GET /api/audit-log/export?format=csv|json&from=2026-07-01&to=2026-09-30
# → streamed download of every matching entry (same filters as above)
# CSV columns: timestamp,action,resource,resourceId,userId,userEmail,
# userName,ipAddress,details
GET /api/audit-log/retention
PUT /api/audit-log/retention # Admin: { "retentionDays": 90 | null | 0,
# "archive": true }
# null = instance default, 0 = keep forever
GET /api/audit-log/archives # archived (purged) entries
GET /api/audit-log/archives/download?key=… # gzipped JSON lines
Trends
GET /api/dashboard/trends?days=30
# → {
# "days": 30,
# "series": [ { "date": "2026-05-01", "critical": 0, "high": 2,
# "medium": 5, "low": 8, "scans": 3, "gateFailed": 1 }, … ],
# "mttr": { "CRITICAL": { "count": 4, "meanHours": 12.5 }, "HIGH": {…} }
# }
Pre-commit
GET /api/precommit/install.sh # installer script (no auth)
POST /api/precommit/scan # API-key auth
{ "files": [ { "path": "src/foo.ts", "content": "…" }, … ],
"failOn": ["CRITICAL","HIGH"] }
# → { "findings": [ … ], "summary": {…}, "block": true|false }
IDE assistants (MCP server)
Pepper is a Model Context Protocol (MCP) server, so the AI assistant in an IDE can read and triage Pepper findings in the editor. Any assistant that supports remote MCP servers over Streamable HTTP with a custom header can connect — for example GitHub Copilot in VS Code, Visual Studio, JetBrains IDEs (IntelliJ IDEA, Android Studio, …) and Eclipse, JetBrains AI Assistant, Cursor, Claude Code and Continue. Other Eclipse-based IDEs such as e² studio, and editors such as NetBeans, can use it through an MCP-capable assistant plugin where one is available for them. Settings → Integrations → IDE has ready-to-copy configuration for each assistant, filled in with this instance's URL.
URL https://<your-pepper-host>/api/mcp
Transport Streamable HTTP (stateless, JSON responses; POST only)
Auth Authorization: Bearer ppr_xxx # a Pepper API key
For example, in VS Code (.vscode/mcp.json; VS Code prompts for the key and stores it securely):
{
"servers": {
"pepper": {
"type": "http",
"url": "https://<your-pepper-host>/api/mcp",
"headers": { "Authorization": "Bearer ${input:pepper-api-key}" }
}
},
"inputs": [
{ "type": "promptString", "id": "pepper-api-key", "description": "Pepper API key", "password": true }
]
}
Configuration file names and menus differ between assistants and versions; where they do, follow your assistant's MCP settings with the URL and header above. Claude Code:
claude mcp add --transport http pepper https://<your-pepper-host>/api/mcp \
--header "Authorization: Bearer ppr_xxx"
| Tool | What it does |
|---|---|
findings_for_file | Findings in one file from the repository's latest completed scan (repository by id, name or URL; repo-relative or absolute path) |
list_repositories | Repositories with latest scan status and severity counts; optional name search |
list_scans | Scans newest first; filter by repository, status or search; paged |
get_scan | Status, progress, severity counts, build-gate result, failure reason and the Incomplete scan note |
list_findings | A scan's findings; filter by severity, scanner, status, file path; false positives hidden by default |
get_finding | Description, location, snippet, CWE / CVE / OWASP, impact, reproduction steps, remediation |
start_scan | Queue a scan of a repository (id, name or URL) or a new Git URL; scan type and branch (Developer) |
rescan | Repeat an earlier scan (Developer) |
update_finding_status | Set OPEN / IN_PROGRESS / RESOLVED with a note; severity totals are recounted (Developer) |
DEVELOPER role. Marking a finding
FALSE_POSITIVE or ACCEPTED_RISK requires the Security role,
so it is refused over MCP; do it in the web UI. Secrets are returned masked and
credentials are removed from repository URLs. Starting scans and status changes
are written to the audit log with "via": "mcp". The server runs inside
the Pepper API and contacts no external service.
Check connectivity from a terminal:
curl -s https://<your-pepper-host>/api/mcp \
-H "Authorization: Bearer ppr_xxx" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Findings REST endpoint
For scripts and custom editor plugins:
GET /api/ide/findings?projectId=<id>&filePath=src/foo.ts&minSeverity=HIGH
Authorization: Bearer ppr_xxx
# → { "project": {…}, "scan": {…},
# "findings": [ { "id": …, "severity": "HIGH", "title": …,
# "filePath": "src/foo.ts", "startLine": 42, … } ] }
CI/CD templates
GET /api/cicd-templates/github # GitHub Actions workflow
GET /api/cicd-templates/gitlab # .gitlab-ci.yml fragment
GET /api/cicd-templates/jenkins # Jenkinsfile
Webhooks (inbound)
POST /api/webhooks/github
X-GitHub-Event: pull_request | push
X-Hub-Signature-256: sha256=…
POST /api/webhooks/bitbucket
X-Event-Key: pullrequest:created | pullrequest:updated | repo:push
X-Hub-Signature: sha256=…
POST /api/webhooks/azure-devops
eventType: git.pullrequest.created | git.pullrequest.updated | git.push
Authorization: Basic base64(:password)
POST /api/webhooks/gitlab
X-Gitlab-Token: …
PR/MR events queue INCREMENTAL scans (git diff — changed files
only for SAST/secrets; SCA when manifests change). Push to the project
default branch queues SAST_ONLY unless
GITHUB_WEBHOOK_MAIN_SCAN_TYPE=FULL.