Review analysis becomes useful when every theme can be traced back to its evidence. A count such as “23 durability complaints” is incomplete without the ASIN, variation, marketplace, rating, review date, and the text that matched the rule.
This tutorial builds a Node.js pipeline that collects controlled review samples, keeps missing fields explicit, applies an auditable keyword taxonomy, and outputs both aggregates and supporting records.
Design the Sample Before Calling the API
The Amazon Reviews List API accepts one ASIN and a marketplace domainCode. The current documentation lists 15 marketplaces: com, ca, co.uk, in, de, fr, it, es, co.jp, com.au, com.br, nl, se, com.mx, and ae.
Separate fields request one- through five-star reviews. Each defaults to 10 and currently supports up to 100. That structure makes a balanced sample possible instead of relying only on the most visible reviews.
export NEXSCOPE_API_KEY="nk_your_key_here"
export AMAZON_ASINS="B072MQ5BRX,B0XXXXXXXXX"
For a real competitor study, choose ASINs with a comparable use case, price band, size, and audience. A larger sample does not repair a mismatched comparison set.
Build the Review Client
The endpoint uses a bearer-authenticated POST request. Keep the credential on the server and send ASINs sequentially until the account's actual limits are known.
const API_KEY = process.env.NEXSCOPE_API_KEY;
const ENDPOINT =
"https://api.nexscope.ai/api/skill-api/v1/skills/amazon-reviews-list/run";
if (!API_KEY) throw new Error("Set NEXSCOPE_API_KEY first");
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function fetchReviews(body, maxAttempts = 3) {
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
let response;
try {
response = await fetch(ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
} catch (error) {
lastError = error;
if (attempt < maxAttempts) {
await wait(500 * 2 ** (attempt - 1));
continue;
}
break;
}
if (response.ok) return response.json();
const message = await response.text();
const retryable = response.status === 429 || response.status >= 500;
if (!retryable) throw new Error(`Request failed (${response.status}): ${message}`);
lastError = new Error(`Transient API failure (${response.status})`);
if (attempt < maxAttempts) await wait(500 * 2 ** (attempt - 1));
}
throw lastError;
}
Collect a Controlled Sample
The request below deliberately includes every rating band and recent verified-purchase reviews. Those are sampling choices, not a universal standard.
async function collectAsin(asin, domainCode = "com") {
return fetchReviews({
asin,
domainCode,
star1Num: 25,
star2Num: 25,
star3Num: 40,
star4Num: 25,
star5Num: 25,
filterByKeyword: "",
sortBy: "recent",
reviewerType: "avp_only_reviews",
mediaType: "all_contents",
formatType: "all_formats",
});
}
const asins = process.env.AMAZON_ASINS
.split(",")
.map((value) => value.trim())
.filter(Boolean);
const rawRuns = [];
for (const asin of asins) {
rawRuns.push({ asin, payload: await collectAsin(asin, "com") });
}
Normalize Without Filling the Gaps
Documented review fields include reviewId, asin, title, text, rating, date, userName, verified, vine, numberOfHelpful, media URLs, product context, and variation fields. Optional values should stay null.
function extractRows(payload) {
if (Array.isArray(payload?.data)) return payload.data;
if (Array.isArray(payload?.columns)) return payload.columns;
if (Array.isArray(payload)) return payload;
return [];
}
function numberOrNull(value) {
if (value === null || value === undefined || value === "") return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function normalizeReview(row, requestedAsin, domainCode) {
return {
reviewId: row.reviewId ?? null,
asin: row.asin ?? requestedAsin,
title: row.title ?? null,
text: row.text ?? null,
rating: numberOrNull(row.rating),
date: row.date ?? null,
userName: row.userName ?? null,
verified: row.verified ?? null,
vine: row.vine ?? null,
helpful: numberOrNull(row.numberOfHelpful),
variationId: row.variationId ?? null,
variationList: Array.isArray(row.variationList) ? row.variationList : null,
domainCode: row.domainCode ?? domainCode,
imageUrls: Array.isArray(row.imageUrlList) ? row.imageUrlList : null,
videoUrls: Array.isArray(row.videoUrlList) ? row.videoUrlList : null,
};
}
const reviews = rawRuns.flatMap(({ asin, payload }) =>
extractRows(payload).map((row) => normalizeReview(row, asin, "com")),
);
Keep the raw payload with a run ID. The fallback extractor is defensive; it does not guarantee that every response uses the same wrapper.
Apply an Explicit Theme Taxonomy
For a first pass, a small regular-expression taxonomy is easier to audit than an opaque sentiment score. The rules below are examples and must be adapted to the category and language.
const THEMES = {
durability: /\b(broke|broken|crack(?:ed)?|fell apart|stopped working)\b/i,
fit: /\b(too small|too large|too tight|too loose|does not fit)\b/i,
battery: /\b(battery|charge|charging|battery life)\b/i,
packaging: /\b(box|packaging|damaged on arrival|missing parts?)\b/i,
instructions: /\b(instructions?|manual|setup|hard to assemble)\b/i,
};
function classify(review) {
const evidence = [review.title, review.text].filter(Boolean).join("\n");
const themes = Object.entries(THEMES)
.filter(([, pattern]) => pattern.test(evidence))
.map(([name]) => name);
return { ...review, themes };
}
const classified = reviews.map(classify);
A keyword match is an indicator, not a verdict. Negation, sarcasm, quoted text, and category-specific language can produce false positives. Inspect supporting records before changing a product or listing.
Aggregate Counts and Preserve Evidence
The report keeps both the summary and a limited evidence sample for each theme. It also records the collection design so a later run can be compared fairly.
function aggregateByTheme(rows) {
const output = {};
for (const review of rows) {
for (const theme of review.themes) {
output[theme] ??= { mentions: 0, ratings: [], evidence: [] };
output[theme].mentions += 1;
if (review.rating !== null) output[theme].ratings.push(review.rating);
if (output[theme].evidence.length < 5) {
output[theme].evidence.push({
reviewId: review.reviewId,
asin: review.asin,
variationId: review.variationId,
rating: review.rating,
date: review.date,
verified: review.verified,
text: review.text,
});
}
}
}
return Object.fromEntries(
Object.entries(output).map(([theme, value]) => [theme, {
mentions: value.mentions,
averageRating: value.ratings.length
? Number((value.ratings.reduce((a, b) => a + b, 0) / value.ratings.length).toFixed(2))
: null,
evidence: value.evidence,
}]),
);
}
const report = {
schemaVersion: 1,
collectedAt: new Date().toISOString(),
requestedAsins: asins,
domainCode: "com",
sampleDesign: {
sortBy: "recent",
reviewerType: "avp_only_reviews",
requestedByStars: [25, 25, 40, 25, 25],
},
receivedReviews: classified.length,
themes: aggregateByTheme(classified),
};
console.log(JSON.stringify(report, null, 2));
For large reports, store full text in access-controlled storage and put only record IDs and short excerpts in downstream dashboards. Review data can contain user-supplied text and names; minimize retention to what the research task requires.
Interpret the Output Carefully
- The requested count is not necessarily the number returned.
- A balanced star sample is not the listing's natural rating distribution.
-
verified, Vine status, helpful votes, and media are segmentation signals, not authenticity scores. - Keep variation and marketplace context; do not generalize one variant's defect to an entire family.
- Recent-review results depend on the collection date and the endpoint's available coverage.
- A theme correlation does not prove the cause of returns, rating changes, or sales movement.
- Source, freshness, coverage, and fixed rate limits should not be assumed when the API definition does not specify them.
The goal is a reproducible evidence queue for human review—not an automatic claim about customers or competitors.
Next Step
The Amazon Reviews List API provides controlled review collection by marketplace, rating count, sort order, reviewer type, media type, and format.
Build with the Amazon Reviews List API →
Disclosure: This article was prepared with AI-assisted editing using the current published API documentation as its technical source of truth.


Top comments (0)