Integration Guide
How to Track Amazon Competitor Prices
Build a price-tracking pipeline for Amazon competitor products: batch price checks, marketplace and location pitfalls, MAP-violation alerts, and handling partial failures without breaking the run.
Written by Alex P.
- Amazon price tracking
- competitor pricing
- price monitoring
- MAP monitoring
- ecommerce
Checking a competitor’s Amazon listing by hand works fine for one product. It stops working the moment you have twenty products across a handful of competitors and want to know the same day a price moves, not the week you happen to remember to look.
This is a batch problem, not a scraping problem: you already know which ASINs you care about, you want their current price on a schedule, and you want to be told when something changes — a price drop, a price hike, or a reseller quietly going below the price you told them to hold.
The architecture
Cron → price-check up to 20 ASINs at once → diff against last known price → alert on change
The only real design decision is which mode to poll with, because it decides both your cost per check and what you can detect.
fast vs. full: pick fast for polling
Amazon’s price-check route has two modes, and they answer different questions:
fast(the default) prices the whole batch from a single listing per product, at low cost. You get price and whether an Add to Cart offer exists.fullreads every product’s page individually for seller and stock status too, at a much higher cost per product.
For a recurring poll, fast is the right default — it returns the same price either way. Reach for full only when you specifically need to know who is selling at that price or whether it’s in stock, not just what the price is.
const res = await fetch('https://api.fetchlayer.dev/amazon/price-check', {
method: 'POST',
headers: {
'Authorization': 'Bearer ss-your-api-key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
asins: ['B08N5WRWNW', 'B07XJ8C8F5'],
mode: 'fast',
marketplace: 'com',
postalCode: '10001',
}),
});
const { results } = await res.json();
Prices depend on where the order ships — pin it down
Every priced route takes a marketplace (the domain suffix — com, co.uk, de, and so on) and a postalCode, and the response reports which deliveryLocation the prices apply to. This isn’t a formality: Amazon prices by delivery location, so if you poll without pinning a postal code you can end up comparing today’s price at one default location against yesterday’s price at a different one and conclude something changed when it didn’t.
Pick one marketplace + postalCode pair per tracked product and keep it fixed across every poll. If a product doesn’t ship to that location at all, it comes back with no offer rather than a price — check for that explicitly rather than treating a missing price as a scrape failure.
A working price monitor
import { readFile, writeFile } from 'node:fs/promises';
const API_KEY = process.env.FETCHLAYER_API_KEY;
const STATE_FILE = './price-history.json';
const MARKETPLACE = 'com';
const POSTAL_CODE = '10001';
// Group your own ASINs against the competitors you're tracking them against.
const WATCHLIST = [
{ label: 'our-product', asin: 'B08N5WRWNW' },
{ label: 'competitor-a', asin: 'B07XJ8C8F5' },
{ label: 'competitor-b', asin: 'B09FZJ7QQK' },
];
async function checkPrices(asins) {
const res = await fetch('https://api.fetchlayer.dev/amazon/price-check', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
asins,
mode: 'fast',
marketplace: MARKETPLACE,
postalCode: POSTAL_CODE,
}),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
return (await res.json()).results;
}
async function run() {
let history = {};
try {
history = JSON.parse(await readFile(STATE_FILE, 'utf8'));
} catch {}
const results = await checkPrices(WATCHLIST.map((w) => w.asin));
const changes = [];
for (const item of results) {
const watched = WATCHLIST.find((w) => w.asin === item.asin);
const label = watched?.label ?? item.asin;
// One bad ASIN shouldn't block reading the rest of the batch.
if (item.status !== 'ok') {
console.warn(`${label}: ${item.status}, skipping this run`);
continue;
}
const previous = history[item.asin]?.price;
if (previous !== undefined && previous !== item.price.amount) {
changes.push({ label, asin: item.asin, from: previous, to: item.price.amount });
}
history[item.asin] = { price: item.price.amount, currency: item.price.currency, checkedAt: new Date().toISOString() };
}
await writeFile(STATE_FILE, JSON.stringify(history, null, 2));
if (changes.length) await alert(changes);
console.log(`Checked ${results.length}, ${changes.length} price change(s).`);
}
run().catch(console.error);
status is one of ok, unavailable, not_found, or failed per product — a batch of 20 where one ASIN is temporarily unavailable still returns the other 19 as ok. Skip the bad one and retry it next run rather than discarding the whole batch.
Catching MAP violations
If you sell through resellers and enforce a minimum advertised price, the same pipeline catches violations by comparing against a floor instead of a previous value:
const MAP_FLOORS = {
B08N5WRWNW: 49.99,
};
function checkMapViolations(results) {
return results
.filter((r) => r.status === 'ok' && MAP_FLOORS[r.asin] !== undefined)
.filter((r) => r.price.amount < MAP_FLOORS[r.asin])
.map((r) => ({ asin: r.asin, floor: MAP_FLOORS[r.asin], actual: r.price.amount }));
}
Run this against every poll alongside the price-change diff — a MAP violation is worth a same-day alert regardless of whether the price also moved from the last check.
Alerting
async function alert(changes) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `${changes.length} price change(s) detected`,
blocks: changes.map((c) => ({
type: 'section',
text: {
type: 'mrkdwn',
text: `*${c.label}* (${c.asin})\n$${c.from} → $${c.to}`,
},
})),
}),
});
}
Scaling past 20 ASINs
price-check takes up to 20 products per call, all on the same marketplace. Past that, batch:
function chunk(arr, size) {
const out = [];
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
return out;
}
async function checkAllPrices(asins) {
const batches = chunk(asins, 20);
const all = [];
for (const batch of batches) {
all.push(...(await checkPrices(batch)));
}
return all;
}
Run batches sequentially rather than in parallel unless you’ve confirmed your plan’s concurrency limits — a burst of simultaneous batches gains you little on a poll that only needs to run hourly or daily.
Practical notes
- There’s no reviews route, by design.
price-checkandproduct-detailsreturn the aggregate star rating and count, not review text. If you need review content, that’s a different lookup entirely. - Search caps at 20 pages. If you’re building the watchlist itself from a category search rather than a hand-picked ASIN list,
search-productswon’t walk past Amazon’s own 20-page ceiling — narrow the query rather than requesting more pages. - Read the
notesarray. Every response carries one, and it’s the only place a partial or substituted answer announces itself — a price quoted for a different location than you asked for, for instance.
Next Steps
- Amazon Product API — full endpoint reference and pricing
- Get a free API key — no credit card required