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

mercapi

v0.3.0

Published

Node.js client for Mercari Japan API

Readme

mercapi-node

npm Node.js API Health

Live API status: zhu-kai.github.io/mercapi-node — a scheduled check runs every 6 hours against the real Mercari API and fails loudly if the API changes.

Node.js client for Mercari Japan API. TypeScript-first, serverless-friendly.

This project is derived from mercapi by Take-kun.

Disclaimer: This is an unofficial library. Use at your own risk.

Features

  • Full TypeScript support with type definitions
  • Covers search, item details, sellers, reviews, similar items, Mercari Shops products, search autocomplete, seller badges, and master data
  • Serverless-optimized (fresh keys per request by default)
  • No heavy dependencies (only jose for JWT signing)
  • ESM and CommonJS support
  • Node.js 18+ (uses native fetch)

Installation

npm install mercapi

Quick Start

import { Mercapi } from 'mercapi';

const results = await Mercapi.search('iPhone 15');
console.log(`Found ${results.meta.numFound} items`);

for (const item of results.items) {
  console.log(`${item.name}: ¥${item.price}`);
}

Usage Patterns

Pattern 1: Static Methods (Simplest)

Best for serverless. Each call uses fresh keys automatically.

import { Mercapi } from 'mercapi';

const results = await Mercapi.search('Nintendo Switch');
const item = await Mercapi.getItem('m12345678901');
const profile = await Mercapi.getProfile('123456789');
const sellerItems = await Mercapi.getSellerItems('123456789');

Pattern 2: Instance with Fresh Keys (Default)

import { Mercapi } from 'mercapi';

const mercapi = new Mercapi();
const results = await mercapi.search('AirPods'); // Fresh keys
const results2 = await mercapi.search('iPad'); // Fresh keys again

Pattern 3: Instance with Key Reuse

For batched requests where you want to reuse keys:

import { Mercapi } from 'mercapi';

const mercapi = new Mercapi({ reuseKeys: true });
const results1 = await mercapi.search('camera');
const results2 = await mercapi.search('lens');

// Manually rotate keys if needed
await mercapi.rotateKeys();

API Reference

search(query, options?)

Search for items on Mercari.

const results = await Mercapi.search('iPhone', options);

Options

| Option | Type | Default | Description | | ------------------ | -------------- | ---------- | --------------------------- | | categories | number[] | | Category IDs | | brands | number[] | | Brand IDs | | sizes | number[] | | Size IDs | | priceMin | number | | Minimum price (JPY) | | priceMax | number | | Maximum price (JPY) | | itemConditions | number[] | | Condition IDs (1-6) | | shippingPayer | number[] | | 1=buyer, 2=seller | | colors | number[] | | Color IDs (1-12) | | status | ItemStatus[] | [OnSale] | OnSale, SoldOut | | sortBy | SortBy | Score | Sort field | | sortOrder | SortOrder | Desc | Sort direction | | excludeKeyword | string | | Keywords to exclude | | pageToken | string | | Pagination token | | withAuction | boolean | true | Include auction data | | excludeShopItems | boolean | false | Exclude Mercari Shops items | | itemTypes | ItemType[] | | Restrict to listing types (e.g. Shops only) | | sellerIds | string[] | | Restrict to specific sellers |

ID-based filters (categories, brands, sizes, etc.) reference Mercari master data. The full lists are in docs/facets/; refresh them anytime with npm run fetch-facets.

Response: SearchResult

interface SearchResult {
  items: SearchResultItem[];
  meta: {
    numFound: number; // Total matching items
    nextPageToken: string; // Token for next page
  };
}

interface SearchResultItem {
  id: string; // Item ID (e.g., "m12345678901")
  sellerId: string; // Seller ID
  name: string; // Item title
  price: number; // Price in JPY
  status: string; // Item status
  categoryId: number; // Category ID
  itemConditionId: number; // Condition ID (1-6)
  shippingPayerId: number; // Shipping payer ID
  thumbnails: string[]; // Thumbnail URLs
  created: number; // Creation timestamp
  updated: number; // Last update timestamp
  isShopItem: boolean; // true if Mercari Shops item
  auction?: {
    // Only present for auction items
    id: string;
    endTime: number; // Bid deadline (Unix timestamp, seconds)
    totalBids: number;
    highestBid: number; // Current highest bid in JPY
    initialPrice: number; // Starting price in JPY
  };
  itemBrand?: { id: number; name: string; subName: string };
  shopName?: string; // Only present for Mercari Shops items
}

