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_flgis1on 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. -
stockis1on both an orderable preorder and a long-gone item. It is not a quantity and not a buyability boolean. Also ignored. -
instock_flgfrom/itemis0for every released product, in stock or not — while the same product reports1in search. Checked within the same minute forFIGURE-191979:Endpoint instock_flg/items(search)1 /item?gcode=FIGURE-1919790
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cdf5e7d7f06394a33d98e8cf50ad7b55d903475d98d75ead9b66e00504502b9e
|
|
| MD5 |
cb866891978e9c63655c2790bf473df8
|
|
| BLAKE2b-256 |
04ceb34dfe06452da502b63c7fc2d96c67c7e5a0f314c077b1084ecbca84e374
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e52f8a096291e233ade790971692ad3ac8add3c79f13288878725055c9ba6995
|
|
| MD5 |
cffc0dd23e8326f3b0f615957e5a6b21
|
|
| BLAKE2b-256 |
99777a0792f1327ebaa9410de557308ba410d8bad50433d8791402560d900c6c
|