npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

keepa-api

v0.4.0

Published

TypeScript SDK for the Keepa API.

Readme

keepa-api

Lightweight TypeScript SDK for the Keepa REST API. A single KeepaClient class exposes typed resources (products, categories, bestSellers) with parsed responses, friendly currency units, and live rate-limit visibility.

Installation

npm install keepa-api
# or
pnpm add keepa-api
# or
yarn add keepa-api
# or
bun add keepa-api

Requires Node 18+. Ships both ESM and CJS — works with import and require.

Quickstart

import KeepaClient from 'keepa-api';

const keepa = new KeepaClient({ apiKey: process.env.KEEPA_API_KEY });

const [product] = await keepa.products.list({ asins: ['B00MNV8E0C'] });

console.log(product?.title, product?.bsr, product?.images[0]);

apiKey falls back to process.env.KEEPA_API_KEY when omitted.

Client

new KeepaClient(options?)

| Option | Type | Default | Notes | |--------|------|---------|-------| | apiKey | string | process.env.KEEPA_API_KEY | Throws if neither is set. | | baseURL | string | 'https://api.keepa.com' | Override for testing/proxying. | | fetch | typeof fetch | globalThis.fetch | Plug in a custom fetch (mocks, retries, etc.). |

Products

keepa.products.list(params)Promise<KeepaProduct[]>

| Param | Type | Default | Notes | |-------|------|---------|-------| | asins | string[] | (required) | Validated + uppercased. Throws on malformed input. | | marketplace | Marketplace | 'US' | Case-insensitive. See marketplaces. | | days | number | 1 | Days of csv history when history: true. | | history | boolean | false | Populates history.price.* + scalar amazonPrice / newPrice / listPrice. Costs extra tokens. | | stats | boolean | false | Populates the stats namespace (buy-box saving basis, etc.). Costs extra tokens. |

keepa.products.retrieve(params)Promise<KeepaProduct>

Same options as list, but takes a single asin: string and returns one product. Throws ProductNotFoundError when Keepa has no record (stub with title === null, or no product returned at all).

KeepaProduct

The SDK reshapes Keepa's wire format into something usable:

| Field | Type | Notes | |-------|------|-------| | images | string[] | Full image URLs (region-neutral CDN). | | bsr | number \| null | Latest non-sentinel BSR for rootCategory. | | amazonPrice | number \| null | Latest price Amazon itself sells the product for (csv[0]) in the marketplace's major unit — dollars/pounds/yen/…. Null without history: true. | | newPrice | number \| null | Latest lowest-3rd-party-new offer price (csv[1]). Distinct from amazonPrice — Amazon isn't always the cheapest new seller. | | listPrice | number \| null | Latest list price / MSRP (csv[4]). | | history.price.amazon | PriceHistoryEntry[] | Amazon's own price over time. Empty without history: true. | | history.price.new | PriceHistoryEntry[] | Lowest-3rd-party-new price over time. | | history.price.list | PriceHistoryEntry[] | List price (MSRP) over time. | | stats.buyBoxSavingBasis | number \| null | Buy box strikethrough reference price in the marketplace's major unit. Null without stats: true or when Keepa has no saving-basis data. | | stats.buyBoxSavingBasisType | SavingBasisType \| null | Reference type for the strikethrough — SavingBasisType.LIST_PRICE or SavingBasisType.WAS_PRICE. Null when unavailable. | | monthlySold | number \| null | Keepa's estimate from Amazon's "X+ bought in past month" widget. Null when Amazon doesn't show the widget for this ASIN — common for lower-velocity / non-US listings. A genuine zero surfaces as 0. |

title, description, parentAsin, categoryTree, salesRanks, variations, features pass through from Keepa unchanged.

Stub records: Keepa returns one entry per requested ASIN even for unknown ASINs — those stubs have title === null. Filter them out:

import { isFoundProduct } from 'keepa-api';

const found = products.filter(isFoundProduct);

Price history

const [product] = await keepa.products.list({
  asins: ['B00MNV8E0C'],
  history: true,
  days: 30,
});

product.amazonPrice;              // 18.99 (USD) — Amazon's own latest
product.newPrice;                 // 16.99       — cheapest 3rd-party new
product.listPrice;                // 29.99       — MSRP
product.history.price.amazon[0];  // { timestamp: Date, price: 19.49 }

Prices are in the marketplace's major unit (USD dollars for 'US', GBP for 'GB', JPY for 'JP', …). Keepa's -1 "no data captured" entries are filtered out.

For csv types beyond Amazon / list price (NEW, USED, REFURBISHED, RATING, …), pull the row off the raw response and parse it:

import { CsvType, parsePriceHistory } from 'keepa-api';

const usedPrices = parsePriceHistory(rawProduct.csv?.[CsvType.USED]);

CsvType covers all 36 of Keepa's csv series names verbatim.

Categories

keepa.categories.list(params)Promise<Record<number, KeepaCategory>>

Resolve category metadata by browse-node id. Returns a map keyed by catId for lookup; ids Keepa doesn't recognise are silently absent from the result.

const cats = await keepa.categories.list({
  ids: [7141123011, 1040660],
  marketplace: 'GB',
});

cats[7141123011]?.contextFreeName; // "Women's Coats, Jackets & Gilets"
cats[7141123011]?.parent;          // 1040660

