Ozon's documented API is for sellers, so it can't show what a competitor charges or the price a shopper sees with the Ozon card. Our Ozon API returns both from public product pages. The script below saves a search to CSV with the card price, crossed-out price and regular price, and appends each run so a daily cron builds a price history.
What Ozon's Seller API covers
Ozon's Seller API works on your own store: it changes stocks and prices, and reads returns and warehouses. You get a key from a seller account under Settings → Seller API. Its sections cover your own products, orders, returns and finance; none searches the catalogue, and the card price shoppers see is not available through it. If you sell on Ozon and need your own stock, orders or prices, use it: it is free with a seller account and writes back to your store.
Ozon prices with ToolzerHub
We make ToolzerHub. Our Ozon API has three routes, each a GET with the x-api-key header at 1 credit a call:
/v1/ozon/products/search?query=...&count=48returns products for a search, as on ozon.ru./v1/ozon/products/detail?url=...returns one product page./v1/ozon/products/category?path=...lists a category or brand path.
export TOOLZERHUB_API_KEY=your_key
curl -G -H "x-api-key: $TOOLZERHUB_API_KEY" \
--data-urlencode "query=кофемашина" -d count=48 \
"https://api.toolzerhub.com/v1/ozon/products/search"
The response is {"data": {...}} with the products in data.products. The first product from a search for "кофемашина", without image and is_adult:
{
"sku": 3665854453,
"title": "Кофеварка с кофемолкой, автоматическим капучинатором для молотого кофе и капсул Tuvio TCM09EA, серая, Яндекс Фабрика",
"url": "https://www.ozon.ru/product/kofevarka-s-kofemolkoy-avtomaticheskim-kapuchinatorom-dlya-molotogo-kofe-i-kapsul-tuvio-3665854453/",
"price": { "amount": 12908, "currency": "RUB", "raw": "12 908 ₽" },
"original_price": { "amount": 45990, "currency": "RUB", "raw": "45 990 ₽" },
"discount_percent": -71,
"rating": 4.6,
"reviews_count": null
}
The search price is the Ozon card price, and discount_percent is the badge Ozon shows, minus sign included. The detail call for the same product returned price 14,213 ₽ (without the card), card_price 12,908 ₽, reviews_count 1,206, in_stock: true and seller_name "Яндекс Фабрика", plus the description, characteristics, images and variants.
Save prices to CSV on every run
The script runs one search, writes every product with its card and crossed-out price, and makes a detail call for the first 10 to add the regular price, seller, stock and review count. Each run appends rows with a fetched_at time. Products with sizes can carry the price only on the variant, so for those the script keeps the variant's card price and leaves regular_price empty. If the Ozon category it prints starts with /brand/, call the category route with that path instead of searching. Save the script as ozon_prices.py and install requests (pip install requests).
import csv
import os
import sys
import time
from datetime import datetime, timezone
import requests
BASE = "https://api.toolzerhub.com/v1/ozon/products"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["fetched_at", "sku", "title", "card_price", "regular_price", "crossed_out", "rating",
"reviews", "seller", "in_stock", "url"]
def get(path, params, tries=3):
# 429, 5xx and dropped connections are retried; failed calls cost no credits.
for attempt in range(tries):
last = attempt == tries - 1
try:
r = requests.get(f"{BASE}/{path}", headers=HEADERS, params=params, timeout=180)
except requests.RequestException:
if last:
raise
time.sleep(10 * (attempt + 1))
continue
if r.status_code in (429, 500, 502, 503, 504) and not last:
time.sleep(10 * (attempt + 1))
continue
r.raise_for_status()
return r.json()["data"]
def amount(p):
return (p or {}).get("amount")
def prices(d, sku):
# (price, card price). Products with sizes can carry only the variant's card price.
if amount(d.get("price")) is not None:
return amount(d["price"]), amount(d.get("card_price"))
for v in d.get("variants") or []:
if str(v.get("sku")) == str(sku):
return None, amount(v.get("price"))
return None, None
def detail(url):
try:
return get("detail", {"url": url}) or {}
except requests.RequestException as e: # keep the search row without detail fields
print(f"detail failed: {e}")
return {}
def main(query, count="48", details="10", out="ozon_prices.csv"):
found = get("search", {"query": query, "count": int(count)})
print(f"{len(found['products'])} products, Ozon category {found['category_path']}")
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
new_file = not os.path.exists(out)
with open(out, "a", newline="", encoding="utf-8-sig" if new_file else "utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
if new_file:
writer.writeheader()
for i, p in enumerate(found["products"]):
row = {"fetched_at": now, "sku": p["sku"], "title": p["title"],
"card_price": amount(p.get("price")), # search shows the Ozon card price
"crossed_out": amount(p.get("original_price")), "rating": p.get("rating"),
"reviews": p.get("reviews_count"), "url": p["url"]}
if i < int(details): # one extra call each: price without the card, seller, stock
d = detail(p["url"])
regular, card = prices(d, p["sku"])
if regular is None and card is None: # sometimes missing; retry once
d = detail(p["url"])
regular, card = prices(d, p["sku"])
row.update(regular_price=regular, card_price=card or row["card_price"],
seller=d.get("seller_name"), in_stock=d.get("in_stock"),
reviews=d.get("reviews_count") or row["reviews"])
writer.writerow(row)
print(f"{len(found['products'])} rows appended to {out}")
if __name__ == "__main__":
if len(sys.argv) < 2:
sys.exit('usage: python ozon_prices.py "кофемашина" [count] [details] [file.csv]')
if not HEADERS["x-api-key"]:
sys.exit("set TOOLZERHUB_API_KEY first")
main(*sys.argv[1:])
We ran python ozon_prices.py "кофемашина" and python ozon_prices.py "кроссовки nike". Each wrote 48 rows, and the shoes took 15 seconds.
What the prices showed
The Ozon card price was below the regular price on every product where we had both: 9.2% to 10.0% for the 10 coffee machines, and 1.0% to 14.5% for 8 Nike shoes, 5 of them at 1%.
Crossed-out prices sit far above what anyone pays. 32 of the 48 coffee machines had one, at a median 2.5 times the card price and up to 11.7 times; 43 of the 48 shoes had one, median 3.04 times, up to 14.1 times. To report real discounts, compare against your own price history rather than discount_percent.
Prices also moved within the hour. The coffee machine above changed across three fetches under an hour apart:
| Fetch | Price | Card price |
|---|---|---|
| First | 14,609 ₽ | 13,267 ₽ |
| Second | 14,213 ₽ | 12,908 ₽ |
| Third | 14,618 ₽ | 13,276 ₽ |
Keep fetched_at with every row. The top 10 coffee machines came from 3 sellers and the top 10 shoes from 5, Ozon Россия among them. Search left reviews_count empty on 37 and 35 of the 38 rows with no detail call; the detail call fills it.
What it costs
A run of one query with 10 detail calls is 11 credits. You get 100 free credits a month; $19 buys 10,000 credits that never expire, and plans for higher volume cover more queries. Set your key and run the script on your first search.
We also cover other services popular in Russia, such as 2GIS reviews and Yandex Maps reviews. For flight fares from Skyscanner's route pages, see is the Skyscanner API free.
We are not affiliated with Ozon.