Example

import { Mercapi, SortBy, SortOrder, ItemCondition, ShippingPayer } from 'mercapi';

const results = await Mercapi.search('AirPods', {
  categories: [8],
  itemConditions: [ItemCondition.BrandNew, ItemCondition.LikeNew],
  shippingPayer: [ShippingPayer.Seller],
  priceMin: 5000,
  priceMax: 20000,
  sortBy: SortBy.Price,
  sortOrder: SortOrder.Asc,
  excludeShopItems: true,
});

// Auction items are flagged via the `auction` field
for (const item of results.items) {
  if (item.auction) {
    console.log(`[Auction] ${item.name}: ¥${item.auction.highestBid} (${item.auction.totalBids} bids)`);
  }
}

getItem(id, options?)

Get detailed item information.

const item = await Mercapi.getItem('m12345678901');

Options

| Option | Type | Default | Description | | ---------------- | --------- | ------- | -------------------- | | includeAuction | boolean | true | Include auction info |

Response: Item | null

interface Item {
  id: string;
  name: string;
  price: number;
  description: string;
  status: string;
  photos: string[]; // Full-size image URLs
  thumbnails: string[];
  seller: {
    id: string;
    name: string;
    photoUrl: string;
    numSellItems: number;
    ratings: { good: number; normal: number; bad: number };
    numRatings: number;
    starRatingScore: number; // 1-5
    isFollowable: boolean;
    isBlocked: boolean;
  };
  itemCategory: {
    id: number;
    name: string;
    parentCategoryId: number;
    parentCategoryName: string;
    rootCategoryId: number;
    rootCategoryName: string;
  };
  itemCondition: { id: number; name: string };
  itemBrand?: { id: number; name: string; subName: string };
  shippingPayer: { id: number; name: string; code: string };
  shippingMethod: { id: number; name: string };
  shippingFromArea: { id: number; name: string };
  shippingDuration?: { id: number; name: string; minDays: number; maxDays: number };
  numLikes: number;
  numComments: number;
  comments: Array<{
    id: string;
    message: string;
    user: { id: string; name: string; photoUrl: string };
    created: number;
  }>;
  created: number;
  updated: number;
  isShopItem: boolean;
  isAnonymousShipping: boolean;
  isOfferable: boolean;
  auctionInfo?: {
    // Only present for auction items
    id: string;
    startTime: number;
    endTime: number;
    totalBids: number;
    initialPrice: number;
    highestBid: number;
    state: string; // e.g., "STATE_ONGOING", "STATE_NO_BID"
    auctionType: string;
  };
  itemAttributes?: Record<string, unknown>[]; // Raw dynamic attributes; schema varies by category
}

Example

const item = await Mercapi.getItem('m12345678901');
if (item) {
  console.log(`${item.name}: ¥${item.price}`);
  console.log(`Seller: ${item.seller.name} (★${item.seller.starRatingScore})`);
  console.log(`Category: ${item.itemCategory.name}`);
  console.log(`Condition: ${item.itemCondition.name}`);
  if (item.itemBrand) {
    console.log(`Brand: ${item.itemBrand.name}`);
  }
  if (item.shippingDuration) {
    console.log(`Ships in: ${item.shippingDuration.minDays}-${item.shippingDuration.maxDays} days`);
  }
}

getProfile(userId)

Get seller profile information.

const profile = await Mercapi.getProfile('123456789');

Response: Profile | null

interface Profile {
  id: string;
  name: string;
  photoUrl: string;
  introduction: string;
  numSellItems: number;
  ratings: { good: number; normal: number; bad: number };
  numRatings: number;
  starRatingScore: number; // 1-5
  followerCount: number;
  followingCount: number;
  isOrganizationalUser: boolean; // true if business account
  created: number;
}

Example

const profile = await Mercapi.getProfile('123456789');
if (profile) {
  console.log(`${profile.name} (★${profile.starRatingScore})`);
  console.log(`Ratings: ${profile.ratings.good} good, ${profile.ratings.bad} bad`);
  console.log(`Followers: ${profile.followerCount}`);
}

getSellerItems(sellerId, pageToken?, options?)

Get items listed by a seller.

