Skip to content
TrackPin developers

TrackPin API · v1

The TrackPin API

The TrackPin API returns amazon.com prices judged against what each product normally sells for. Base URL https://trackpin.co/api/v1. JSON only. Money is integer cents (USD). Timestamps are milliseconds since the Unix epoch. v1 changes are additive only; see the changelog.

Early access.

Free keys are emailed straight away; Pro and Business are bought by card through Stripe. Request an API key

Endpoints

Quickstart

  1. Request a key

    Free keys are sent straight away by email; paid keys are ready as soon as you pay. Keep yours on your server, for example in an environment variable:

    export TRACKPIN_API_KEY=pk_…

  2. Make a request

    Fetch one product by its ASIN, here the Instant Pot Duo Plus 6 Qt pressure cooker (B01NBKTPTS), with your key in the x-api-key header.

  3. Read verdict.label and price.reference_cents

    verdict.label is one of great, good, normal, above, steady or early. price.reference_cents is the normal price it's measured against. The Product reference describes every field.

No key yet? Try the product endpoint without one

Request
curl https://trackpin.co/api/v1/product/B01NBKTPTS \
  -H "x-api-key: $TRACKPIN_API_KEY"

Authentication

Send your key in the x-api-key header, or as Authorization: Bearer pk_…. Keys start with pk_. We store only a hash of your key, so we can't show it again. If you lose it, request a new one.

Keep keys on your server. The API doesn't send CORS headers, so a web page on another site can't call it directly. To see a key's plan and today's usage without spending a request, use Check your key.

Subscribed on RapidAPI? Call the API through RapidAPI with your X-RapidAPI-Key; you don't need a pk_key. RapidAPI applies your plan's quota and shows your usage. The per-minute ceilings and live-check limits on this page still apply, by RapidAPI plan: BASIC gets the Free limits (no live checks), PRO the Pro limits, ULTRA and MEGA the Business limits.

Either header works
curl https://trackpin.co/api/v1/usage \
  -H "x-api-key: $TRACKPIN_API_KEY"

curl https://trackpin.co/api/v1/usage \
  -H "Authorization: Bearer $TRACKPIN_API_KEY"

Quotas

Each key has a daily quota, counted per UTC day and reset at 00:00 UTC.

Daily quota by plan
PlanRequests per day
Free100
Pro20,000
Business250,000
EnterpriseMore than 250,000

Per-minute ceilings

Each key also has per-minute ceilings, counted over a moving 60-second window: one for all calls to product, deals and search, and lower ones for search and deals on their own.

Per-minute ceilings by plan
PlanAll callsSearchDeals
Free601030
Pro3002060
Business1,2002060
EnterpriseCustom

A call over a ceiling gets 429 rate limit exceeded with a Retry-After header: wait that many seconds, then retry. It isn't counted against your daily quota. /v1/usage doesn't count toward these ceilings but has its own, 60 a minute.

Metered responses carry three headers, so you can pace your calls without guessing:

  • X-RateLimit-Limit
    Your daily quota.
  • X-RateLimit-Remaining
    Requests left today.
  • X-RateLimit-Reset
    ISO time of the next reset (00:00 UTC).

Every authenticated call to product, deals or search counts against the daily quota, including ones that return 404, because quota is counted before the request runs. A call turned away by a per-minute ceiling doesn't count. /v1/usage is free. Over the daily quota, calls return 429 daily quota exceeded until the reset.

A product call with live=1 counts as 5 requests, or 1 when the live check fails or can't run, and live checks have a daily cap per key. See Live checks. A product call for a product we don't track yet counts as 1, also when it starts tracking it. See New products.

Compare plans

Example · Response headers, Pro plan
Content-Type: application/json
X-RateLimit-Limit: 20000
X-RateLimit-Remaining: 18760
X-RateLimit-Reset: 2026-11-12T00:00:00.000Z

Errors

