TikTok Shop rankings are snapshots, not conclusions. A useful pipeline records the market, requested date, raw response, optional fields, and retrieval time before it calculates any score or trend.
This Node.js example uses the current Nexscope TikTok New Product Rank API contract. It preserves missing values, retries transient failures, and compares two dated snapshots without treating correlation as causation.
Prerequisites
- Node.js 18 or later
- A server-side
NEXSCOPE_API_KEY - A supported region code and date in
YYYY-MM-DD
export NEXSCOPE_API_KEY="nk_your_key_here"
Keep the key on the server. Do not expose it in browser code or commit it to source control.
Request a Ranking Page
The documented endpoint is:
POST https://api.nexscope.ai/api/skill-api/v1/skills/tiktok-new-product-rank/run
const ENDPOINT = "https://api.nexscope.ai/api/skill-api/v1/skills/tiktok-new-product-rank/run";
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
async function fetchRanking({ region, date, pageNum = 1, pageSize = 50 }, attempts = 3) {
for (let attempt = 1; attempt <= attempts; attempt++) {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NEXSCOPE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ region, date, pageNum, pageSize }),
});
if (response.ok) return response.json();
const body = await response.text();
const retryable = response.status >= 500;
if (!retryable || attempt === attempts) {
throw new Error(`Ranking request failed (${response.status}): ${body.slice(0, 300)}`);
}
await wait(500 * 2 ** (attempt - 1));
}
}
The current documentation lists date as required. region, pageNum, and pageSize are optional. Region availability and limits should be read from the live endpoint reference rather than hard-coded from an old article.
Normalize Optional Fields
The response can include optional values. Preserve null instead of converting missing fields to zero.
const numberOrNull = value =>
typeof value === "number" && Number.isFinite(value) ? value : null;
const stringOrNull = value =>
typeof value === "string" && value.length ? value : null;
function normalizeProduct(product, context) {
return {
productId: stringOrNull(product.asin),
title: stringOrNull(product.title),
region: stringOrNull(product.region) ?? context.region,
requestedDate: context.date,
retrievedAt: context.retrievedAt,
price: numberOrNull(product.price),
currency: stringOrNull(product.currency),
sales30d: numberOrNull(product.totalSale30dCnt),
gmv30d: numberOrNull(product.totalSaleGmv30dAmt),
videos: numberOrNull(product.totalVideoCnt),
livestreams: numberOrNull(product.totalLiveCnt),
creators: numberOrNull(product.totalIflCnt),
commissionRate: numberOrNull(product.productCommissionRate),
rating: numberOrNull(product.productRating),
reviews: numberOrNull(product.reviewCount),
availableDate: stringOrNull(product.availableDate),
trendLabel: stringOrNull(product.salesTrendFlagText),
raw: product,
};
}
Store the Snapshot
Save both normalized and raw data. The raw payload allows future schema changes to be audited.
import { mkdir, writeFile } from "node:fs/promises";
async function saveSnapshot({ region, date }) {
const retrievedAt = new Date().toISOString();
const payload = await fetchRanking({ region, date });
const products = Array.isArray(payload.products) ? payload.products : [];
const snapshot = {
region,
requestedDate: date,
retrievedAt,
total: numberOrNull(payload.total),
products: products.map(p => normalizeProduct(p, { region, date, retrievedAt })),
raw: payload,
};
await mkdir("snapshots", { recursive: true });
const path = `snapshots/${region}-${date}.json`;
await writeFile(path, JSON.stringify(snapshot, null, 2));
return snapshot;
}
Compare Dated Snapshots
Use stable product IDs where available. A missing product ID should be excluded from automated matching.
function compareSnapshots(previous, current) {
const before = new Map(
previous.products.filter(p => p.productId).map(p => [p.productId, p])
);
return current.products
.filter(p => p.productId)
.map((now, rankIndex) => {
const old = before.get(now.productId);
const delta = (a, b) => a == null || b == null ? null : a - b;
return {
productId: now.productId,
currentRank: rankIndex + 1,
sales30dChange: old ? delta(now.sales30d, old.sales30d) : null,
gmv30dChange: old ? delta(now.gmv30d, old.gmv30d) : null,
creatorChange: old ? delta(now.creators, old.creators) : null,
videoChange: old ? delta(now.videos, old.videos) : null,
firstSeenInComparison: !old,
};
});
}
Do not describe a higher creator count as the cause of higher sales. The data can flag acceleration for review, but it does not establish why the change happened.
Production Safeguards
- Partition snapshots by requested date and region
- Store retrieval timestamps and the complete raw response
- Keep long identifiers as strings
- Log status codes without logging API keys
- Treat missing data as unknown
- Document the scoring formula and version
- Recheck current source, freshness, coverage, and rate-limit notes
The current endpoint documentation explicitly says that source, freshness, coverage, rate limits, and estimation notes are unspecified unless documented in request or response fields. Keep that boundary in the report.
Next Step
Use the live TikTok New Product Rank API reference to confirm the current contract before running the pipeline.
Disclosure: This article was prepared with AI-assisted editing using the current published API documentation as its technical source of truth.
Top comments (0)