PullLight API

A read-only JSON API for piping catches, repo stats, and weekly digests into DataDog, Grafana, Linear, or any internal dashboard.

Read-only JSON 60 req/min Bearer token auth

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:

HEADERMEANING
X-RateLimit-LimitAlways 60
X-RateLimit-RemainingRequests left this minute
X-RateLimit-ResetUnix timestamp when window resets
Retry-AfterSeconds to wait (only on 429)

Endpoints

GET /api/v1/catches Paginated list of public catches

Returns a paginated list of bugs PullLight has caught across all public repos. Mirrors the /catches feed.

Query parameters

PARAMTYPEDESCRIPTION
severitystringFilter: high, medium, or low
categorystringFilter by category slug, e.g. sql-injection
repostringFilter by owner/name, e.g. acme/api
sinceISO dateReturn only catches published after this date
limitintMax results (default 50, max 200)
offsetintSkip 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
  }
}
GET /api/v1/repos/:owner/:repo/stats Repo-level aggregated stats

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"
}
GET /api/v1/installations/:id/digest Weekly digest for one installation

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.

CMD @pulllight recheck Re-run analysis on current head SHA

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
CMD @pulllight ignore <review-id> Mark a review as ignored — remove from queue

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.
CMD @pulllight explain <review-id> Ask Claude for a deeper explanation + fix patch

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' });
```
CMD @pulllight help List available commands

Posts a formatted command reference table as a PR comment. Safe to run at any time — no side effects.

Example

@pulllight help

Error codes

STATUSERROR CODEMEANING
400invalid_paramA query param or path segment is malformed
401unauthorizedMissing, invalid, or revoked token
403forbiddenToken does not have access to this resource (wrong installation)
429rate_limited60 req/min exceeded — wait for Retry-After seconds
500internal_errorServer 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 →