All error bodies are {"error": string}. A 429 over the daily quota adds limit and used. A 429 over a per-minute ceiling adds limit (the ceiling), window_s and retry_after_s, and comes with a Retry-After header.

  • 401missing api key

    No key sent.

  • 401invalid api key

    Unknown or revoked key.

  • 403live checks need a paid plan

    live=1 on the product endpoint with a key on a plan without live checks. Not counted against your quota.

  • 404not found

    We don't track this ASIN and couldn't start tracking it: tracking.status says why. See New products.

  • 405method not allowed

    The data endpoints are GET only. The Allow header lists the methods an endpoint answers: GET, HEAD and OPTIONS (POST and OPTIONS for /v1/keys/rotate).

  • 429rate limit exceeded

    More than your plan's per-minute ceiling; wait Retry-After seconds. Not charged. The body adds limit, window_s and retry_after_s.

  • 429daily quota exceeded

    Over today's quota. The body adds limit and used.

  • 500internal error

    Something failed on our side, not in your request.

  • 503temporarily unavailable

    Our database can't be reached right now. Retry after the number of seconds in the Retry-After header.

Some endpoints add their own errors: Search returns 400 for a query that's too short and 504 when a title search takes too long. Each reference page lists them.

401 · No key sent
{
  "error": "missing api key"
}
429 · Over today's quota
{
  "error": "daily quota exceeded",
  "limit": 100,
  "used": 101
}
429 · Over a per-minute ceiling
Retry-After: 7

{
  "error": "rate limit exceeded",
  "limit": 300,
  "window_s": 60,
  "retry_after_s": 7
}

Concepts

Normal price

A product's normal price is the lowest price level the product held for at least 15% of the last 45 days (about a week). Prices within 3% of each other count as one level, and we use the lowest price in that level.

It's price.reference_cents in every response. price.discount_pct compares the current price with it: positive means below normal, negative means above. It's never measured against yesterday's price or against the crossed-out price on the listing.

Why the lowest price that lasted: a price seen for only an hour shouldn't define normal, and neither should an inflated price nobody paid. Because the window is the last 45 days, the normal price follows genuine long-term price changes.

How TrackPin works out the normal price

Live example, Refreshed daily
{
  "asin": "B01NBKTPTS",
  "price": {
    "current_cents": 9498,
    "reference_cents": 9498,
    "discount_pct": 0,
    "is_deal": false,
    "observed_at": 1790785935486,
    "changed_at": 1790064174666,
    "checked_at": 1794355114364,
    "source": "stored"
  }
}

Verdicts

verdict.label is the same verdict the website shows, and verdict.sentence the same plain-English sentence. It compares the current price with the normal price:

Verdicts
VerdictWhen
Great pricelabel: great
20% or more below normal
Good pricelabel: good
5% to 20% below normal
Normal pricelabel: normal
Within 5% of normal
Above normallabel: above
5% or more above normal
Steady pricelabel: steady
One price the whole time, over at least 14 days
Too early to telllabel: early
Less than 14 days of history, or no normal price yet

If we haven't seen the price for more than 3 days, verdict.stale is true and the verdict may be out of date. Prices under $1, or more than 90% below normal, are treated as likely listing errors: verdict.suspect is true, and verdict.label still shows the verdict the numbers give.

Verdicts describe the price against its normal price. They aren't advice to buy or to wait.

The verdicts, explained for shoppers

Live example, Refreshed daily
{
  "asin": "B01NBKTPTS",
  "verdict": {
    "label": "early",
    "text": "Too early to tell",
    "pct_vs_normal": null,
    "evidence": "none",
    "stale": true,
    "suspect": false,
    "steady_since": null,
    "sentence": "Too early to tell. We haven't worked out its normal price yet. Not on sale at our last check (17h ago). Last price seen Sep 30."
  }
}

Evidence

verdict.evidence says how much history backs the verdict, counted in days tracked:

  • Strong strong

    60+ days tracked.

  • Moderate moderate

    30 to 59 days.

  • Thin thin

    14 to 29 days. The website marks the verdict as provisional.

  • None none

    Under 14 days. No verdict: the label is early.

A stale price caps evidence at Thin. stats.history_days, stats.price_changes and stats.distinct_prices give you the underlying numbers.

How sure we are

Live example, Refreshed daily
{
  "asin": "B01NBKTPTS",
  "verdict": {
    "label": "early",
    "evidence": "none",
    "stale": true
  },
  "stats": {
    "history_days": 99,
    "price_changes": 17,
    "distinct_prices": 7
  }
}

Price history

history[] is the price as steps, oldest first. Each step has price_cents, from (when we first saw that price) and to (when we last saw it).

