Skip to content

Repository files navigation

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

About

TypeScript client for the Mercari Japan API (Node.js port of mercapi)

Topics

Resources

Stars

26 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages