SDK Reference
The @pxdiff/sdk package provides programmatic access to the pxdiff API for screenshot capture, diffing, and approval workflows.
Installation
Section titled “Installation”npm install @pxdiff/sdkPxdiffClient
Section titled “PxdiffClient”The main class for interacting with pxdiff.
Constructor
Section titled “Constructor”import { PxdiffClient } from "@pxdiff/sdk";
const client = new PxdiffClient({ apiKey: "pxd_your_key_here", apiUrl: "https://pxdiff.com", // optional, this is the default project: "my-app", // optional project slug maxRetries: 3, // optional, retry on 429/5xx retryBaseDelayMs: 1000, // optional, base delay for backoff});| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | (required) | API key (format: pxd_ prefix + base64url) |
apiUrl | string | "https://pxdiff.com" | API base URL |
project | string | Project slug for scoping operations | |
maxRetries | number | 3 | Max retry attempts on 429 and 5xx. Set to 0 to disable. |
retryBaseDelayMs | number | 1000 | Base delay in ms for exponential backoff |
Captures
Section titled “Captures”createCapture(options)
Section titled “createCapture(options)”Creates a new capture. Supports two modes:
- Fleet mode — provide URLs, pxdiff captures the screenshots for you
- Manual mode — provide PNG buffers directly
// Fleet mode: pxdiff captures the screenshotsconst capture = await client.createCapture({ suite: "storybook", branch: "feat/login", commit: "abc123", targets: [ { name: "button-primary", url: "https://storybook.example.com/iframe.html?id=button--primary" }, { name: "button-secondary", url: "https://storybook.example.com/iframe.html?id=button--secondary" }, ],});
// Manual mode: upload your own screenshotsconst capture = await client.createCapture({ suite: "e2e", branch: "feat/login", commit: "abc123", snapshots: [ { name: "login-page", png: screenshotBuffer }, { name: "signup-page", png: signupBuffer, viewport: { width: 1280, height: 900 } }, ],});Options:
| Field | Type | Default | Description |
|---|---|---|---|
suite | string | (required) | Suite identifier |
branch | string | Git branch name | |
commit | string | Git commit SHA | |
baselineRef | string | Baseline branch for comparison | |
autoApprove | boolean | Auto-approve diffs | |
ephemeral | boolean | Non-persistent capture (no branch/commit) | |
isLocal | boolean | Local development capture | |
snapshots | SnapshotInput[] | PNG buffers (manual mode) | |
targets | CaptureTarget[] | URLs to capture (fleet mode) |
Returns: Capture
getCapture(captureId)
Section titled “getCapture(captureId)”Fetch full details of a capture including all snapshots.
const capture = await client.getCapture("cap_abc123");console.log(capture.status); // "completed"console.log(capture.snapshots.length); // 12Returns: Capture
findCapture(options)
Section titled “findCapture(options)”Search for a capture by commit, branch, or suite.
const capture = await client.findCapture({ suite: "storybook", commits: ["abc123def456"],});| Field | Type | Description |
|---|---|---|
suite | string | Suite identifier |
commits | string[] | Commit SHAs to search |
branch | string | Git branch name |
Returns: Capture or null
uploadSnapshotWithDiff(input)
Section titled “uploadSnapshotWithDiff(input)”Upload a screenshot into an existing session-owned diff and get the inline diff result. Callers must supply sessionId and complete the session afterward.
const result = await client.uploadSnapshotWithDiff({ name: "homepage", png: screenshotBuffer, suite: "e2e", branch: "feat/redesign", commit: "abc123", baselineRef: "main", viewport: { width: 1280, height: 720 }, sessionId: "session_xyz",});
console.log(result.status); // "changed"console.log(result.diffPercentage); // 2.4| Field | Type | Default | Description |
|---|---|---|---|
name | string | (required) | Snapshot identifier |
png | Buffer | Uint8Array | (required) | Image buffer |
suite | string | (required) | Suite identifier |
branch | string | (required) | Git branch |
commit | string | (required) | Git commit SHA |
baselineRef | string | (required) | Baseline reference |
sessionId | string | (required) | Session identifier created via createSession() |
viewport | object | { width, height } | |
sessionId | string | Session ID for grouping | |
path | string[] | Display hierarchy path | |
metadata | Record | Custom metadata | |
isLocal | boolean | Local development | |
autoApprove | boolean | Auto-approve this snapshot |
Returns: SnapshotDiffResult
createDiff(options)
Section titled “createDiff(options)”Create a diff comparing a head capture against a baseline.
const diff = await client.createDiff({ suite: "storybook", headCaptureId: "cap_abc", baselineRef: "main", threshold: 0.063,});| Field | Type | Default | Description |
|---|---|---|---|
suite | string | (required) | Suite identifier |
headCaptureId | string | (required) | Capture to compare |
baselineRef | string | Baseline branch reference | |
baseCaptureId | string | Specific capture to compare against | |
threshold | number | 0.063 | Pixel difference threshold (0–1) |
antiAliasing | boolean | Ignore anti-aliasing differences | |
manifest | string[] | Only diff these snapshot names | |
targetRefs | string[] | Target branches for approval |
Returns: Diff
getDiff(diffId)
Section titled “getDiff(diffId)”Fetch full diff details including per-snapshot results.
const diff = await client.getDiff("diff_abc");
for (const result of diff.results) { if (result.status === "changed") { console.log(`${result.name}: ${result.diffPercentage}% different`); }}Returns: Diff
reDiff(diffId)
Section titled “reDiff(diffId)”Trigger a re-diff against current baselines. Useful after baselines have been updated.
Returns: Diff
Approvals
Section titled “Approvals”approve(diffId, options?)
Section titled “approve(diffId, options?)”Approve snapshots in a diff, promoting them to baselines.
// Approve specific snapshotsawait client.approve("diff_abc", { snapshots: ["button-primary", "header"],});
// Approve allawait client.approve("diff_abc");| Field | Type | Description |
|---|---|---|
snapshots | string[] | Specific snapshot names. Omit to approve all. |
Returns: { approved: number, total: number }
revoke(diffId, snapshots?)
Section titled “revoke(diffId, snapshots?)”Revoke approval for snapshots, resetting them to pending.
// Revoke specificawait client.revoke("diff_abc", ["button-primary"]);
// Revoke allawait client.revoke("diff_abc");Returns: { revoked: number }
getBaselineRefs(suite)
Section titled “getBaselineRefs(suite)”List all branch refs that have approved baselines for a suite.
const refs = await client.getBaselineRefs("storybook");// ["main", "develop"]Sites & Stories
Section titled “Sites & Stories”uploadSite(tarGz)
Section titled “uploadSite(tarGz)”Upload a tar.gz archive of a static site (e.g. Storybook build) for story discovery.
const site = await client.uploadSite(archiveBuffer);console.log(site.id); // "site_abc"console.log(site.status); // "pending"Returns: Site
discoverStories(siteId)
Section titled “discoverStories(siteId)”Discover stories from an uploaded Storybook site.
const stories = await client.discoverStories("site_abc");for (const story of stories) { console.log(story.title, story.name, story.url);}Returns: StoryEntry[]
Sessions
Section titled “Sessions”Sessions group inline screenshot uploads into a single capture and diff. Used by the Playwright plugin.
createSession(options)
Section titled “createSession(options)”const session = await client.createSession({ suite: "playwright", branch: "feat/login", commit: "abc123", baselineRef: "main",});| Field | Type | Default | Description |
|---|---|---|---|
suite | string | (required) | Suite identifier |
branch | string | (required) | Git branch |
commit | string | (required) | Git commit SHA |
baselineRef | string | Baseline reference | |
autoApprove | boolean | Auto-approve all uploads | |
isLocal | boolean | Development capture |
Returns: { sessionId, captureId, diffId }
completeSession(sessionId, options?)
Section titled “completeSession(sessionId, options?)”Complete a session, triggering carry-forward and GitHub check run posting. A session may contain diffs from multiple suites.
const result = await client.completeSession("session_abc", { exitCode: 0,});
console.log(result.imageToken); // signed token for image accessfor (const diff of result.diffs) { console.log(diff.suite); // "my-suite" console.log(diff.diffId); // "diff_xyz" console.log(diff.conclusion); // "success" | "failure" | null console.log(diff.reviewUrl); // URL to review UI console.log(diff.summary); // { changed, new, unchanged, removed, failed } console.log(diff.changedSnapshots); // ["button", "header"]}| Field | Type | Default | Description |
|---|---|---|---|
exitCode | number | Test runner exit code. 0 = tests passed, 1 = tests failed cleanly. Anything else (crash, SIGKILL, SIGTERM) is treated as a non-clean exit. | |
error | string | Optional error message for logging. | |
authoritative | boolean | false | Signals that the test runner reached a clean end-of-run state. See below. |
Returns: { imageToken, diffs } where each diff contains { diffId, suite, conclusion, checkRunUrl, reviewUrl, summary, changedSnapshots }
Removed-baseline detection
Section titled “Removed-baseline detection”When authoritative: true is passed, the server treats the session’s captured snapshots as the complete head set for the session’s diffs and marks any baseline without a matching head snapshot as removed. When authoritative is false or absent (the default), removed-detection is skipped — this is the safe default for legacy clients and runs that did not reach a clean end.
This field is plumbing set automatically by the pxdiff run CLI wrapper. Do not set it from custom code. The pxdiff run wrapper is the only component that knows whether the child test runner reached end-of-run; the Vitest and Playwright plugins themselves don’t, because individual test files are not authoritative for the whole suite.
In practice this means:
- Removed-baseline detection only runs for sessions wrapped with
pxdiff run -- vitestorpxdiff run -- playwright test. Invokingvitestorplaywright testdirectly — withoutpxdiff run— will NOT produce removed-baseline detection, by design. - Only exit codes
0and1are treated as authoritative. Crashes, signals, or any exit code greater than1cause the server to skip removed-detection even ifauthoritative: truewas passed. - Local-mode diffs (e.g.,
pxdiff run --local -- vitest) never trigger removed-detection. Shared baselines are never modified by local runs, regardless of theauthoritativeflag.
Error Handling
Section titled “Error Handling”All methods throw PxdiffError on failure.
import { PxdiffClient, PxdiffError } from "@pxdiff/sdk";
try { await client.getCapture("invalid-id");} catch (e) { if (e instanceof PxdiffError) { console.error(e.code); // "CAPTURE_NOT_FOUND" console.error(e.statusCode); // 404 console.error(e.message); // "Capture not found" }}| Property | Type | Description |
|---|---|---|
code | string | Error code (e.g. "VALIDATION_ERROR", "RATE_LIMITED") |
message | string | Human-readable message |
statusCode | number | HTTP status code |
retryAfter | number | undefined | Seconds to wait (from Retry-After header, present on 429 responses) |
Retryable errors (429, 500, 502, 503) are automatically retried with exponential backoff before being thrown. See Rate Limits for details.
Git Utilities
Section titled “Git Utilities”Exported from @pxdiff/sdk/git.
detectBranch(override?)
Section titled “detectBranch(override?)”Detect the current git branch. Checks CI environment variables first, then falls back to git.
import { detectBranch } from "@pxdiff/sdk/git";const branch = await detectBranch();Detection order: GITHUB_HEAD_REF → GITHUB_REF → CI_COMMIT_REF_NAME → git rev-parse --abbrev-ref HEAD
detectCommit(override?)
Section titled “detectCommit(override?)”Detect the current git commit SHA.
import { detectCommit } from "@pxdiff/sdk/git";const commit = await detectCommit();Detection order: GitHub PR event payload → GITHUB_SHA → CI_COMMIT_SHA → git rev-parse HEAD
detectBaselineRef()
Section titled “detectBaselineRef()”Detect the baseline branch reference (synchronous).
import { detectBaselineRef } from "@pxdiff/sdk/git";const ref = detectBaselineRef(); // "main" or nullDetection order: GITHUB_BASE_REF → PXDIFF_BASELINE_REF → null
getGitMergeBase(ref)
Section titled “getGitMergeBase(ref)”Find the merge-base commit between HEAD and a remote branch.
import { getGitMergeBase } from "@pxdiff/sdk/git";const mergeBase = await getGitMergeBase("main"); // commit SHA or nullresolveTargetRefs(client, suite, options?)
Section titled “resolveTargetRefs(client, suite, options?)”Resolve all target refs (branches with baselines that are git ancestors of the current branch). This is the primary mechanism for baseline resolution — used by the CLI, Playwright plugin, and Vitest plugin.
import { PxdiffClient } from "@pxdiff/sdk";import { resolveTargetRefs } from "@pxdiff/sdk/resolve-target-refs";
const client = new PxdiffClient({ apiKey: "pxd_..." });const targetRefs = await resolveTargetRefs(client, "storybook", { env: process.env });// ["staging", "main"] — all ancestor refs with baselinesResolution:
- Include
GITHUB_BASE_REFif set (does not short-circuit further detection) - Include
PXDIFF_BASELINE_REFif set - Call
getBaselineRefs(suite)to find all refs with baselines - For each, run
git merge-baseto check if it’s a git ancestor of HEAD - Return all matching refs, deduplicated
Options:
| Parameter | Type | Description |
|---|---|---|
explicitRefs | string[] | Skip auto-detection, return these refs directly |
env | Record<string, string> | Environment variables (defaults to process.env) |
Story Utilities
Section titled “Story Utilities”explodeStoryTargets(stories)
Section titled “explodeStoryTargets(stories)”Convert Storybook story entries into concrete capture targets. Handles mode explosion, viewport explosion, and parameter passthrough.
import { explodeStoryTargets } from "@pxdiff/sdk";
const stories = await client.discoverStories(siteId);const targets = explodeStoryTargets(stories);
const capture = await client.createCapture({ suite: "storybook", branch: "main", commit: "abc123", targets,});Behavior:
- Stories with
parameters.disable: trueare filtered out - Each mode in
parameters.modesproduces a separate target with[modeName]suffix - If no modes, each entry in
parameters.viewportsproduces a target with__widthsuffix - Copies
delay,selector,cropToViewport,ignoreSelectorsto all generated targets - Includes
framework: "storybook",storyId, andtagsin metadata
Capture
Section titled “Capture”interface Capture { id: string; suite: string; branch: string | null; commit: string | null; status: "pending" | "capturing" | "processing" | "completed" | "failed"; snapshots: Snapshot[]; totalSnapshots: number; completedSnapshots: number; failedSnapshots: number; partial: boolean; ephemeral: boolean; autoApprove: boolean; createdAt: string;}Snapshot
Section titled “Snapshot”interface Snapshot { name: string; viewport: { width: number; height: number } | null; imageUrl: string;}interface Diff { id: string; suite: string; status: "processing" | "completed" | "failed"; headCaptureId: string; baseCaptureId: string | null; baselineRef: string | null; isStale: boolean; targetRefs: string[] | null; isLocal: boolean; results: DiffResult[]; reviewUrl: string | null; createdAt: string;}DiffResult
Section titled “DiffResult”interface DiffResult { name: string; viewport: { width: number; height: number } | null; status: "unchanged" | "changed" | "new" | "removed" | "skipped"; diffPercentage: number | null; diffImageUrl: string | null; currentImageUrl: string; baselineImageUrl: string | null; headHash: string | null; approvalStatus: "pending" | "approved" | "rejected"; baselineSource: string | null;}SnapshotDiffResult
Section titled “SnapshotDiffResult”interface SnapshotDiffResult { name: string; suite: string; status: "new" | "changed" | "unchanged"; diffPercentage: number | null; diffId: string; captureId: string; currentS3Key: string | null; baselineS3Key: string | null; diffS3Key: string | null; path: string[] | null; approvalStatus: "pending" | "approved" | "rejected"; baselineSource: string | null;}CaptureTarget
Section titled “CaptureTarget”interface CaptureTarget { name: string; url: string; selector?: string; viewport?: { width: number; height: number }; framework?: "storybook"; delay?: number; cropToViewport?: boolean; ignoreSelectors?: string[]; path?: string[]; metadata?: Record<string, unknown>;}interface Site { id: string; project: string; status: "pending" | "uploading" | "ready" | "failed"; fileCount: number; totalBytes: number; cdnUrl: string; createdAt: string; error: string | null;}StoryEntry
Section titled “StoryEntry”interface StoryEntry { url: string; name: string; id?: string; title?: string; tags?: string[]; parameters?: { delay?: number; modes?: Record<string, Record<string, unknown>>; viewports?: number[]; disable?: boolean; selector?: string; cropToViewport?: boolean; ignoreSelectors?: string[]; diffThreshold?: number; pauseAnimationAtEnd?: boolean; forcedColors?: string; prefersReducedMotion?: string; media?: string; };}