Draw it as a step chart and don't interpolate: between one step's to and the next step's from, the price changed at a moment we didn't observe.

It covers everything we hold for the product, and our oldest history starts on Jun 21, 2026. stats.distinct_prices of 1 means the price hasn't moved since we started tracking it. Pass history=0 to leave the steps out when you only need the verdict.

Live example, Refreshed daily
{
  "asin": "B01NBKTPTS",
  "history": [
    {
      "price_cents": 15967,
      "from": 1789236147916,
      "to": 1789661870487
    },
    {
      "price_cents": 14749,
      "from": 1789663675256,
      "to": 1790037958180
    },
    {
      "price_cents": 9498,
      "from": 1790064174666,
      "to": 1790785935486
    }
  ]
}
// 199 earlier steps not shown

Listing claims

Many listings show a crossed-out List or Typical price. listing_claim reports it, with the discount the listing claims and the real discount against the normal price.

listing discount
= 1 − price ÷ crossed-out price
real discount
= 1 − price ÷ normal price
  • holds_upHolds up
    The real discount is at least half the listing's.
  • smallerSmaller than shown
    Below normal, but by less than half of what the listing shows.
  • not_below_normalNot below normal
    The real discount is under 2%.
  • uncheckedNot checked yet
    Under 30 days of history, or the price is stale.

listing_claim is null when the listing shows no crossed-out price, or when the listing price differs from the latest price we recorded, so the claim and the verdict always describe the same price.

The crossed-out price is the reference shown on the listing. We don't know how it was set; we only compare it with prices we've observed.

How we check crossed-out prices

Live example, Refreshed daily
{
  "asin": "B0CWB3TF7X",
  "price": {
    "current_cents": 11999,
    "reference_cents": 11999,
    "discount_pct": 0
  },
  "listing_claim": {
    "label": "List:",
    "listing_reference_cents": 13999,
    "claimed_pct": 14,
    "real_pct": 0,
    "class": "not_below_normal",
    "observed_at": 1794411379405
  }
}

Freshness

price.observed_at is when we last saw the current price. price.changed_at is when it became the current price, and price.checked_at is the latest check of the product we have on record. When price.checked_at is later than price.observed_at, the price wasn't on the listing at that check: the product was out of stock or had no offer. verdict.stale is true after 72 hours. Refresh frequency varies by product.

When you show a price, show when it was checked next to it, the way TrackPin does. Prices are observations and can be out of date. To read a price at request time instead, use a live check.

Live example, Refreshed daily
{
  "asin": "B01NBKTPTS",
  "price": {
    "current_cents": 9498,
    "observed_at": 1790785935486,
    "changed_at": 1790064174666,
    "checked_at": 1794355114364
  },
  "verdict": {
    "stale": true
  },
  "last_seen": 1794355115607
}

Live checks

With live=1, the product endpoint checks the price on amazon.com at request time instead of returning the last price we recorded. Live checks are available on Pro and Business.

  • Cost
    5 requests from your daily quota. When the live check fails or can't run, the call counts as 1.
  • Daily cap
    Live checks per key per UTC day: 4,000 on Pro and 20,000 on Business. Only checks that come back ok count. Past the cap you get the stored data with live.status unavailable and live.reason limit, and the call counts as 1.
  • Time
    About 1–2 seconds in our tests. live_timeout_ms sets how long to wait, from 1,000 to 15,000 ms (default 8,000), counted from when we receive the request.
  • Result
    price.source is live, price.current_cents is the price the check found and price.observed_at is when amazon.com answered (or a later sighting of the same price we already held). The verdict, listing claim, stats and history describe that price.
  • History
    The reading is recorded into the price history (live.recorded), so later responses include it.

If the live check fails or can't run, you still get HTTP 200 with the stored data: price.source is stored, and live.status and live.reason say what happened. Read price.source before presenting a price as checked at request time.

  • ok
    The check read the listing. If the listing showed no single price, live.price_cents is null and price keeps the stored price.
  • failed · timeout
    The check didn't finish within live_timeout_ms.
  • failed · upstream
    amazon.com didn't answer the check normally.
  • failed · not_listed
    amazon.com's answer didn't include this product.
  • failed · error
    Something failed on our side.
  • unavailable · busy
    Too many live checks were running, for your key or overall.
  • unavailable · paused
    Live checks are paused for a while.
  • unavailable · disabled
    Live checks are switched off.
  • unavailable · limit
    Today's quota had room for this call but not for the 5 requests of a live check, or your key has used today's live checks (4,000 on Pro and 20,000 on Business).

