Patchlightdocs
SDK

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

MethodDescription
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

MethodDescription
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

MethodDescription
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

FieldTypeDescription
repostringRepository identifier (e.g. "owner/repo"), max 256 chars. Groups reviews in the dashboard
diffstringUnified diff, max 1 MB
filesRecord<string, string>Optional path → content map for review context. Max 100 files, 200 KB per file, 5 MB total
refstringOptional reviewed commit sha/branch
baseRefstringOptional base branch of the diff
titlestringOptional title

createReview options

FieldDescription
idempotencyKeyReplays 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

FieldDefaultDescription
timeoutMs900000 (15 min)Overall deadline
intervalMs5000Poll interval
signalAbortSignal 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:

StatusMeaning
doneFinished; summary and findings are available
failedThe worker errored — reason in error
skippedA 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, "…"
  }
}
codeHTTPMeaning
invalid_request400Malformed body (details in message)
repo_not_scannable400Scan requested for a repo with no GitHub connection
invalid_api_key / missing_api_key401Bad or absent key
insufficient_funds402Workspace balance below the cost of the operation
spend_cap_reached / spend_cap / repo_budget402Monthly workspace cap or per-repo budget reached
not_found / repo_not_found404Does not exist, or belongs to another workspace
already_running409That repo already has a queued or running scan
payload_too_large413Diff/file caps exceeded
too_many_pending_reviews429More than 10 queued/running API reviews in the workspace
queue_unavailable / reviews_not_configured / scan_runner_not_configured503Transient platform issue — retry later
network_error / http_5xxTransport/server failure after retries
review_failed / review_skipped / timeout / abortedFrom waitForReview
scan_failed / timeoutFrom 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);
  }
}

On this page