| Param | Type | Default | Notes | |-------|------|---------|-------| | ids | number[] | (required) | Browse-node ids. Empty array → no HTTP call, returns {}. | | marketplace | Marketplace | 'US' | | | withParents | boolean | false | Asks Keepa for ancestor records. Empirically the shape doesn't change for most categories — walk categoryTree on the product if you need a breadcrumb. |

KeepaCategory

| Field | Type | Notes | |-------|------|-------| | catId | number | Amazon browse-node id. | | name | string | Leaf-only name (collides across departments). | | contextFreeName | string \| undefined | Pre-disambiguated label (e.g. "Women's Coats, Jackets & Gilets"). Missing on very old / experimental nodes — fall back to name. | | parent | number | Parent catId; 0 marks a root. | | children | number[] \| null | Direct children, or null for leaves. | | productCount | number | Active listings in the category. |

keepa.categories.search(params)Promise<CategorySearchHit[]>

Free-text search against Keepa's category index. Hits Keepa's /search?type=category endpoint — that endpoint is category-specific despite the generic-sounding path, so it lives here as a Categories method. Empty array on "no matches".

const hits = await keepa.categories.search({
  term: 'yoga mat',
  marketplace: 'GB',
});

hits[0]?.name;        // "Yoga Mats"
hits[0]?.lowestRank;  // Lowest BSR of any product currently in this category

| Param | Type | Default | Notes | |-------|------|---------|-------| | term | string | (required) | URL-encoded for you. | | marketplace | Marketplace | 'US' | |

CategorySearchHit

| Field | Type | Notes | |-------|------|-------| | catId | number | Amazon browse-node id. | | name | string | Leaf-only display name. | | lowestRank | number | Lowest BSR (= strongest top performer) in the category. | | highestRank | number | Highest BSR (= weakest top performer) in the category. | | productCount | number | Active listings. |

Best sellers

keepa.bestSellers.retrieve(params)Promise<KeepaBestSellerList \| null>

Returns null (not an error) when Keepa has no list for the category — typical for non-leaf nodes and sparse leaves.

const list = await keepa.bestSellers.retrieve({
  categoryId: 7141123011,
  marketplace: 'GB',
});

list?.asinList[0]; // top bestseller ASIN

| Param | Type | Default | Notes | |-------|------|---------|-------| | categoryId | number | (required) | Browse-node id. | | marketplace | Marketplace | 'US' | | | sublist | boolean | true | When true, Keepa returns the sub-category top list (shorter). |

KeepaBestSellerList

| Field | Type | Notes | |-------|------|-------| | categoryId | number | Echoed from Keepa, or the caller's id when Keepa omits it. | | asinList | string[] | Top-to-bottom ordered. May be empty when the response carried no list. |

Marketplaces

import { resolveDomainId } from 'keepa-api';

resolveDomainId('gb');      // 2 (case-insensitive)
resolveDomainId(undefined); // 1 (defaults to US)

| Code | Domain | Code | Domain | |------|--------|------|--------| | US | 1 | IT | 8 | | GB | 2 | ES | 9 | | DE | 3 | IN | 10 | | FR | 4 | MX | 11 | | JP | 5 | BR | 12 | | CA | 6 | | |

Codes are ISO 3166-1 alpha-2. Domain 7 is reserved (formerly Amazon China, retired by Keepa).

Rate limits

The client tracks Keepa's token bucket on every response — success and 429 alike — and exposes the latest snapshot:

await keepa.products.list({ asins });

if (keepa.rateLimit && keepa.rateLimit.tokensLeft < 50) {
  await sleep(keepa.rateLimit.refillIn); // ms until next token refills
}

On a 429, the snapshot is also attached to the thrown error:

import { RateLimitError } from 'keepa-api';

try {
  await keepa.products.list({ asins: bigBatch });
} catch (err) {
  if (err instanceof RateLimitError && err.rateLimit) {
    await sleep(err.rateLimit.refillIn);
    // retry…
  }
}

RateLimitInfo: tokensLeft, refillIn (ms), refillRate (tokens/min), tokenFlowReduction, receivedAt.

The SDK does not auto-retry on 429. Handling that is your call.

Errors

All errors extend KeepaError:

import {
  KeepaError,
  APIError,
  RateLimitError,
  AuthenticationError,
  NetworkError,
} from 'keepa-api';

try {
  await keepa.products.list({ asins });
} catch (err) {
  if (err instanceof RateLimitError)           /* 429 — err.rateLimit */;
  else if (err instanceof AuthenticationError) /* 401 — invalid API key */;
  else if (err instanceof APIError)            /* 4xx/5xx — err.status, err.body */;
  else if (err instanceof NetworkError)        /* transport — err.cause */;
  else throw err;
}

ASIN utilities

import { isValidAsin, normalizeAsins } from 'keepa-api';

isValidAsin('B00MNV8E0C');           // true
normalizeAsins(['  b00mnv8e0c ']);   // ['B00MNV8E0C']
normalizeAsins(['B07XYZ']);          // throws — too short

products.list / products.retrieve call normalizeAsins for you. The regex catches malformed ASINs but can't tell you whether Keepa has the record — use isFoundProduct for that.

Security note

Keepa's API authenticates via ?key=... query parameter (not a header), so:

  • Don't run this in a browser. Your key would be visible in DevTools and to any browser extension or middlebox.
  • Server-side access logs will record full URLs (with key). Configure log scrubbing if the layer isn't trusted.
  • APIError.body and NetworkError.message are scrubbed of the key before being thrown.

License

MIT — see LICENSE.