Integrations and API
Send Casefile events to Slack, GitHub, Jira or your own endpoint, and read your programs' records through the REST API.
You manage integrations at Integrations & API in the account menu. Owners and admins with two-factor authentication can create, test, pause and delete them. Everyone else on the team can see the list and recent deliveries.
What leaves Proofline, and what doesn't
Integrations notify you that something happened, not what the vulnerability is. Each delivery carries:
- the Casefile code, status, severity and a link back to the case
- the program slug and name
- the event type and when it happened
Report bodies, proofs of concept, evidence, messages and decision reasoning never leave Proofline. Report titles are off by default because a title can describe an unfixed issue. Turn on Include report titles only for destinations with restricted access, such as a private channel or a private repository.
Delivery guarantees
- Never blocks a decision. Deliveries are written to an outbox in the same database transaction as the case event and sent by the scheduled job. If Slack is down, triage still works.
- At-least-once. Failed deliveries retry with exponential backoff (1, 2, 4, 8… minutes, capped at 6 hours) up to 8 attempts, then show as failed on the Integrations page. Use the delivery
idto de-duplicate. - In order per Casefile for trackers. GitHub and Jira deliveries for the same Casefile are sent in order, so a retry never opens a second issue.
- SSRF-safe. Endpoints must use HTTPS. Private, loopback, link-local and cloud-metadata addresses are refused when you save the integration and again when each connection opens, which defeats DNS rebinding. Redirects aren't followed, and each request times out after 10 seconds.
Use Send test to deliver a PING immediately. Pause holds deliveries without losing them. They're sent in order when you resume, and anything still undelivered after 7 days expires. Delete removes the integration and its delivery history.
Events
| Event | Default |
|---|---|
REPORT_SUBMITTED: new report |
on |
RESEARCHER_RESPONDED: researcher answered an info request |
on |
REPORT_ACCEPTED |
on |
SEVERITY_CHANGED |
off |
CLOSURE_PROPOSED |
off |
REBUTTAL_SUBMITTED: final rebuttal received, 48h clock running |
on |
MEDIATION_REQUESTED / MEDIATION_DECISION |
on |
RETEST_PASSED / RETEST_FAILED |
on |
REMEDIATION_DECLINED: Won't Fix recorded (still a payout obligation) |
off |
PAYOUT_SENT |
on |
MESSAGE_POSTED: new message in the case room |
off |
Slack
Create an incoming webhook for the channel and paste its https://hooks.slack.com/services/… URL. Each event posts one line with an Open Casefile button.
GitHub Issues
Use a private repository and a fine-grained personal access token scoped to that repository only, with Issues: read & write permission. Enter the repository as owner/repo. The first event for a Casefile opens an issue labelled security and proofline, and later events are added as comments. The issue tracks workflow only and links back to the case for details.
Jira Cloud
Enter your site (https://acme.atlassian.net), credentials as email:api-token (create a token at id.atlassian.com), and the project key. Add :IssueType to choose an issue type, for example SEC:Bug. The default is Task. As with GitHub, there's one ticket per Casefile, and later events become comments.
Signed webhooks
A generic webhook receives a POST with a JSON body:
{
"id": "cm3x…",
"type": "REPORT_ACCEPTED",
"occurredAt": "2026-09-30T18:04:11.000Z",
"casefile": { "code": "CF-1004", "status": "ACCEPTED", "severity": "HIGH", "url": "https://…/cases/CF-1004" },
"program": { "slug": "helix", "name": "Helix Protocol" }
}
Each request also carries these headers:
Proofline-Event: the event typeProofline-Delivery: the delivery id (stable across retries)Proofline-Signature:t=<unix seconds>,v1=<hex HMAC-SHA256>
The signing secret (whsec_…) is shown once when you create the webhook. Proofline keeps only an encrypted copy. To verify a delivery, compute the HMAC over "<t>.<raw body>", compare it in constant time, and reject old timestamps:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyProofline(secret, header, rawBody, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1 ?? "", "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}
Verify against the raw request body, before any JSON parsing. Respond with any 2xx within 10 seconds. Anything else is retried.
REST API
The API is read-only and scoped to your organization. Create a key under API keys. It's shown once and stored only as a hash. Send it as a bearer token:
curl -H "Authorization: Bearer pl_live_…" https://<your-proofline-host>/api/v1/casefiles?status=PAYOUT_PENDING
GET /api/v1/casefiles
Lists Casefiles across your programs, newest first.
| Query | Meaning |
|---|---|
status |
Filter by status, e.g. TRIAGE, ACCEPTED, PAYOUT_PENDING |
program |
Filter by program slug |
limit |
1–200 (default 50) |
Each item has code, program, status, severity, requestedSeverity, outcome, remediation, disclosure, submittedAt, updatedAt, proofline (events, head hash) and url.
GET /api/v1/casefiles/{code}
Returns one Casefile with:
- the rulebook it's judged under (
versionandfingerprint) - the content hash and lifecycle timestamps
- every recorded decision (kind, verdict, severity, cited rule, and whether it was rebutted)
- the current reward and payouts, including on-chain transaction hashes
Codes that belong to other organizations return 404, the same as codes that don't exist.
Errors and limits
| Status | Meaning |
|---|---|
401 unauthorized |
Missing, invalid or revoked key |
404 not_found |
No such Casefile in your organization |
429 rate_limited |
More than 120 requests per minute per organization. Retry after 60 seconds. |
Responses are never cached (Cache-Control: no-store). You can compare proofline.head with the hash shown on the case page to confirm you're looking at the same record.