Skip to main content

amiami-api

An unofficial AmiAmi API: a Python library plus a local MCP server, so agents can search Japan's largest online figure and hobby retailer, read live prices and stock, and join it to MyFigureCollection by barcode.

AmiAmi's storefront runs on a JSON API that has never been documented or announced. This package speaks it directly.

New here? The short version

AmiAmi is where a large share of the world's anime figures are actually bought — mostly as preorders, months before release. There is no official API, the existing wrappers are unmaintained toys, and the whole thing sits behind Cloudflare.

This project gives you a working client on your own computer. An AI assistant like Claude can then answer things like "is this Nendoroid still up for preorder, and what does it cost?" — and, because AmiAmi publishes JAN barcodes, tie that answer to the same figure on MyFigureCollection.

sequenceDiagram
    participant You
    participant Claude
    participant mfc as mfc-api
    participant ami as amiami-api
    You->>Claude: What's this figure going for?
    Claude->>mfc: search_by_barcode(4570232589346)
    mfc-->>Claude: MFC item 287 + metadata
    Claude->>ami: find_by_jan(4570232589346)
    ami-->>Claude: FIGURE-205113 · Pre-order · ¥9,900
    Claude-->>You: Still up for preorder at AmiAmi, ¥9,900 (¥11,000 taxed)

What is MCP? The Model Context Protocol is a standard plug for giving AI assistants new abilities. An MCP server is a small program on your machine that an assistant is allowed to call. This one gives your assistant three AmiAmi abilities.

The one thing to understand: availability has seven states

AmiAmi sells the same catalogue three ways — stock on hand, open preorders, and second-hand — and a boolean is wrong for all of them in different ways.

availability What it means
in_stock On the shelf, ships now.
preorder Not made yet; orderable, with a release month.
provisional_preorder AmiAmi will take the order, but release date and sometimes price aren't fixed.
backorder Orderable on back-order. Real, but AmiAmi seems to have wound these down — the filter returned 0 results on every keyword tried.
order_closed The order window shut.
unavailable Released and no longer orderable. Modelled, but not once observed in the wild — see below.
unknown Not checked, or unreadable. Never treat this as available.

unavailable is defined because the state logically exists, but every product that /item knows about was also findable in search with a real stock flag, and every code that isn't returns "Invalid Request" (a AmiAmiNotFoundError, not an availability). AmiAmi appears to delist rather than mark-as-gone. If you see unavailable in the wild, that is new behaviour worth reporting.

condition (new / pre_owned) is a separate axis. A pre-owned item can be in stock and a new one can be gone. Collapsing them is how you end up offering someone a used figure when they asked for a sealed one.

Three fields that lie, and two endpoints that disagree

Verified live on 2026-07-31, and the reason this package doesn't just forward AmiAmi's JSON:

  • soldout_flg is 1 on open preorders you can add to a cart right now. It seems to mean "no physical stock on hand", which is trivially true of anything unreleased. Ignored here.

  • stock is 1 on both an orderable preorder and a long-gone item. It is not a quantity and not a buyability boolean. Also ignored.

  • instock_flg from /item is 0 for every released product, in stock or not — while the same product reports 1 in search. Checked within the same minute for FIGURE-191979:

    Endpoint instock_flg
    /items (search) 1
    /item?gcode=FIGURE-191979 0

So item() makes a second request against search and takes availability from there. Without it the answer is unknown with availability_confirmed: false — never a confident unavailable. confirm_stock=False opts out and keeps the honest unknown.

A client that trusts any of these three gets the answer backwards on most of the catalogue.

Search never returns unavailable items

All 158 sampled result rows had order_closed_flg: 0. Sold-out products are absent from search entirely — so an empty result means "nothing buyable matched", not "no such product". To see something retired, look it up by gcode.

And when you do, note that AmiAmi returns the same error for a retired product and a code that never existed (RSuccess: false, "Invalid Request"). It does not distinguish them, so neither does this package.

Architecture