const sellerItems = await Mercapi.getSellerItems('123456789');
const onSaleOnly = await Mercapi.getSellerItems('123456789', undefined, {
  limit: 10,
  status: ['on_sale'],
  excludeArchivedItem: true,
});

Options

| Option | Type | Default | Description | | --------------------- | ---------- | -------------------------------- | ------------------------------ | | limit | number | 30 | Maximum items to return | | status | string[] | ['on_sale','trading','sold_out'] | Statuses to include | | excludeArchivedItem | boolean | false | Skip listings archived by seller |

Response: SellerItems

interface SellerItems {
  items: Array<{
    id: string;
    name: string;
    price: number;
    status: string;
    thumbnails: string[];
    created: number;
    updated: number;
    isNoPrice: boolean;
    itemBrand?: { id: number; name: string; subName: string };
    auctionInfo?: AuctionInfo; // Only present for auction items
  }>;
  nextPageToken: string;
}

Example

const sellerItems = await Mercapi.getSellerItems('123456789');
for (const item of sellerItems.items) {
  console.log(`${item.name}: ¥${item.price} (${item.status})`);
}

// Pagination
if (sellerItems.nextPageToken) {
  const nextPage = await Mercapi.getSellerItems('123456789', sellerItems.nextPageToken);
}

getReviews(userId, options?)

Get reviews written about a user, newest first.

const reviews = await Mercapi.getReviews('123456789', { limit: 10 });
for (const r of reviews) {
  console.log(`[${r.fame}] ${r.user.name}: ${r.message}`);
}

// Older reviews: pass the last review's pagerId - 1
const older = await Mercapi.getReviews('123456789', {
  limit: 10,
  maxPagerId: reviews[reviews.length - 1].pagerId - 1,
});
interface Review {
  subject: string; // Role of the reviewed user: 'seller' | 'buyer'
  fame: string; // 'good' | 'normal' | 'bad'
  message: string;
  user: { id: string; name: string; photoUrl: string };
  created: number;
  pagerId: number; // Pagination cursor
}

getSimilarItems(itemId, options?)

Get items similar to a listing, as shown on the item page. Accepts { limit?: number } (default 15).

const similar = await Mercapi.getSimilarItems('m12345678901', { limit: 10 });
interface SimilarItem {
  id: string; // Item ID or Shops product ID
  name: string;
  price: number;
  status: string;
  thumbnail: string;
  itemType: string; // 'ITEM_TYPE_MERCARI' | 'ITEM_TYPE_BEYOND'
  auctionHighestBid?: number; // Only present for auction items
}

getSearchSuggestions(query)

Search bar autocomplete: suggested keywords with category context for a partial query.

const suggestions = await Mercapi.getSearchSuggestions('ニンテンド');
// [{ keyword: 'ニンテンドースイッチ', title: ..., subtitle: 'テレビゲーム', categories: [{ id: 76, name: 'テレビゲーム' }] }, ...]

getShopsProduct(productId)

Get Mercari Shops product details. Search results with itemType of ITEM_TYPE_BEYOND (i.e. isShopItem: true) are Shops products; getItem() does not work for them.

const results = await Mercapi.search('switch', { itemTypes: [ItemType.Beyond] });
const product = await Mercapi.getShopsProduct(results.items[0].id);
interface ShopsProduct {
  id: string;
  displayName: string;
  price: number;
  productTags: string[]; // e.g. ['sold_out']
  thumbnail: string;
  created: number;
  updated: number;
  photos: string[];
  description: string;
  shop?: {
    id: string;
    displayName: string;
    thumbnail: string;
    score: number;
    reviewCount: number;
  };
  productDetail?: Record<string, unknown>; // Raw remaining data (categories, condition, shipping, variants, ...)
}

Returns null if the product does not exist.


getSellerBadges(userId) / hasIdentityVerifiedBadge(userId)

Seller achievement badges and identity verification (本人確認) status.

const badges = await Mercapi.getSellerBadges('123456789');
// [{ id: 10004, name: '高評価', description: ..., iconUrl: ... }, ...]

const verified = await Mercapi.hasIdentityVerifiedBadge('123456789'); // boolean

getDesiredPriceInfo(itemId)

Aggregated "desired price" (希望価格) registrations for a listing.

const info = await Mercapi.getDesiredPriceInfo('m12345678901');
// { itemId, registeredCount, highestDesiredPrice, lowestDesiredPrice, highestDesiredPriceCount }

