/amazon-product-statistics returns its buyBoxPrice as an integer in minor units: 41800 means 418.00. /amazon-product-lookup and /amazon-product-offers return the same kind of price already in decimal currency units, such as 418.00, and /amazon-product-offers additionally returns an unrounded float sum for buyBox.totalPrice. Mixing these without converting first makes a total come out 100 times too high or carries a float tail into a stored price.
Key Takeaways
- /amazon-product-statistics buyBoxPrice is an integer in cents (41800 = 418.00), or the string "N/A" when nobody holds the Buy Box.
- /amazon-product-lookup productInfo.buyBoxInfo.price is already decimal currency units (418.00), or "N/A" with no featured offer.
- /amazon-product-offers buyBox.totalPrice is decimal units but an unrounded float sum of price and shipping (444.97999999999996); offers[].totalPrice is rounded, buyBox.totalPrice is not.
- Skipping the statistics /100 conversion is the specific bug class that makes a mixed-endpoint price total 100 times too high; it never raises an error.
- Each endpoint shapes "nobody holds the Buy Box" differently: a string "N/A", another string "N/A", or an empty buyBox object with buyBoxSuppressed true.
Which Amazon API endpoint returns price in cents, and which in decimal?
Only one of the 3 endpoints that carry a Buy Box price uses minor units. /amazon-product-statistics buyBoxPrice is an integer: 41800 means 418.00, divide by 100 to get the decimal amount. /amazon-product-lookup productInfo.buyBoxInfo.price and /amazon-product-offers buyBox.totalPrice are already decimal currency units, so dividing those by 100 would be the opposite mistake. The table below is the full reference for all 3 fields.
| Endpoint | Field | Raw example | Format | Normalised |
|---|---|---|---|---|
| /amazon-product-statistics | buyBoxPrice | 41800 | integer, minor units (cents) | 418.00 |
| /amazon-product-lookup | productInfo.buyBoxInfo.price | 418.0 | decimal currency units | 418.00 |
| /amazon-product-offers | buyBox.totalPrice | 444.97999999999996 | decimal, unrounded float sum | 444.98 |