api.amiami.com blocks on TLS fingerprint. httpx gets a 403 challenge page no matter what headers it sends. This package uses curl_cffi with impersonate="chrome" — the same trick that makes myfigurecollection-api work. That choice is load-bearing.

The profile has to be current, not merely present:

Profile Result
httpx (no impersonation) 403 Cloudflare
curl_cffi, no profile 403 Cloudflare
chrome110 403 Cloudflare
chrome (latest), safari17_0, firefox133 200

If this starts raising AmiAmiBlockedError, try a newer profile before assuming the API is gone.

Cloudflare also refuses a perfectly valid request now and then — roughly 1 in 6 cold requests, measured over six fresh sessions on 2026-07-31. It's transient, so the transport retries before giving up; AmiAmiBlockedError only surfaces when every attempt is challenged. Don't treat a single challenge in your own logs as evidence of anything.

Two things folklore says you need and you don't: no session warm-up (both endpoints answer cold — hitting amiami.com first is pointless, since the storefront is more walled than the API and 403s even under impersonation), and no cookies.

One thing you do need: the X-User-Key: amiami_dev header. It isn't a secret or per-user — it's a constant the public site ships. Curiously, omitting it gets you a Cloudflare challenge rather than an API error, while sending a wrong one gets a clean HTTP 400 (ErrorCode 10210). This package raises different exceptions for the two, because they need different fixes.

HTTP still sits behind an ABC, same seam as mandarake-api:

class Transport(ABC):
    @abstractmethod
    def get(self, url: str, **kw) -> Response: ...

Parsers take response bodies as strings and never touch the network.

Install

Needs Python 3.10+. The package is on PyPI:

pip install amiami-api

On a Mac, python3 is often Xcode's 3.9, which will not run this code, so be explicit about the interpreter (e.g. pip3.12). Working from a clone instead, use pip3.12 install -e ..

Then check it landed:

amiami-api search "hatsune miku"

Use it as an MCP server

amiami-api with no arguments starts the MCP server on stdio. Add this to your MCP config — ~/Library/Application Support/Claude/claude_desktop_config.json for Claude Desktop, or .mcp.json in your project for Claude Code:

{
  "mcpServers": {
    "amiami": {
      "command": "uvx",
      "args": ["amiami-api"]
    }
  }
}

This needs uv installed; uvx fetches the package from PyPI on first run, so there is nothing else to set up.

No uv? Point command at the installed script by its absolute path, e.g. /Library/Frameworks/Python.framework/Versions/3.12/bin/amiami-api. The path must be absolute because MCP clients launch servers with a minimal PATH that does not include your shell's Python bin directory — a bare "command": "amiami-api" will fail even though it works in your terminal.

Tools

Tool What it returns
search(keyword, lang, page, per_page, preorder_only, in_stock_only, preowned_only) Listings with price, availability, condition and JAN
item(gcode, lang) Full record: both names, prices, JAN, spec, sculptors, series/character tags
find_by_jan(jan, lang) Barcode lookup — the join key to MyFigureCollection

Configuration

Environment variable Default Meaning
AMIAMI_API_RATE_LIMIT 1.0 Seconds between requests
AMIAMI_API_CACHE_TTL 3600 Cache lifetime in seconds; 0 disables
AMIAMI_API_CACHE_DIR ~/.cache/amiami-api Where cached responses live
AMIAMI_API_IMPERSONATE chrome curl_cffi TLS profile
AMIAMI_API_USER_KEY amiami_dev The X-User-Key header
AMIAMI_API_LANG eng Default store (eng or jpn)
AMIAMI_API_LOG_LEVEL WARNING Server log level (logs go to stderr)

Every request is logged at DEBUG with its URL, status and body size.

Use it as a library

from amiami_api import AmiAmiClient, Availability, Condition