getMasterData(dataset)

Reference (master) data enumerating IDs usable in search filters. Returns raw JSON since the schema differs per dataset.

const conditions = await Mercapi.getMasterData('itemConditions');

Available datasets: itemCategories, itemCategoryGroups, itemBrands, shippingMethods, itemSizes, itemColors, itemConditions, shippingPayers.


Pagination

let pageToken: string | undefined;

do {
  const results = await Mercapi.search('manga', {
    pageToken,
    sortBy: SortBy.CreatedTime,
    sortOrder: SortOrder.Desc,
  });

  for (const item of results.items) {
    console.log(item.name);
  }

  pageToken = results.meta.nextPageToken || undefined;
} while (pageToken);

Enums

SortBy

| Value | Description | | -------------------- | ------------------- | | SortBy.Score | Relevance (default) | | SortBy.CreatedTime | Newest first | | SortBy.Price | Price | | SortBy.NumLikes | Number of likes |

SortOrder

| Value | Description | | ---------------- | -------------------- | | SortOrder.Desc | Descending (default) | | SortOrder.Asc | Ascending |

ItemStatus

| Value | Description | | -------------------- | ----------------- | | ItemStatus.OnSale | Currently on sale | | ItemStatus.SoldOut | Sold out |

ItemType

| Value | Description | | ------------------ | ---------------------------------- | | ItemType.Mercari | Regular listing (individual seller) | | ItemType.Beyond | Mercari Shops listing |

ItemCondition

| Value | ID | Japanese | English | | ------------------------ | --- | -------------------- | --------- | | ItemCondition.BrandNew | 1 | 新品、未使用 | Brand new | | ItemCondition.LikeNew | 2 | 未使用に近い | Like new | | ItemCondition.Good | 3 | 目立った傷や汚れなし | Good | | ItemCondition.Fair | 4 | やや傷や汚れあり | Fair | | ItemCondition.Poor | 5 | 傷や汚れあり | Poor | | ItemCondition.Bad | 6 | 全体的に状態が悪い | Bad |

ShippingPayer

| Value | ID | Japanese | English | | ---------------------- | --- | -------- | --------------------------- | | ShippingPayer.Buyer | 1 | 着払い | Buyer pays | | ShippingPayer.Seller | 2 | 送料込み | Seller pays (free shipping) |

Color

| Value | ID | | -------------- | --- | | Color.Black | 1 | | Color.White | 2 | | Color.Gray | 3 | | Color.Brown | 4 | | Color.Red | 5 | | Color.Pink | 6 | | Color.Purple | 7 | | Color.Blue | 8 | | Color.Beige | 9 | | Color.Green | 10 | | Color.Yellow | 11 | | Color.Orange | 12 |

Reference Data

For facet IDs (categories, brands, sizes, etc.), see: https://github.com/take-kun/mercapi/tree/main/docs/facets

Top-Level Categories

| ID | Japanese | English | | --- | ------------------------ | ----------------- | | 1 | レディース | Women's | | 2 | メンズ | Men's | | 3 | ベビー・キッズ | Baby & Kids | | 4 | インテリア・住まい・小物 | Interior | | 5 | 本・音楽・ゲーム | Books/Music/Games | | 6 | おもちゃ・ホビー・グッズ | Toys/Hobbies | | 7 | コスメ・香水・美容 | Cosmetics | | 8 | 家電・スマホ・カメラ | Electronics | | 9 | スポーツ・レジャー | Sports | | 10 | ハンドメイド | Handmade | | 11 | チケット | Tickets | | 12 | 自動車・オートバイ | Vehicles | | 13 | その他 | Other |

Error Handling

All methods throw errors on API failures. Use try-catch for error handling:

import { Mercapi } from 'mercapi';

try {
  const results = await Mercapi.search('iPhone');
} catch (error) {
  // Error message format: "{Method} failed: {status} {statusText}"
  // e.g., "Search failed: 500 Internal Server Error"
  console.error(error.message);
}

Return Values

| Method | Not Found | Error | | ------------------ | ---------------------------------------------- | ------------ | | search() | Returns { items: [], meta: { numFound: 0 } } | Throws Error | | getItem() | Returns null | Throws Error | | getProfile() | Returns null | Throws Error | | getSellerItems() | Returns { items: [], nextPageToken: '' } | Throws Error |

