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.

What a key can do. API keys act with 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.
A key stops working if the user who created it leaves the organisation — a credential does not outlive its owner's access. Reissue the key under a current member.

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:

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 & pathEffect
POST /api/scans/<id>/pausePause a running scan (worker honours it at safe points).
POST /api/scans/<id>/resumeResume a paused or stopped scan.
POST /api/scans/<id>/stopStop early; partial findings are kept.
POST /api/scans/<id>/cancelDiscard the scan (partial findings deleted).
POST /api/scans/<id>/rescanRe-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
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"
ToolWhat it does
findings_for_fileFindings in one file from the repository's latest completed scan (repository by id, name or URL; repo-relative or absolute path)
list_repositoriesRepositories with latest scan status and severity counts; optional name search
list_scansScans newest first; filter by repository, status or search; paged
get_scanStatus, progress, severity counts, build-gate result, failure reason and the Incomplete scan note
list_findingsA scan's findings; filter by severity, scanner, status, file path; false positives hidden by default
get_findingDescription, location, snippet, CWE / CVE / OWASP, impact, reproduction steps, remediation
start_scanQueue a scan of a repository (id, name or URL) or a new Git URL; scan type and branch (Developer)
rescanRepeat an earlier scan (Developer)
update_finding_statusSet OPEN / IN_PROGRESS / RESOLVED with a note; severity totals are recounted (Developer)
Tools act as the API key's principal (see Authentication): the key's organization only, at most the 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.