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 plugins, 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.
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,SAST_ONLY,SCA_ONLY,SECRETS_ONLY,CONTAINER_ONLY,DAST_ONLY,INCREMENTALrepoUrl— 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 seven artifact types stored in MinIO:
SARIF, SBOM_CYCLONEDX, SBOM_SPDX,
SCAN_LOG, CONTAINER_REPORT, DAST_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/dast # dapper report (when DAST ran)
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, dastTargetUrl
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
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"] } }
// 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"
// Dapper (DAST)
{ "kind": "DAST", "name": "Dapper",
"config": { "endpoint": "http://dapper:8080", "apiKey": "…" } }
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": "…" }
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 plugin endpoint
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-Hub-Signature-256: sha256=… # validated against GITHUB_WEBHOOK_SECRET
POST /api/webhooks/gitlab
X-Gitlab-Token: … # validated against GITLAB_WEBHOOK_SECRET
Both endpoints accept push + PR/MR events and queue an
INCREMENTAL scan against the head commit.