Client reference
TypeScript client methods, options, types, and error handling.
import { createClient, PatchlightError } from "@patchlight/sdk";
const client = createClient({
apiKey: process.env.PATCHLIGHT_API_KEY!,
// baseUrl: "https://…", // optional override (or PATCHLIGHT_BASE_URL)
});Prop
Type
Methods
Reviews
| Method | Description |
|---|---|
createReview(input, options?) | POST /v1/reviews — submit a diff for review. Returns { id, status } |
getReview(id) | GET /v1/reviews/:id — status, summary, error, timestamps |
getReviewFindings(id) | GET /v1/reviews/:id/findings — { status, findings } (empty while running) |
waitForReview(id, options?) | Polls until done. Throws review_failed, review_skipped, or timeout |
Security scans
| Method | Description |
|---|---|
startScan(repoId) | POST /v1/repos/:id/scan — start a scan. Returns { id, status } |
getScan(id) | GET /v1/scans/:id — status, cost, findingsCount |
getScanFindings(id) | GET /v1/scans/:id/findings — { status, findings } (empty while running) |
waitForScan(id, options?) | Polls until done. Throws scan_failed or timeout |
repoId is a repository id from listRepos(), not an "owner/repo" name. Scans require a
connected GitHub repository — repos created implicitly by createReview cannot be scanned.
const repos = await client.listRepos();
const repo = repos.find((r) => r.fullName === "acme/checkout-service")!;
const { id } = await client.startScan(repo.id);
await client.waitForScan(id);
const { findings } = await client.getScanFindings(id);Findings & workspace
| Method | Description |
|---|---|
listFindings() | Recent findings across repositories, review and scan alike |
updateFinding(id, status) | PATCH /v1/findings/:id — triage: "open", "resolved", "dismissed" |
getBalance() | Remaining USD balance of the key's workspace |
listRepos() | Repositories in the workspace |
listReviews() | Recent reviews — same fields as getReview |
updateFinding accepts both review and scan finding ids, so you can triage straight from a
listFindings() result without checking which kind you hold:
const findings = await client.listFindings();
for (const f of findings.filter((f) => f.category === "style")) {
await client.updateFinding(f.id, "dismissed");
}createReview input
| Field | Type | Description |
|---|---|---|
repo | string | Repository identifier (e.g. "owner/repo"), max 256 chars. Groups reviews in the dashboard |
diff | string | Unified diff, max 1 MB |
files | Record<string, string> | Optional path → content map for review context. Max 100 files, 200 KB per file, 5 MB total |
ref | string | Optional reviewed commit sha/branch |
baseRef | string | Optional base branch of the diff |
title | string | Optional title |
createReview options
| Field | Description |
|---|---|
idempotencyKey | Replays with the same key return the original review instead of creating a duplicate. Also enables automatic retry of the submit on transient errors |
waitForReview / waitForScan options
| Field | Default | Description |
|---|---|---|
timeoutMs | 900000 (15 min) | Overall deadline |
intervalMs | 5000 | Poll interval |
signal | — | AbortSignal to cancel waiting |
Types
Timestamps (createdAt, startedAt, completedAt, finishedAt) are ISO-8601 strings, not
epoch numbers — parse them with new Date(value). Versions before 0.2.0 typed them as number,
which did not match what the API returned.
A Review settles into one of three terminal statuses:
| Status | Meaning |
|---|---|
done | Finished; summary and findings are available |
failed | The worker errored — reason in error |
skipped | A spend cap or repo budget blocked it before any work started. Terminal: it never advances |
A Finding has the same shape wherever it comes from. Exactly one of reviewId / scanId is
set and the other is null; cwe is only ever populated by scans.
Error handling
All failures throw PatchlightError:
try {
await client.createReview({ repo, diff });
} catch (err) {
if (err instanceof PatchlightError) {
console.error(err.code, err.status, err.message);
// e.g. "insufficient_funds", 402, "…"
}
}code | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | Malformed body (details in message) |
repo_not_scannable | 400 | Scan requested for a repo with no GitHub connection |
invalid_api_key / missing_api_key | 401 | Bad or absent key |
insufficient_funds | 402 | Workspace balance below the cost of the operation |
spend_cap_reached / spend_cap / repo_budget | 402 | Monthly workspace cap or per-repo budget reached |
not_found / repo_not_found | 404 | Does not exist, or belongs to another workspace |
already_running | 409 | That repo already has a queued or running scan |
payload_too_large | 413 | Diff/file caps exceeded |
too_many_pending_reviews | 429 | More than 10 queued/running API reviews in the workspace |
queue_unavailable / reviews_not_configured / scan_runner_not_configured | 503 | Transient platform issue — retry later |
network_error / http_5xx | — | Transport/server failure after retries |
review_failed / review_skipped / timeout / aborted | — | From waitForReview |
scan_failed / timeout | — | From waitForScan |
Retries and backoff
GET requests are retried up to 3 times with exponential backoff on network errors and 5xx
responses. createReview is only retried when an idempotencyKey makes the replay safe.
startScan is never retried — the API dedupes concurrent scans of one repo with
already_running, so a replay reports a conflict rather than returning the original scan.
On 429, err.retryAfterSeconds carries the response's Retry-After value (null when absent),
so a pipeline can back off for exactly as long as the API asked:
catch (err) {
if (err instanceof PatchlightError && err.retryAfterSeconds) {
await sleep(err.retryAfterSeconds * 1000);
}
}