PullLight API
A read-only JSON API for piping catches, repo stats, and weekly digests into DataDog, Grafana, Linear, or any internal dashboard.
Overview
All API endpoints are under /api/v1/. Every request must include a valid Bearer token. Tokens are scoped to an installation and are read-only — they cannot approve reviews, post comments, or modify any data.
Base URL: https://pulllight.polsia.app
Authentication
Generate tokens at /settings/api-tokens?installation_id=<n>. Tokens are plk_-prefixed and 68 characters long. The plaintext is only shown once — store it in a secret manager.
Pass the token in the Authorization header:
curl -H "Authorization: Bearer plk_your_token_here" \ https://pulllight.polsia.app/api/v1/catches
A missing or invalid token returns 401 Unauthorized. A revoked token also returns 401 — revocation is immediate.
Rate limits
60 requests per minute per token. Limits reset at the top of each minute. Response headers tell you where you stand:
| HEADER | MEANING |
|---|---|
X-RateLimit-Limit | Always 60 |
X-RateLimit-Remaining | Requests left this minute |
X-RateLimit-Reset | Unix timestamp when window resets |
Retry-After | Seconds to wait (only on 429) |
Endpoints
Returns a paginated list of bugs PullLight has caught across all public repos. Mirrors the /catches feed.
Query parameters
| PARAM | TYPE | DESCRIPTION |
|---|---|---|
| severity | string | Filter: high, medium, or low |
| category | string | Filter by category slug, e.g. sql-injection |
| repo | string | Filter by owner/name, e.g. acme/api |
| since | ISO date | Return only catches published after this date |
| limit | int | Max results (default 50, max 200) |
| offset | int | Skip N results for pagination |
Example
curl -H "Authorization: Bearer plk_..." \ "https://pulllight.polsia.app/api/v1/catches?severity=high&limit=10"
{
"data": [
{
"id": 42,
"slug": "a3f9d1c2",
"repo": "acme/api",
"severity": "high",
"category": "sql-injection",
"title": "[HIGH] sql injection catch",
"summary": "Unsanitized user input passed directly to a raw SQL query...",
"created_at": "2026-06-10T14:22:00Z",
"permalink": "https://pulllight.polsia.app/catches/a3f9d1c2"
}
],
"pagination": {
"total": 148,
"limit": 10,
"offset": 0,
"has_more": true
}
}
Returns total PRs reviewed, total findings, severity breakdown, top 5 categories, and a 12-week sparkline. Mirrors /repos/:owner/:repo.
Example
curl -H "Authorization: Bearer plk_..." \ https://pulllight.polsia.app/api/v1/repos/acme/api/stats
{
"repo": "acme/api",
"total_prs_reviewed": 37,
"total_findings": 84,
"severity_breakdown": { "high": 12, "medium": 31, "low": 41 },
"top_categories": [
{ "category": "missing-await", "count": 18 },
{ "category": "null-deref", "count": 14 }
],
"sparkline": [
{ "week_label": "04/07", "prs": 3, "findings": 7 }
],
"report_url": "https://pulllight.polsia.app/repos/acme/api"
}
Returns last-7-day stats in the same shape as the weekly digest email: PRs reviewed, findings, severity, ROI math, and the top catch. The token must belong to the requested installation — cross-installation reads return 403.
Example
curl -H "Authorization: Bearer plk_..." \ https://pulllight.polsia.app/api/v1/installations/12345/digest
{
"installation_id": 12345,
"period": "last_7_days",
"stats": {
"prs_reviewed": 8,
"total_findings": 19,
"approved_findings": 14,
"pending_queue": 2,
"severity": { "high": 3, "medium": 7, "low": 4 },
"top_categories": ["missing await", "null deref"],
"top_repo": { "owner": "acme", "name": "api", "prCount": 5 }
},
"all_time": { "total_prs": 37, "total_bugs": 84, "approval_rate_pct": 74, "avg_hours_to_review": 1.4 },
"roi": {
"estimated_hours_saved": 2.3,
"estimated_dollars_saved": 350,
"assumption": "$150/hr, 10 min per finding reviewed"
},
"top_catch": { "summary": "...", "pr_number": 142 }
}
PR Commands
Type @pulllight <command> in any PR comment thread to trigger commands inline — no browser tab required. PullLight will acknowledge with a 👀 reaction immediately, then post results as a follow-up comment.
No setup required. Commands work on any PR in repos where PullLight is installed. The bot ignores its own comments to prevent loops.
Kicks off a fresh full analysis on the PR's latest commit. Creates a new Check Run, runs Claude, queues findings for human approval, and posts an updated summary comment. Use it after a fix push or a force-push to confirm the issues are resolved.
Example
# In any PR comment:
@pulllight recheck
# PullLight replies: > @alice — re-running analysis on the latest commit… I'll post a new summary when done. 🔍 # After analysis (async): > @alice — recheck complete. Claude found 2 findings on the latest commit. > Review in PullLight → https://pulllight.polsia.app/reviews/42
Marks the specified review as rejected. It disappears from /reviews and will never be posted to GitHub. The review ID is the number shown at the top of each review card in the queue.
Example
# Ignore review #47 (false positive, already fixed, etc.):
@pulllight ignore 47
# PullLight replies: > @alice — review #47 marked as ignored. 👋 > It's been removed from the queue and won't post to GitHub.
Re-prompts Claude with the finding body and asks for: the bug class, why it matters in production, and a concrete before/after fix patch. The explanation is posted as a new comment — useful when a teammate asks "why does this matter?"
Example
# Ask Claude to explain the findings in review #47:
@pulllight explain 47
# PullLight replies with a full explanation: **PullLight Explanation** · review #47 > @alice asked for a deeper look at this finding: **Finding:** `req.user` is accessed without a null check on line 42... --- **Bug class:** Null dereference / authentication bypass **Why it matters:** If the middleware fails silently and passes `req.user = undefined`... **Fix:** ```diff - const userId = req.user.id; + const userId = req.user?.id; + if (!userId) return res.status(401).json({ error: 'Unauthenticated' }); ```
Posts a formatted command reference table as a PR comment. Safe to run at any time — no side effects.
Example
@pulllight help
Error codes
| STATUS | ERROR CODE | MEANING |
|---|---|---|
| 400 | invalid_param | A query param or path segment is malformed |
| 401 | unauthorized | Missing, invalid, or revoked token |
| 403 | forbidden | Token does not have access to this resource (wrong installation) |
| 429 | rate_limited | 60 req/min exceeded — wait for Retry-After seconds |
| 500 | internal_error | Server error — safe to retry with backoff |
Error responses always include { "error": "code", "message": "..." }.
Full examples
Poll for new critical catches (DataDog / alerting)
# Fetch critical catches in the last 24h curl -s \ -H "Authorization: Bearer plk_your_token" \ "https://pulllight.polsia.app/api/v1/catches?severity=high&since=$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)" \ | jq '.data[] | {slug, repo, title, created_at}'
Weekly digest into Slack (cron)
# Fetch digest and post summary to Slack DIGEST=$(curl -s \ -H "Authorization: Bearer plk_your_token" \ https://pulllight.polsia.app/api/v1/installations/12345/digest) FINDINGS=$(echo $DIGEST | jq '.stats.total_findings') SAVED=$(echo $DIGEST | jq '.roi.estimated_dollars_saved') curl -X POST $SLACK_WEBHOOK \ -H 'Content-type: application/json' \ --data "{\"text\":\"PullLight this week: $FINDINGS findings, ~\$$SAVED saved\"}"
Repo stats into Grafana (scrape endpoint)
curl -s \ -H "Authorization: Bearer plk_your_token" \ https://pulllight.polsia.app/api/v1/repos/acme/api/stats \ | jq '{total_prs_reviewed, total_findings, severity_breakdown}'
Revoke a compromised token
Go to /settings/api-tokens?installation_id=<n>, find the token, and click revoke. Revocation is immediate — the next request returns 401.
Ready to start?
Generate your first token from the settings page. Tokens are free, read-only, and scoped to your installation.
Get a token →