A key on a plan without live checks gets 403 live checks need a paid plan, and the call isn't counted. live.shared is true when the reading wasn't made for your request: amazon.com was read within the last 60 seconds, by another request's live check or by our regular tracking. HEAD requests never run a live check.

The live fields in the Product reference

Request
curl -G https://trackpin.co/api/v1/product/B01NBKTPTS \
  -d live=1 \
  -H "x-api-key: $TRACKPIN_API_KEY"

New products

Ask for a product we don't track yet and we check it on amazon.com in the same request. If amazon.com lists it, we start tracking it and you get a 200 with today's price and a tracking object. This works on every plan, without live=1.

  • Cost
    1 request, whatever the outcome. Also with live=1 on a plan that has live checks; on other plans live=1 is still a 403 and nothing is checked.
  • Result
    tracking.started is true and price.source is live. For a product we held no prices for, history holds the one price we have and the verdict is early: we judge a price once we have about 14 days of history (tracking.verdict_after_days). A product we had prices for keeps its history. When amazon.com shows no single price, price.current_cents is null. live.recorded says whether the reading went into the price history. image, rating and category follow after our first full check, in about 25 minutes (tracking.first_scan_eta_s).
  • Not started
    404 not found with tracking.status saying why, and a Retry-After header when asking again later may work. When we hold older prices for the product, you get a 200 with them and tracking instead. If the check read a price but adding the product failed, you get a 200 with that price and tracking.started false.
  • Opt out
    discover=0 skips the check: a product we don't track gets 404 not found straight away, without tracking.
  • Timing
    The check has live_timeout_ms (default 8 seconds); if it doesn't answer in time, tracking.status is failed with tracking.reason timeout. Requests for the same product at the same moment share one check: each waits at most its own live_timeout_ms, and the check runs within the first request's. tracking.new is true for the one that added the product.
  • started
    We track the product now: amazon.com listed it and it was added to the products we check. live.recorded says whether the reading went into its price history (a reading without a price doesn't). tracking.new is true for the request that added it.
  • not_listed · not_listed
    amazon.com's answer didn't include this product, so there's nothing to track. Asking again within 24 hours gets the same answer without a new check.
  • failed · timeout | upstream | error
    The check didn't get a reading in time, amazon.com didn't answer normally, or adding the product failed on our side. Try again after retry_after_s.
  • unavailable · busy | paused
    The check couldn't run just now: too many checks for your key or overall, or checks (or adding new products) are paused for a while. Try again after retry_after_s.
  • blocked · blocked
    We don't track this product.
  • disabled · disabled
    Checks on amazon.com are switched off for now.

The check is a live check: your key's live-check speed limits apply, but the daily live-check cap doesn't, and it doesn't count toward it. HEAD requests never check.

The tracking fields in the Product reference

Example, Illustrative values
{
  "error": "not found",
  "tracking": {
    "started": false,
    "new": false,
    "status": "unavailable",
    "reason": "busy",
    "since": null,
    "source": "api",
    "first_scan_eta_s": null,
    "verdict_after_days": null,
    "retry_after_s": 15
  }
}

Resources

Tools and extension

Link to a verdict

Any page can link to a TrackPin verdict with https://trackpin.co/check?q={url}, where {url} is a URL-encoded amazon.com product link, a short link or an ASIN. The bookmarklet on Check prices from Amazon does exactly this for the page you're on.

Chrome extension

Shows the same verdict on amazon.com product pages, for shoppers. It needs no key and doesn't use your quota. It reads only the product ID from the page.

Install steps

OpenAPI

/api/v1/openapi.json is an OpenAPI 3.1 description generated from the same definitions as these pages. Import it into your HTTP client or code generator.

Link format
https://trackpin.co/check?q={url}
// e.g. https://trackpin.co/check?q=https%3A%2F%2Fwww.amazon.com%2Fdp%2FB01NBKTPTS
Bookmarklet
javascript:(()=>{location.href='https://trackpin.co/check?q='+encodeURIComponent(location.href)})()