For a service — a JSON API
The same numbers come back from a plain HTTP request. One request, one configuration: model, storage, condition. No keys, no sign-up, nothing to pay.
https://trofey.app/api/v1
The base is the address below. Five endpoints, all GET, all with Access-Control-Allow-Origin: *, so a browser can call them directly.
Every integration starts here · GET /models
The model name matches in full. So this is the first call: it hands back our exact strings and how many configurations each has, with no prices. q filters by substring and ignores case; without q the whole category comes back.
curl -s -G "https://trofey.app/api/v1/models" \ --data-urlencode "q=iphone 13"{ "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on.", "category": "phones", "count": 4, "currency": "UAH", "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "license": "https://trofey.app/terms/", "method": "https://trofey.app/methodology/", "models": [ {"brand": "iPhone", "configurations": 4, "model": "iPhone 13"}, {"brand": "iPhone", "configurations": 4, "model": "iPhone 13 Pro"}, {"brand": "iPhone", "configurations": 5, "model": "iPhone 13 Pro Max"}, {"brand": "iPhone", "configurations": 2, "model": "iPhone 13 mini"} ], "query": "iphone 13", "source": "https://trofey.app/analytics/phones/", "updated": "2026-09-22"}
«iPhone 16 Pro» and «iPhone 16 Pro Max» are different phones at different prices. That is why the match is exact, and why this call exists. Substring search lives here only: picking wrong costs one more call, while a substring inside a price lookup would cost a published wrong answer. An empty result carries a reason: a maker we do not publish, a plain misspelling, a product line and a model published in another category are told apart, because they call for opposite next moves. The last one comes with try_category, the same field /configurations gives, and the neighbouring category is asked with the same search, so a partial name finds it too. The nearest labels in this category arrive in did_you_mean — never beside try_category: the answer carries exactly one next step, not two opposite ones. That holds for every refusal carrying try_category, not for this one call: /configurations and /price behave the same way.
brand is a product line, not a manufacturer. All nine values we publish: iPhone, Samsung, Redmi, POCO, Xiaomi, Google Pixel, Apple iPad, Samsung Galaxy Tab, Lenovo. So iPhone rather than Apple, Redmi and POCO apart from Xiaomi, and Samsung under one name in tablets and another in phones. That is deliberate: the brand pages and their URLs stand on this field, and it is how people search. If you need the maker, the search understands one: q=Apple returns 50 models under brand=iPhone, q=Xiaomi returns 108 across three lines. There is no manufacturer field.
The price of one configuration · GET /price
Parameters: model (required, matched in full), storage_gb (an integer), condition — used or new, category (defaults to phones).
curl -s -G "https://trofey.app/api/v1/price" \ --data-urlencode "model=iPhone 13" \ -d storage_gb=128 \ -d condition=used{ "above_range": 24, "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on.", "below_range": 6, "brand": "iPhone", "category": "phones", "cheapest_live_uah": 5000, "condition": "used", "currency": "UAH", "dearest_live_uah": 26000, "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "finds_14d": 160, "license": "https://trofey.app/terms/", "live_listings": 1293, "market_price_uah": 12500, "method": "https://trofey.app/methodology/", "model": "iPhone 13", "price_range_uah": [ 7000, 17000 ], "slug": "iphone-13-128-used", "source": "https://trofey.app/analytics/phones/", "source_page": "https://trofey.app/analytics/phones/iphone-13-128-used/", "storage_gb": 128, "typical_spread_uah": 1000, "updated": "2026-09-22", "watch_price_credits_per_day": 27}
Half a key is half an answer, and we do not answer it. If what you sent leaves exactly one candidate, you get the price. If several remain, we do not guess — we ask back:
curl -s -G "https://trofey.app/api/v1/price" \ --data-urlencode "model=iPhone 13"{ "error": "ambiguous configuration", "model": "iPhone 13", "category": "phones", "choose_from": [ {"storage_gb": 128, "condition": "new"}, {"storage_gb": 128, "condition": "used"}, {"storage_gb": 256, "condition": "used"}, {"storage_gb": 512, "condition": "used"} ], "see": "https://trofey.app/api/v1/configurations?model=iPhone%2013&category=phones", "updated": "2026-09-22", "currency": "UAH", "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "method": "https://trofey.app/methodology/", "license": "https://trofey.app/terms/", "source": "https://trofey.app/analytics/phones/", "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on."}Inside the responses, URL values are percent-encoded: %20 is a space. That is the strictly correct form for the machine that parses them. In a terminal it is easier not to encode by hand and to pass values with --data-urlencode, as the examples above do.
What we never measured has no price — not a price of zero. A «not found» answer carries no numeric field at all: market_price_uah: 0 would be read by the next chart along as «this phone is worthless», and a filled-in estimate would arrive looking exactly like a measurement.
curl -s -G "https://trofey.app/api/v1/price" \ --data-urlencode "model=Nokia 3310"{ "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on.", "category": "phones", "found": false, "model": "Nokia 3310", "model_known": false, "reason": "No published configuration matches, and the model name is not one Trofey publishes in this category. Names are matched in full — call `models` (MCP) or GET /models to see the exact strings. Trofey publishes a price only where it measured one; it does not estimate on request.", "source": "https://trofey.app/analytics/phones/", "updated": "2026-09-22"}
Every configuration of one model · GET /configurations
The same thing a model page shows. model is required here too and matched in full — there is no value of it that returns another model's rows, let alone every row.
curl -s -G "https://trofey.app/api/v1/configurations" \ --data-urlencode "model=iPhone 13"{ "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on.", "category": "phones", "configurations": [ {"above_range": 1, "below_range": 0, "brand": "iPhone", "cheapest_live_uah": 10000, "condition": "new", "dearest_live_uah": 32000, "finds_14d": 4, "live_listings": 37, "market_price_uah": 14000, "model": "iPhone 13", "price_range_uah": [10000, 27000], "slug": "iphone-13-128-new", "source_page": "https://trofey.app/analytics/phones/iphone-13-128-new/", "storage_gb": 128, "typical_spread_uah": 2500, "watch_price_credits_per_day": 3}, {"above_range": 24, "below_range": 6, "brand": "iPhone", "cheapest_live_uah": 5000, "condition": "used", "dearest_live_uah": 26000, "finds_14d": 160, "live_listings": 1293, "market_price_uah": 12500, "model": "iPhone 13", "price_range_uah": [7000, 17000], "slug": "iphone-13-128-used", "source_page": "https://trofey.app/analytics/phones/iphone-13-128-used/", "storage_gb": 128, "typical_spread_uah": 1000, "watch_price_credits_per_day": 27}, {"above_range": 8, "below_range": 6, "brand": "iPhone", "cheapest_live_uah": 5000, "condition": "used", "dearest_live_uah": 35500, "finds_14d": 20, "live_listings": 240, "market_price_uah": 14500, "model": "iPhone 13", "price_range_uah": [9000, 19500], "slug": "iphone-13-256-used", "source_page": "https://trofey.app/analytics/phones/iphone-13-256-used/", "storage_gb": 256, "typical_spread_uah": 1495, "watch_price_credits_per_day": 4}, {"above_range": 1, "below_range": 0, "brand": "iPhone", "cheapest_live_uah": 12300, "condition": "used", "dearest_live_uah": 27000, "finds_14d": 5, "live_listings": 14, "market_price_uah": 15000, "model": "iPhone 13", "price_range_uah": [12300, 20000], "slug": "iphone-13-512-used", "source_page": "https://trofey.app/analytics/phones/iphone-13-512-used/", "storage_gb": 512, "typical_spread_uah": 1951, "watch_price_credits_per_day": 1} ], "currency": "UAH", "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "license": "https://trofey.app/terms/", "method": "https://trofey.app/methodology/", "model": "iPhone 13", "query": "iPhone 13", "source": "https://trofey.app/analytics/phones/", "updated": "2026-09-22"}
The same price, day by day · GET /price-history
The second question after «what does it cost» is whether it is getting cheaper, and one number cannot answer it. This is the line on the position page — up to 31 points, one a day.
curl -s -G "https://trofey.app/api/v1/price-history" \ --data-urlencode "model=iPhone 13" \ -d storage_gb=128 \ -d condition=used \ -d days=7{ "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on.", "category": "phones", "change_pct": 0.0, "change_uah": 0, "condition": "used", "currency": "UAH", "days": 7, "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "first_point": "2026-09-16", "history_updated": "2026-09-22", "last_point": "2026-09-22", "license": "https://trofey.app/terms/", "live_listings": 1293, "market_price_uah": 12500, "method": "https://trofey.app/methodology/", "model": "iPhone 13", "price_history": [ {"date": "2026-09-16", "market_price_uah": 12500}, {"date": "2026-09-17", "market_price_uah": 12500}, {"date": "2026-09-18", "market_price_uah": 12500}, {"date": "2026-09-19", "market_price_uah": 12500}, {"date": "2026-09-20", "market_price_uah": 12500}, {"date": "2026-09-21", "market_price_uah": 12500}, {"date": "2026-09-22", "market_price_uah": 12500} ], "published_delay_days": 0, "slug": "iphone-13-128-used", "source": "https://trofey.app/analytics/phones/", "source_page": "https://trofey.app/analytics/phones/iphone-13-128-used/", "storage_gb": 128, "updated": "2026-09-22", "window_from": "2026-09-16", "window_to": "2026-09-22"}
Without days the whole published month arrives — there is no more. change_uah is computed over the points returned. days counts calendar days, as it does on /find-history: window_from and window_to are what was asked for, while first_point and last_point are the days the line actually has. The ceiling is 31 days here and 30 on /find-history — the difference is the build day this line already carries — and asking past it returns requested_days beside the days you got. This line runs one day further than the finds one, and both say so in published_delay_days.
Where the listings actually sit · GET /price-distribution
price_range_uah on /price is not the cheapest and dearest listing: it is the histogram's span, and some lots sit outside it. How many is in the same answer, as below_range and above_range. This is the histogram on the position page — four to eight bars between the same bounds.
curl -s -G "https://trofey.app/api/v1/price-distribution" \ --data-urlencode "model=iPhone 13" \ -d storage_gb=128 \ -d condition=used{ "above_range": 24, "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on.", "below_range": 6, "bins": [ {"from_uah": 7000, "listings": 20, "share_pct": 1.5, "to_uah": 8250}, {"from_uah": 8250, "listings": 39, "share_pct": 3.0, "to_uah": 9500}, {"from_uah": 9500, "listings": 143, "share_pct": 11.1, "to_uah": 10750}, {"from_uah": 10750, "listings": 243, "share_pct": 18.8, "to_uah": 12000}, {"from_uah": 12000, "listings": 441, "share_pct": 34.1, "to_uah": 13250}, {"from_uah": 13250, "listings": 211, "share_pct": 16.3, "to_uah": 14500}, {"from_uah": 14500, "listings": 118, "share_pct": 9.1, "to_uah": 15750}, {"from_uah": 15750, "listings": 48, "share_pct": 3.7, "to_uah": 17000} ], "category": "phones", "condition": "used", "currency": "UAH", "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "history_updated": "2026-09-22", "in_range_listings": 1263, "license": "https://trofey.app/terms/", "live_listings": 1293, "market_price_uah": 12500, "method": "https://trofey.app/methodology/", "model": "iPhone 13", "range_uah": [ 7000, 17000 ], "slug": "iphone-13-128-used", "source": "https://trofey.app/analytics/phones/", "source_page": "https://trofey.app/analytics/phones/iphone-13-128-used/", "storage_gb": 128, "unverified_listings": 7, "updated": "2026-09-22"}
below_range and above_range are the listings outside the bars: without them the bars add up to less than live_listings. in_range_listings is the sum of the bars themselves — what the percentages fall short of a hundred by — and the three together are live_listings. A distribution exists only above eight live listings — the same floor the price uses.
The cheapest and dearest listing that really exist are cheapest_live_uah and dearest_live_uah in the same answer. Those are the ones to quote when somebody asks what a phone starts at. The histogram bounds are live listing prices too, but not the extremes: they are the cheapest and dearest that fit inside the scale. Across 651 positions the lower bound equals the cheapest listing on 419 and the upper equals the dearest on 274, and the whole market of a position fits the chart on 222. Where something is left outside, it is left far outside: the cheapest listing sits 44% below the bound at the median.
How many finds there were, per day · GET /find-history
Counts per day and nothing else: the feed of individual lots is paid for and delivered to an account's own channel. This is finds_14d in time — «six finds» and «six in one morning» differ only here.
curl -s -G "https://trofey.app/api/v1/find-history" \ --data-urlencode "model=iPhone 13" \ -d storage_gb=128 \ -d condition=used \ -d days=7{ "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on.", "category": "phones", "condition": "used", "currency": "UAH", "days": 7, "days_with_finds": 7, "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "find_history": [ {"date": "2026-09-15", "finds": 13}, {"date": "2026-09-16", "finds": 12}, {"date": "2026-09-17", "finds": 14}, {"date": "2026-09-18", "finds": 7}, {"date": "2026-09-19", "finds": 16}, {"date": "2026-09-20", "finds": 11}, {"date": "2026-09-21", "finds": 16} ], "finds_14d": 160, "finds_total": 89, "first_find": "2026-09-15", "history_updated": "2026-09-22", "last_find": "2026-09-21", "license": "https://trofey.app/terms/", "live_listings": 1293, "method": "https://trofey.app/methodology/", "model": "iPhone 13", "published_delay_days": 1, "slug": "iphone-13-128-used", "source": "https://trofey.app/analytics/phones/", "source_page": "https://trofey.app/analytics/phones/iphone-13-128-used/", "storage_gb": 128, "updated": "2026-09-22", "window_from": "2026-09-15", "window_to": "2026-09-21"}
A row holds two fields: date and finds — no link, no title, no price. Days without finds are absent, not zero.
Is this a normal price · GET /price-check
The question the product exists for, publishing nothing new: the arithmetic the bot's card does. Without it, three calls and a division by hand.
curl -s -G "https://trofey.app/api/v1/price-check" \ --data-urlencode "model=iPhone 13" \ -d storage_gb=128 \ -d condition=used \ -d price_uah=11250{ "above_every_live_listing": false, "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on.", "below_every_live_listing": false, "category": "phones", "cheaper_listings_at_least": 208, "cheaper_listings_at_most": 451, "cheaper_share_at_least_pct": 16.1, "cheapest_live_uah": 5000, "condition": "used", "currency": "UAH", "dearest_live_uah": 26000, "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "discount_vs_market_pct": 10.0, "discount_vs_market_uah": 1250, "history_updated": "2026-09-22", "license": "https://trofey.app/terms/", "live_listings": 1293, "market_price_uah": 12500, "method": "https://trofey.app/methodology/", "model": "iPhone 13", "price_bin": { "from_uah": 10750, "listings": 243, "to_uah": 12000 }, "quoted_uah": 11250, "range_uah": [ 7000, 17000 ], "slug": "iphone-13-128-used", "source": "https://trofey.app/analytics/phones/", "source_page": "https://trofey.app/analytics/phones/iphone-13-128-used/", "spreads_from_market": 1.2, "storage_gb": 128, "typical_spread_uah": 1000, "updated": "2026-09-22", "verdict": "below_market", "verdict_basis": "typical_spread"}
The verdict is measured in the position's own spread first, rather than as a percentage of its median: typical_spread_uah is how far a normal listing of this configuration sits from the market price, and half the live listings are inside it. spreads_from_market says how many of those the quote is away. One percentage is not one situation: «20% under» is an ordinary listing on a wide market and cheaper than almost everything on a narrow one. Beneath the spread sits one absolute floor: 45% off the median is far_below_market on any market at all, because past that point the discount stops reading as a bargain and starts reading as a reason. verdict itself is one of five: far_below_market, below_market, about_market, above_market, far_above_market. And verdict_basis says which yardstick answered: typical_spread, percent_floor, percent_ceiling (the mirror of the floor: where a position's spread has collapsed, three of them can be a one per cent discount) or percent_of_market — the last where the spread was never measured and the percentage ladder answers alone.cheaper_listings_at_least is a floor, not a percentile: the bar holding the quote is not split and travels as price_bin, with cheaper_listings_at_most as the ceiling beside it. When below_every_live_listing is true, nothing on sale is cheaper at all, and above_every_live_listing says the same about the top. Both are read off the cheapest and the dearest live price, and the bounds agree with them: a price dearer than the cheapest listing has at least that one under it, and a price at or under the dearest cannot have every listing under it. And it is not advice: we say where the number sits, while the battery and the scratches are visible only to whoever holds the phone.
What this service offers and what it limits · GET /info
One call instead of reading this page: what we publish, what it costs (nothing), under which licence, what is deliberately absent — and the only rate limit on the whole surface. Fields: limits, licence, transports, not_available.
Which model is it · eight digits · GET /device
The first eight digits of an IMEI are the TAC: a model, not a phone. Returns brand, model, as_of — and published_as, the same device under the name WE publish it under, ready to ask a price about. This is where «photograph the box → exact name → price» closes: the registry says GALAXY A21 and we publish Galaxy A21s, which are different phones at different prices, so there are two fields rather than one. Where we publish no price for it, published_as is null and did_you_mean names the closest labels we do publish. Eight digits, not fifteen — a caller who only needs the model never sends the rest of the number.
Whether the phone is reported · GET /imei-check
The full fifteen digits against the lost-and-stolen registry we republish, plus the device that TAC identifies. Fields: reported, status, listed_on, checksum_ok, as_of, and imei_tail instead of the number itself — the answer never repeats it in full. published_as travels here too: the same name of ours as in /device.
reported: false means «not in THIS registry», not a guarantee about the phone. Say that to anyone about to pay for one. checksum_ok: false usually means a typo, but the lookup runs anyway: a sixth of the registry's own rows fail their own check digit. If the registry cannot be reached you get error: "registry unavailable", which is also not a clean phone. These two are the only calls with a limit — 120 a minute per caller, because they are the only ones here that are not a cached file.
What we measure at all · GET /categories
Counters and dates, never the positions themselves. This is the «is my product covered at all» call, not a way to take everything in one request. If a category fails to build in the morning it simply drops out of the list — the rest keeps working.
curl -s "https://trofey.app/api/v1/categories"{ "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on.", "categories": [ {"category": "phones", "configurations": 566, "currency": "UAH", "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "license": "https://trofey.app/terms/", "method": "https://trofey.app/methodology/", "page": "https://trofey.app/analytics/phones/", "updated": "2026-09-22"}, {"category": "tablets", "configurations": 85, "currency": "UAH", "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "license": "https://trofey.app/terms/", "method": "https://trofey.app/methodology/", "page": "https://trofey.app/analytics/tablets/", "updated": "2026-09-22"} ], "source": "https://trofey.app/"}
Alive, and from which day · GET /health
«Up» is not the interesting question — a process serving a week-old file is up too. So the dates come with it. This answer is cached nowhere.
curl -s "https://trofey.app/api/v1/health"{ "ok": true, "source": "https://trofey.app/", "updated": { "phones": "2026-09-22", "tablets": "2026-09-22" }}
Errors
400 · no model{ "error": "model is required", "example": "https://trofey.app/api/v1/price?model=iPhone%2013&storage_gb=128"}400 · storage_gb is not a number{ "error": "storage_gb must be an integer"}400 · condition is not one we publish{ "error": "unknown condition", "condition": "refurbished", "category": "phones", "published": [ "new", "used" ]}400 · several candidates remain{ "error": "ambiguous configuration", "model": "iPhone 13", "category": "phones", "choose_from": [ {"storage_gb": 128, "condition": "new"}, {"storage_gb": 128, "condition": "used"}, {"storage_gb": 256, "condition": "used"}, {"storage_gb": 512, "condition": "used"} ], "see": "https://trofey.app/api/v1/configurations?model=iPhone%2013&category=phones", "updated": "2026-09-22", "currency": "UAH", "definition": "market_price_uah = медіана цін живих оголошень позиції за останні 14 днів; рахується лише за наявності щонайменше 8 живих оголошень", "method": "https://trofey.app/methodology/", "license": "https://trofey.app/terms/", "source": "https://trofey.app/analytics/phones/", "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on."}404 · we do not publish that category{ "error": "unknown category", "category": "cars", "published": [ "phones", "tablets" ]}404 · we never measured that configuration{ "attribution": "Trofey — https://trofey.app . Free to use; credit Trofey and link the page the number is published on.", "category": "phones", "found": false, "model": "Nokia 3310", "model_known": false, "reason": "No published configuration matches, and the model name is not one Trofey publishes in this category. Names are matched in full — call `models` (MCP) or GET /models to see the exact strings. Trofey publishes a price only where it measured one; it does not estimate on request.", "source": "https://trofey.app/analytics/phones/", "updated": "2026-09-22"}503 · the file cannot be read right now{ "error": "data unavailable", "category": "phones"}Errors are cached nowhere (Cache-Control: no-store): an error is about this request. And storage_gb=128gb is a 400, not a silently dropped parameter: dropping the filter would answer about a different configuration and you would never find out. For the same reason the same parameter sent twice is a 400 as well: two different values under one name have no rule saying which of them is ours. An unknown parameter is still tolerated and may repeat — the &_cb= cache-buster keeps working.
Cache, and a polite client
A successful answer carries public, max-age=3600, stale-while-revalidate=86400 and an ETag computed from the bytes of the answer itself. A polite client sends If-None-Match back, gets a 304 with no body, keeps its copy for an hour and polls us once a day — nothing changes between morning builds. There is no limit that will stop you, which is exactly why this is your call, not ours.
The numbers are recomputed every day, and every answer names the day it was computed — so yesterday's number never passes for today's.
What we ask in return
One thing: name Trofey and link the page the number is published on. That page already arrived with the number — it is the source_page field. The terms are the license field, how the number is computed is the method field. The ask travels in every answer as attribution, not only here.
Questions and answers
How many requests may I make?
Prices have no limit: they come from files the site publishes once a day and the edge caches, so asking twice costs nothing. The only limit is on the IMEI check — 120 requests a minute.
How often do the numbers change?
Once a day. Every answer carries the day it was computed — quote that, not the day you made the call.
May I use this data in my own product?
Yes, free of charge. One condition: credit Trofey and link the page the number is published on. The licence link travels in every answer.