offers.buyBox.totalPrice needs rounding, not a unit conversion
buyBox.totalPrice from /amazon-product-offers is already in decimal currency units, so it needs no /100 conversion, but it is a raw float sum of the item price plus shipping and can read as 444.97999999999996 instead of 444.98. offers[].totalPrice, the field on each individual offer in the same response, is rounded to 2 decimals by the backend; buyBox.totalPrice is not, so round it on the client before storing or displaying it, the same round(x, 2) used for the other 2 fields in the table above.
The same ASIN's Buy Box price read from all 3 endpoints side by side (illustrative values)
{
"amazon-product-statistics": {"buyBoxPrice": 41800},
"amazon-product-lookup": {"productInfo": {"buyBoxInfo": {"price": 418.0}}},
"amazon-product-offers": {"buyBox": {"totalPrice": 418.0}}
}
No Buy Box holder: three different null shapes
When nobody currently holds the Buy Box, each endpoint says so in its own way: /amazon-product-statistics returns the string "N/A" for buyBoxPrice, /amazon-product-lookup returns the string "N/A" for productInfo.buyBoxInfo.price, and /amazon-product-offers returns buyBoxSuppressed: true with buyBox as an empty object, no totalPrice key at all. A price reader that only checks if price: can still crash or silently return 0 on 3 different shapes of the same real-world case; check the type (statistics, lookup) or the flag and key presence (offers) instead.
How do I reconcile all 3 formats in one script?
Normalise each endpoint's price with its own rule, then compare: /100 for statistics, pass-through for lookup, round-to-2dp for offers. The script below calls all 3 endpoints for a list of ASINs, normalises each price, and flags a result where the 3 normalised prices disagree by more than a 5x factor, the signature of a unit conversion that was missed somewhere upstream. get_with_retry and new_session come from retry.py and sellermagnet.py, both published in the Python quickstart.
Install requests, set the key, run the script
pip install requests
export SELLERMAGNET_API_KEY="your-key-here"
python3 reconcile_prices.py
reconcile_prices.py: normalise the Buy Box price from 3 endpoints and flag a unit mismatch
"""Reconcile Amazon Buy Box prices across 3 SellerMagnet endpoints that use 3 different price formats.
/amazon-product-statistics buyBoxPrice is an INTEGER in minor units (cents): 41800 means 418.00. A client that
treats that integer as if it were already a decimal amount reports a price 100x too high. /amazon-product-lookup
productInfo.buyBoxInfo.price and /amazon-product-offers buyBox.totalPrice are already decimal currency units
(444.99), but offers.totalPrice is a raw float sum of item price + shipping and is not pre-rounded (it can read
444.97999999999996), so it needs client-side rounding, not a unit conversion.
Helpers: sellermagnet.py and retry.py from https://sellermagnet-api.com/blog/amazon-product-api-python
"""
import re
from collections import Counter
import requests
from retry import get_with_retry
from sellermagnet import ApiError, new_session
MARKETPLACE = "ATVPDKIKX0DER" # amazon.com
ASIN_FILE = "asins.txt" # one ASIN per line
MAX_FAILED = 3 # 404/502/503 are billed on every attempt: stop after 3 ASINs that keep failing
def read_asins(path):
asins, rejected = [], []
with open(path, encoding="utf-8-sig") as fh:
for line in fh:
value = line.strip().upper()
if value and not re.fullmatch(r"[A-Z0-9]{10}", value):
rejected.append(value) # billed anyway: statistics 400s it, lookup/offers send it upstream
elif value and value not in asins:
asins.append(value) # each ASIN once: a repeat would cost 3 more credits
return asins, rejected
def stats_price(data):
"""/amazon-product-statistics buyBoxPrice: integer minor units, or the string 'N/A' with no Buy Box holder."""
raw = data.get("buyBoxPrice")
if not isinstance(raw, int): # "N/A" (string) when nobody holds the Buy Box
return None, raw
return round(raw / 100, 2), raw
def lookup_price(data):
"""/amazon-product-lookup productInfo.buyBoxInfo.price: decimal units already, or 'N/A' with no featured offer."""
raw = (data.get("productInfo") or {}).get("buyBoxInfo", {}).get("price")
if not isinstance(raw, (int, float)): # "N/A" (string)
return None, raw
return round(float(raw), 2), raw
def offers_price(data):
"""/amazon-product-offers buyBox.totalPrice: decimal units, unrounded float sum; {} when buyBoxSuppressed."""
box = data.get("buyBox") or {}
if data.get("buyBoxSuppressed") or "totalPrice" not in box:
return None, box
return round(box["totalPrice"], 2), box["totalPrice"]
# A mismatch beyond this factor between two endpoints that both claim a current Buy Box price almost always means
# one of them was read as the wrong format (most commonly: statistics cents used without the /100 conversion).
MISMATCH_FACTOR = 5.0
def reconcile(asin, stats, lookup, offers):
s_price, s_raw = stats_price(stats)
l_price, l_raw = lookup_price(lookup)
o_price, o_raw = offers_price(offers)
prices = [p for p in (s_price, l_price, o_price) if p is not None]
suspect = False
if len(prices) >= 2:
suspect = max(prices) / min(prices) >= MISMATCH_FACTOR if min(prices) > 0 else False
return {
"asin": asin,
"statistics": s_price, "statistics_raw": s_raw,
"lookup": l_price, "lookup_raw": l_raw,
"offers": o_price, "offers_raw": o_raw,
"suspect": suspect,
}
def row(r):
def cell(price, raw):
if price is None:
return f"- (raw {raw!r})"
return f"{price:.2f} (raw {raw!r})"
flag = " <-- CHECK UNITS" if r.get("suspect") else ""
print(f"{r['asin']:<10} stats={cell(r['statistics'], r['statistics_raw']):<28} "
f"lookup={cell(r['lookup'], r['lookup_raw']):<22} offers={cell(r['offers'], r['offers_raw'])}{flag}")
def summary(rows):
print()
held = [r for r in rows if r["statistics"] is not None]
print(f"ASINs checked: {len(rows)}, Buy Box held per statistics: {len(held)}, "
f"flagged for a unit mismatch: {sum(r['suspect'] for r in rows)}")
def main():
asins, rejected = read_asins(ASIN_FILE)
if rejected:
print("skipped, not an ASIN: " + ", ".join(rejected))
session = new_session()
rows, failed, gates = [], 0, Counter()
for asin in asins:
try:
stats = get_with_retry(session, "amazon-product-statistics", asin=asin, marketplaceId=MARKETPLACE)
lookup = get_with_retry(session, "amazon-product-lookup", asin=asin, marketplaceId=MARKETPLACE)
offers = get_with_retry(session, "amazon-product-offers", asin=asin, marketplaceId=MARKETPLACE)
except ApiError as err:
if err.status in (400, 401, 403): # parameter, key or credits problem: it repeats on every ASIN
print(f"stopped: {err}")
break
failed += 1
gates[err.status] += 1
print(f"{asin:<10} error {err.status}: {err}")
if failed >= MAX_FAILED:
print(f"stopped after {failed} failed ASINs")
break
continue
except requests.RequestException as err:
failed += 1
print(f"{asin:<10} {type(err).__name__}")
if failed >= MAX_FAILED:
print(f"stopped after {failed} failed ASINs")
break
continue
r = reconcile(asin, stats, lookup, offers)
rows.append(r)
row(r)
summary(rows)
if __name__ == "__main__":
main()
Output of reconcile_prices.py (illustrative ASINs: a Buy Box held, no holder, and an unrounded float)
B0CL61F39H stats=418.00 (raw 41800) lookup=418.00 (raw 418.0) offers=418.00 (raw 418.0)
B0D1XYZ002 stats=- (raw 'N/A') lookup=- (raw 'N/A') offers=- (raw {})
B0D1XYZ003 stats=444.98 (raw 44498) lookup=444.98 (raw 444.98) offers=444.98 (raw 444.97999999999996)
ASINs checked: 3, Buy Box held per statistics: 2, flagged for a unit mismatch: 0

