Docs
Reference

Errors, rate limits & signatures

What the API returns when things go wrong, how to stay within limits, and how to verify webhook authenticity.

Error envelope

Every error returns a structured, machine-readable object. The error.code is stable and documented below; each error also carries a doc_url that deep-links to its entry on this page, plus a request_id (also in the X-Request-Id header) to quote when contacting support.

{
  "error": {
    "type": "rate_limit_error",
    "code": "quota_exceeded",
    "message": "Monthly check quota exceeded. Upgrade your plan or wait for the next billing period.",
    "doc_url": "https://leakjar.com/docs/errors#quota_exceeded",
    "resetAt": "2026-07-01T00:00:00.000Z",
    "upgradeUrl": "https://leakjar.com/pricing/password-protect?ref=429",
    "usage": { "used": 10000, "limit": 10000 }
  },
  "request_id": "req_8f3c1a9b2d..."
}

Error codes

codeHTTPtypeMeaning / action
malformed_request400invalid_request_errorBody isn't valid JSON or required fields are missing. Fix the request; do not retry as-is.
invalid_prefix400invalid_request_errorRange prefix isn't 5 uppercase hex chars. Send the first 5 hex chars of the SHA-1, uppercased.
batch_too_large400invalid_request_errorMore than 1000 emails in one /check call. Split into batches of ≤1000.
missing_bearer401authentication_errorAuthorization header missing or malformed. Send `Authorization: Bearer lj_…`.
invalid_api_key401authentication_errorKey is invalid, expired, or revoked. Check the key in the dashboard; rotate if needed.
missing_scope403permission_errorKey lacks the required scope. Issue a key with the `bpd` scope.
quota_exceeded429rate_limit_errorMonthly check quota exceeded. Honor Retry-After; upgrade the plan.
upstream_error502api_errorBreach datastore lookup failed. Retry with exponential backoff + jitter.
not_configured500api_errorBackend datastore not configured. Transient; contact support if it persists.

Rate-limit headers

Every API response (not just 429s) carries usage headers so you can self-throttle before hitting a limit:

  • RateLimit-Limit — your monthly quota.
  • RateLimit-Remaining — checks left this period.
  • RateLimit-Reset — unix seconds when the window resets.
  • Retry-After — seconds to wait (on 429 only).
  • X-LeakJar-Tier — your current plan tier.
  • X-Request-Id — trace id for support (also in error bodies).

Retry strategy

Retry 5xx and 429 (after Retry-After); never retry 4xx other than 429. Use exponential backoff with jitter.

async function withRetry(fn, max = 4) {
  for (let attempt = 0; ; attempt++) {
    const res = await fn();
    if (res.ok) return res;
    const retryable = res.status === 429 || res.status >= 500;
    if (!retryable || attempt >= max) return res;
    const retryAfter = Number(res.headers.get("Retry-After")) || 0;
    const backoff = retryAfter * 1000 || 2 ** attempt * 250 + Math.random() * 250;
    await new Promise((r) => setTimeout(r, backoff));
  }
}

Verifying webhook signatures

Each webhook POST carries an X-LeakJar-Signature header of the form t=<unix>,v1=<hex>. The signature is HMAC-SHA256(secret, "<t>.<rawBody>"). Reject requests whose timestamp is more than 5 minutes old (replay protection) or whose signature doesn’t match.

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.trim().split("=")),
  );
  const t = parts.t, v1 = parts.v1;
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // replay
  const expected = createHmac("sha256", secret)
    .update(`${t}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

See Webhooks for the event catalog and payload shapes.