with AmiAmiClient() as amiami:
    results = amiami.search("hatsune miku", preorder_only=True)
    print(results.pagination.total_results)

    for listing in results.items:
        print(listing.availability.value, listing.price_jpy, listing.name)

    # Barcode in, live price out — the join to MyFigureCollection
    for match in amiami.find_by_jan("4570232589346"):
        print(match.gcode, match.price_jpy, match.availability.value)

    item = amiami.item("FIGURE-205113")
    print(item.name, "/", item.name_ja)
    print(item.availability is Availability.PREORDER, item.condition is Condition.NEW)
    print(item.sculptors, item.release_date)

lang picks the store, and the store decides your keyword's script

eng is the international store, jpn the Japanese one. They hold different products — but the trap is the keyword, not the catalogue. Each store indexes names in its own writing system, so a keyword in the wrong script silently under-matches. Measured 2026-07-31:

keyword lang=eng lang=jpn
nendoroid 7,527 4
ねんどろいど 1,236 12,670
miku 11,282 459
初音ミク 5,476 11,037

Searching nendoroid against the Japanese store returns 4 results and looks like a working query. Match the script to the store.

Use it from the shell

amiami-api search "hatsune miku" --preorder
amiami-api item FIGURE-205113
amiami-api jan 4570232589346
amiami-api search miku --preowned --lang jpn

Everything prints JSON on stdout; logs go to stderr. Add -v for request logs.

A note on find_by_jan

AmiAmi has no barcode endpoint. find_by_jan searches the barcode as a keyword and then keeps only exact jancode matches.

That filter is insurance, not a fix for observed misbehaviour: barcode search was exact in every case tested on 2026-07-31 (five JANs, zero non-matching rows). It stays because keyword search is not documented to be exact, and silently pairing a barcode with a neighbouring product is the kind of error you only notice after buying the wrong figure.

It returns a list, because one barcode can match more than one listing — 4570157184640 returns two.

Being a good citizen

  • Rate-limited to 1 request/second by default, process-wide.
  • Responses are cached on disk for an hour.
  • Read-only. No cart, order, account or write operations, and none are planned.

This is an undocumented API belonging to a real shop. Don't hammer it, and don't build anything that competes with them on their own data.

Tests

python3.12 -m pytest

The default run is entirely offline — parser tests read saved API responses captured on 2026-07-31, and the transport is driven with a fake session. Tests that hit the live API are marked live:

python3.12 -m pytest -m live

Those are the canary. They fail loudly and specifically if the TLS profile goes stale, if X-User-Key stops working, if the JSON envelope changes shape, or if the misleading flags start meaning what they say.

Disclaimer

Unofficial. Not affiliated with, endorsed by, or supported by AmiAmi or Oh-ami Inc. It calls an undocumented, unversioned API and will break without notice. Product data and images belong to AmiAmi.

MIT licensed — see LICENSE.

Built by Sara Kay.

mcp-name: io.github.ssskay/amiami-api

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

amiami_api-0.1.0.tar.gz (55.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

amiami_api-0.1.0-py3-none-any.whl (34.6 kB view details)

Uploaded Python 3

File details

Details for the file amiami_api-0.1.0.tar.gz.

File metadata

  • Download URL: amiami_api-0.1.0.tar.gz
  • Upload date:
  • Size: 55.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.4

File hashes

Hashes for amiami_api-0.1.0.tar.gz
Algorithm Hash digest
SHA256 cdf5e7d7f06394a33d98e8cf50ad7b55d903475d98d75ead9b66e00504502b9e
MD5 cb866891978e9c63655c2790bf473df8
BLAKE2b-256 04ceb34dfe06452da502b63c7fc2d96c67c7e5a0f314c077b1084ecbca84e374

See more details on using hashes here.

File details

Details for the file amiami_api-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: amiami_api-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 34.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.4

File hashes

Hashes for amiami_api-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e52f8a096291e233ade790971692ad3ac8add3c79f13288878725055c9ba6995
MD5 cffc0dd23e8326f3b0f615957e5a6b21
BLAKE2b-256 99777a0792f1327ebaa9410de557308ba410d8bad50433d8791402560d900c6c

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page