TypeScript

All types are exported for full TypeScript support:

import {
  // Main class
  Mercapi,
  createMercapi,

  // Options type
  type MercapiOptions,

  // Enums
  SortBy,
  SortOrder,
  ItemStatus,
  ItemCondition,
  ShippingPayer,
  Color,

  // Search types
  type SearchOptions,
  type SearchResult,
  type SearchResultItem,
  type SearchMeta,
  type SearchAuction,

  // Item types
  type Item,
  type Seller,
  type ItemCategory,
  type ItemConditionInfo,
  type ItemBrand,
  type ShippingPayerInfo,
  type ShippingMethodInfo,
  type ShippingFromArea,
  type ShippingDuration,
  type Comment,
  type AuctionInfo,

  // Profile types
  type Profile,
  type SellerItem,
  type SellerItems,
} from 'mercapi';

Type Examples

import { Mercapi, SearchResult, Item, Profile } from 'mercapi';

// Function with typed return
async function searchItems(keyword: string): Promise<SearchResult> {
  return Mercapi.search(keyword);
}

// Nullable item
async function getItemSafe(id: string): Promise<Item | null> {
  return Mercapi.getItem(id);
}

// Type guard for auction items
function isAuctionItem(item: Item): boolean {
  return item.auctionInfo !== undefined;
}

Integration Examples

Express.js API

import express from 'express';
import { Mercapi, SortBy, SortOrder } from 'mercapi';

const app = express();

app.get('/api/search', async (req, res) => {
  try {
    const { q, minPrice, maxPrice, page } = req.query;

    const results = await Mercapi.search(q as string, {
      priceMin: minPrice ? Number(minPrice) : undefined,
      priceMax: maxPrice ? Number(maxPrice) : undefined,
      pageToken: page as string,
      sortBy: SortBy.CreatedTime,
      sortOrder: SortOrder.Desc,
    });

    res.json(results);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/api/items/:id', async (req, res) => {
  try {
    const item = await Mercapi.getItem(req.params.id);
    if (!item) {
      return res.status(404).json({ error: 'Item not found' });
    }
    res.json(item);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

AWS Lambda

import { Mercapi } from 'mercapi';

export const handler = async (event: any) => {
  const keyword = event.queryStringParameters?.q || '';

  try {
    // Static methods are ideal for Lambda - fresh keys each invocation
    const results = await Mercapi.search(keyword);

    return {
      statusCode: 200,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(results),
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: error.message }),
    };
  }
};

Batch Processing

import { Mercapi } from 'mercapi';

async function batchFetchItems(itemIds: string[]) {
  // Use instance with key reuse for batch operations
  const client = new Mercapi({ reuseKeys: true });

  const results = await Promise.all(itemIds.map((id) => client.getItem(id)));

  return results.filter((item): item is NonNullable<typeof item> => item !== null);
}

Monitoring New Listings

import { Mercapi, SortBy, SortOrder } from 'mercapi';

async function monitorNewListings(keyword: string, callback: (items: any[]) => void) {
  let lastSeenId: string | undefined;

  setInterval(async () => {
    try {
      const results = await Mercapi.search(keyword, {
        sortBy: SortBy.CreatedTime,
        sortOrder: SortOrder.Desc,
      });

      const newItems = lastSeenId
        ? results.items
            .filter((item) => item.id !== lastSeenId)
            .slice(
              0,
              results.items.findIndex((i) => i.id === lastSeenId)
            )
        : results.items;

      if (newItems.length > 0) {
        lastSeenId = results.items[0].id;
        callback(newItems);
      }
    } catch (error) {
      console.error('Monitor error:', error);
    }
  }, 60000); // Check every minute
}

Known Issues

Key Rotation

The original Python mercapi has a known issue where search results may differ from browser results after extended use. This is likely due to stale ECDSA keys.

Our solution: By default, this library generates fresh keys for each request, avoiding this issue entirely. This is ideal for serverless environments where each invocation is independent.

If you need to reuse keys (e.g., for performance in batch operations), use reuseKeys: true and call rotateKeys() periodically.

Credits

This project is a TypeScript/Node.js port of mercapi by take-kun, which pioneered the reverse-engineering of the Mercari Japan API, including the DPoP request signing scheme and the auction/shop item response formats. All API insights originate from that project — please consider starring it if you find this library useful.

License

MIT