Skip to content

SDK Reference

The @pxdiff/sdk package provides programmatic access to the pxdiff API for screenshot capture, diffing, and approval workflows.

Terminal window
npm install @pxdiff/sdk

The main class for interacting with pxdiff.

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
});
OptionTypeDefaultDescription
apiKeystring(required)API key (format: pxd_ prefix + base64url)
apiUrlstring"https://pxdiff.com"API base URL
projectstringProject slug for scoping operations
maxRetriesnumber3Max retry attempts on 429 and 5xx. Set to 0 to disable.
retryBaseDelayMsnumber1000Base delay in ms for exponential backoff

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 screenshots
const 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 screenshots
const 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:

FieldTypeDefaultDescription
suitestring(required)Suite identifier
branchstringGit branch name
commitstringGit commit SHA
baselineRefstringBaseline branch for comparison
autoApprovebooleanAuto-approve diffs
ephemeralbooleanNon-persistent capture (no branch/commit)
isLocalbooleanLocal development capture
snapshotsSnapshotInput[]PNG buffers (manual mode)
targetsCaptureTarget[]URLs to capture (fleet mode)

Returns: Capture


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); // 12

Returns: Capture


Search for a capture by commit, branch, or suite.

const capture = await client.findCapture({
suite: "storybook",
commits: ["abc123def456"],
});
FieldTypeDescription
suitestringSuite identifier
commitsstring[]Commit SHAs to search
branchstringGit branch name

Returns: Capture or null


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
FieldTypeDefaultDescription
namestring(required)Snapshot identifier
pngBuffer | Uint8Array(required)Image buffer
suitestring(required)Suite identifier
branchstring(required)Git branch
commitstring(required)Git commit SHA
baselineRefstring(required)Baseline reference
sessionIdstring(required)Session identifier created via createSession()
viewportobject{ width, height }
sessionIdstringSession ID for grouping
pathstring[]Display hierarchy path
metadataRecordCustom metadata
isLocalbooleanLocal development
autoApprovebooleanAuto-approve this snapshot

Returns: SnapshotDiffResult


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,
});
FieldTypeDefaultDescription
suitestring(required)Suite identifier
headCaptureIdstring(required)Capture to compare
baselineRefstringBaseline branch reference
baseCaptureIdstringSpecific capture to compare against
thresholdnumber0.063Pixel difference threshold (0–1)
antiAliasingbooleanIgnore anti-aliasing differences
manifeststring[]Only diff these snapshot names
targetRefsstring[]Target branches for approval

Returns: Diff


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


Trigger a re-diff against current baselines. Useful after baselines have been updated.

Returns: Diff


Approve snapshots in a diff, promoting them to baselines.

// Approve specific snapshots
await client.approve("diff_abc", {
snapshots: ["button-primary", "header"],
});
// Approve all
await client.approve("diff_abc");
FieldTypeDescription
snapshotsstring[]Specific snapshot names. Omit to approve all.

Returns: { approved: number, total: number }


Revoke approval for snapshots, resetting them to pending.

// Revoke specific
await client.revoke("diff_abc", ["button-primary"]);
// Revoke all
await client.revoke("diff_abc");

Returns: { revoked: number }


List all branch refs that have approved baselines for a suite.

const refs = await client.getBaselineRefs("storybook");
// ["main", "develop"]

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


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 group inline screenshot uploads into a single capture and diff. Used by the Playwright plugin.

const session = await client.createSession({
suite: "playwright",
branch: "feat/login",
commit: "abc123",
baselineRef: "main",
});
FieldTypeDefaultDescription
suitestring(required)Suite identifier
branchstring(required)Git branch
commitstring(required)Git commit SHA
baselineRefstringBaseline reference
autoApprovebooleanAuto-approve all uploads
isLocalbooleanDevelopment capture

Returns: { sessionId, captureId, diffId }


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 access
for (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"]
}
FieldTypeDefaultDescription
exitCodenumberTest runner exit code. 0 = tests passed, 1 = tests failed cleanly. Anything else (crash, SIGKILL, SIGTERM) is treated as a non-clean exit.
errorstringOptional error message for logging.
authoritativebooleanfalseSignals 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 }

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 -- vitest or pxdiff run -- playwright test. Invoking vitest or playwright test directly — without pxdiff run — will NOT produce removed-baseline detection, by design.
  • Only exit codes 0 and 1 are treated as authoritative. Crashes, signals, or any exit code greater than 1 cause the server to skip removed-detection even if authoritative: true was passed.
  • Local-mode diffs (e.g., pxdiff run --local -- vitest) never trigger removed-detection. Shared baselines are never modified by local runs, regardless of the authoritative flag.

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"
}
}
PropertyTypeDescription
codestringError code (e.g. "VALIDATION_ERROR", "RATE_LIMITED")
messagestringHuman-readable message
statusCodenumberHTTP status code
retryAfternumber | undefinedSeconds 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.


Exported from @pxdiff/sdk/git.

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_REFGITHUB_REFCI_COMMIT_REF_NAMEgit rev-parse --abbrev-ref HEAD


Detect the current git commit SHA.

import { detectCommit } from "@pxdiff/sdk/git";
const commit = await detectCommit();

Detection order: GitHub PR event payload → GITHUB_SHACI_COMMIT_SHAgit rev-parse HEAD


Detect the baseline branch reference (synchronous).

import { detectBaselineRef } from "@pxdiff/sdk/git";
const ref = detectBaselineRef(); // "main" or null

Detection order: GITHUB_BASE_REFPXDIFF_BASELINE_REFnull


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 null

resolveTargetRefs(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 baselines

Resolution:

  1. Include GITHUB_BASE_REF if set (does not short-circuit further detection)
  2. Include PXDIFF_BASELINE_REF if set
  3. Call getBaselineRefs(suite) to find all refs with baselines
  4. For each, run git merge-base to check if it’s a git ancestor of HEAD
  5. Return all matching refs, deduplicated

Options:

ParameterTypeDescription
explicitRefsstring[]Skip auto-detection, return these refs directly
envRecord<string, string>Environment variables (defaults to process.env)

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: true are filtered out
  • Each mode in parameters.modes produces a separate target with [modeName] suffix
  • If no modes, each entry in parameters.viewports produces a target with __width suffix
  • Copies delay, selector, cropToViewport, ignoreSelectors to all generated targets
  • Includes framework: "storybook", storyId, and tags in metadata

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;
}
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;
}
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;
}
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;
}
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;
}
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;
};
}