A missed /100 never raises an error
Forgetting to divide statistics buyBoxPrice by 100 produces a valid-looking number, not an exception, so it reaches a report or a repricer unnoticed until someone spots a price 100 times too high. The reconcile() sanity check above compares the 3 normalised prices and flags any pair more than 5x apart, which catches this specific bug class before it ships.
What does the API charge for checking all 3 endpoints?
Each endpoint is billed on its own at 1 credit per request, so reconciling one ASIN across statistics, lookup and offers costs 3 credits. /amazon-product-statistics is the one endpoint of the 3 that validates ASIN format and bills a 400 for a malformed one; /amazon-product-lookup and /amazon-product-offers send a malformed ASIN upstream and bill it like any other request, so filtering obviously bad ASINs client-side, as read_asins() does above, saves credits on all 3 calls, not just one.
| Status | Meaning | Billed |
|---|---|---|
| 200 | all 3 calls answered | yes, 3 credits |
| 400 gate | missing or invalid marketplaceId | no |
| 400 endpoint (statistics only) | malformed ASIN | yes |
| 401 / 403 | key or credits | no |
| 404 | ASIN not on this marketplace | yes, not retried |
| 429 | per-key concurrency cap | no |
| 500 | internal error | no, refunded, retried once |
| 502 / 503 | upstream error | yes, every attempt |
Frequently Asked Questions
Why is the Buy Box price from /amazon-product-statistics 100 times too high?
buyBoxPrice from /amazon-product-statistics is an integer in minor units (cents): 41800 means 418.00. Divide by 100 before using it as a decimal amount, or treat it as if it were already dollars and cents and every total comes out 100 times too high.
Do /amazon-product-lookup and /amazon-product-offers also return cents?
No. productInfo.buyBoxInfo.price from /amazon-product-lookup and buyBox.totalPrice from /amazon-product-offers are already decimal currency units, such as 418.00. Only /amazon-product-statistics uses minor units for buyBoxPrice.
Why does offers.buyBox.totalPrice need rounding if it is already decimal?
buyBox.totalPrice is an unrounded float sum of the item price plus shipping, so it can read as 444.97999999999996. offers[].totalPrice (the full offer list) is rounded to 2 decimals by the backend; buyBox.totalPrice is not, so round it client-side.
What does a price field look like when nobody holds the Buy Box?
Each endpoint shapes it differently: /amazon-product-statistics returns the string "N/A" for buyBoxPrice, /amazon-product-lookup returns the string "N/A" for productInfo.buyBoxInfo.price, and /amazon-product-offers returns an empty object for buyBox with buyBoxSuppressed set to true. Check the type or the flag, not just truthiness.
Does checking all 3 endpoints for one ASIN cost more credits?
Yes. Each endpoint is billed separately at 1 credit per request, so reconciling the same ASIN across statistics, lookup and offers costs 3 credits total, the same as calling each one once on its own.
Bottom line: /amazon-product-statistics buyBoxPrice is cents, divide by 100; /amazon-product-lookup and /amazon-product-offers are already decimal, but offers needs rounding; and all 3 shape a missing Buy Box differently. Normalise each one with its own rule before comparing or summing them. A free account comes with 150 credits after e-mail verification, and every field above is in the API documentation.