Breached Password API (range endpoint)
The range API implements a k-anonymity model that lets you check passwords against known breaches without exposing the password — or even its full hash — to our servers.
This in-browser demo runs against the live deployment using your signed-in console session (no API key needed here). In production you call the endpoint below with a project API key.
The k-anonymity approach
Sending a full password hash to an external service creates a risk: an attacker who intercepts the hash can use rainbow tables to reverse it. The range-prefix model solves this by sending only a partial hash.
With a 5-character hex prefix, each request maps to roughly 1 million possible full hashes. The server returns every match for that bucket, and your application finds the exact match locally. An observer (including LeakJar) cannot determine which suffix you actually care about.
Endpoint
GET https://api.leakjar.com/v1/passwords/range/{prefix}
- prefix: first 5 uppercase hex characters of the SHA-1 hash.
- Authorization: Bearer token with a project API key (
lj_…). - Quota: usage counts against your plan’s monthly check quota. See the
RateLimit-*response headers and Errors & limits. - Cache: responses carry
Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400. Safe to cache at the edge.
Response format
Content-Type: text/plain. One match per line, in SUFFIX:COUNT format (same shape as HIBP’s Pwned Passwords). The count is how many leaked rows in our corpus share that hash — treat it as a popularity / prevalence signal.
CBFDAC6008F9CAB4083784CBD1874F76618D2A97:3730471
7C4A8D09CA3762AF61E59520943DC26494F8941B:124812
D033E22AE348AEB5660FC2140AEC35850C4DA997:42
...Step 1: Hash the password
Compute SHA-1 and convert to uppercase hex. This must happen on your server, never in the browser.
import { createHash } from "crypto";
function sha1Hex(password: string): string {
return createHash("sha1")
.update(password, "utf8")
.digest("hex")
.toUpperCase();
}Step 2: Extract the prefix
const hash = sha1Hex("user-password");
const prefix = hash.slice(0, 5); // e.g. "CBFDA"
const suffix = hash.slice(5); // remaining 35 charsStep 3: Query the range endpoint
const res = await fetch(
`https://api.leakjar.com/v1/passwords/range/${prefix}`,
{
headers: {
Authorization: `Bearer ${process.env.LEAKJAR_API_KEY}`,
},
}
);
if (!res.ok) throw new Error(`LeakJar ${res.status}`);
const body = await res.text();
const entries = body
.split("\n")
.filter(Boolean)
.map((line) => {
const [s, c] = line.split(":");
return { suffix: s, count: Number(c) };
});Step 4: Compare locally
const match = entries.find((e) => e.suffix === suffix);
if (match) {
console.log(`Password seen ${match.count} times in breaches.`);
}Drop-in helper
import { createHash } from "crypto";
export interface BreachResult {
breached: boolean;
count: number;
}
export async function checkPassword(
password: string
): Promise<BreachResult> {
const hash = createHash("sha1")
.update(password, "utf8")
.digest("hex")
.toUpperCase();
const prefix = hash.slice(0, 5);
const suffix = hash.slice(5);
const res = await fetch(
`https://api.leakjar.com/v1/passwords/range/${prefix}`,
{
headers: {
Authorization: `Bearer ${process.env.LEAKJAR_API_KEY}`,
},
}
);
if (!res.ok) throw new Error(`LeakJar ${res.status}`);
for (const line of (await res.text()).split("\n")) {
if (!line) continue;
const [s, c] = line.split(":");
if (s === suffix) {
return { breached: true, count: Number(c) };
}
}
return { breached: false, count: 0 };
}