Otto.de API & Scraper
The Otto.de API returns product data from Otto, one of Germany's largest online retailers, as clean JSON.
🤖 Using an AI assistant? Copy this link into ChatGPT / Claude / Cursor — it reads every endpoint and parameter instantly and tells you if this API fits your use case.
The primary search endpoint returns products with name, brand, price, condition, availability, rating, review count, image and URL, and you can browse, pull a product, deals, new_arrivals, brands, categories, filters and reviews. It is built for price intelligence, catalog enrichment and German e-commerce analytics that need Otto data without a scraper. One ReefAPI key, one shared credit pool, the standard envelope.
Otto's two ids, and why a search page returns more rows than it returns prices
Every Otto row is keyed twice, and only part of a result page arrives fully populated. Rather than silently dropping the thin rows or padding them out, this API returns them as id-only stubs and publishes the fill rate in meta.completeness_pct so you can decide what to do with them. All figures below are from live calls on 2026-08-27.
| Field | Format | Measured |
|---|---|---|
| product_id | 10 digits, the number at the end of /p/<slug>-<id>/ | 1786938266 |
| variation_id | 10 digits, the ?variationId= on that same URL | 1786938267 - never equal to product_id, and exactly product_id+1 on 26 of 55 rows |
| price / currency | JSON number plus a currency code | 715.52 with currency "EUR"; this tier is DE and EUR only |
| condition | Otto's Zustand, normalized | "new" on all 17 populated rows of a search for iphone |
| availability | normalized stock state | "in_stock" on all 17 populated rows |
| sku | in the schema, never filled on this tier | null on 55 of 55 rows |
| meta.completeness_pct | share of returned rows carrying name, brand and price | 30.91 on search 'iphone' (17 of 55); 52.94 on browse 'technik/computer' (18 of 34) |
| offset / page_size | offset pagination, not page numbers | page size 72, next_offset 72 - page with meta.next_offset |
The product and reviews actions take a query alongside the id, because they resolve the item through the search grid rather than through the standalone product page. Pass the keyword the item shows up under; the id on its own is not enough.
Real request and response JSON
Captured from the indexed primary action, search, on .
{
"method": "POST",
"url": "https://api.reefapi.com/otto/v1/search",
"headers": {
"x-api-key": "$REEF_KEY",
"content-type": "application/json"
},
"body": {
"query": "iphone"
}
}{
"ok": true,
"meta": {
"api": "otto",
"endpoint": "search",
"mode": "live",
"latency_ms": 1955.5,
"record_count": 55,
"bytes": 656468,
"cache_hit": false,
"completeness_pct": 29.09,
"method": "ssr_embedded_ldjson",
"url": "https://www.otto.de/suche/iphone/",
"offset": 0,
"page_size": 72,
"has_more": true,
"next_offset": 72
},
"data": {
"products": [
{
"variation_id": "[redacted-phone]",
"product_id": "[redacted-phone]",
"name": "[redacted-name]",
"url": "https://www.otto.de/p/apple-iphone-air-smartphone-16-6-cm-6-5-zoll-1000-gb-speicherplatz-48-mp-kamera-[redacted-phone]/?variationId=[redacted-phone]",
"image": "https://i.otto.de/i/otto/336d77e2-dc72-5c9a-b515-09d0739f3f5b?$responsive_ft2$",
"brand": "Apple",
"price": 1639.99,
"currency": "EUR",
"condition": "new",
"availability": "in_stock",
"rating": 4.5,
"review_count": 68,
"sku": null,
"sponsored": true
},
{
"variation_id": "[redacted-phone]",
"product_id": "[redacted-phone]",
"name": "[redacted-name]",
"url": "https://www.otto.de/p/apple-iphone-17-pro-smartphone-15-9-cm-6-3-zoll-1000-gb-speicherplatz-48-mp-kamera-[redacted-phone]/?variationId=[redacted-phone]",
"image": "https://i.otto.de/i/otto/7db310fe-b236-5a0a-a2c7-64bbf85d77f0?$responsive_ft2$",
"brand": "Apple",
"price": 1255.62,
"currency": "EUR",
"condition": "new",
"availability": "in_stock",
"rating": 4.5,
"review_count": 362,
"sku": null,
"sponsored": true
},
{
"variation_id": "[redacted-phone]",
"product_id": "[redacted-phone]",
"name": "[redacted-name]",
"url": "https://www.otto.de/p/apple-iphone-16e-smartphone-15-4-cm-6-1-zoll-512-gb-speicherplatz-48-mp-kamera-[redacted-phone]/?variationId=[redacted-phone]",
"image": "https://i.otto.de/i/otto/bdc47e20-418d-51cb-b942-6941ed4d23af?$responsive_ft2$",
"brand": "Apple",
"price": 581.13,
"currency": "EUR",
"condition": "new",
"availability": "in_stock",
"rating": 5,
"review_count": 274,
"sku": null
}
],
"offset": 0,
"page_size": 72,
"has_more": true,
"next_offset": 72
}
}What the Otto.de API does
| Action | Description | Concrete use case | Key params |
|---|---|---|---|
| search | Keyword product search on Otto.de with brand, price, rating, condition and availability filters, sort order and offset pagination. Returns product cards (name, brand, price in EUR, condition, availability, rating, review count, image, URL). | Pricing teams call search to get keyword product search on Otto.de with brand, price, rating, condition and availability filte…. | query, offset, marke, brand, sortierung, ... |
| browse | Browse an Otto.de category landing (e.g. moebel, technik/computer, mode) with the same brand / price / rating / sort filters and offset pagination as search. | Marketplace operators call browse to get browse an Otto.de category landing (e.g. | category, offset, marke, brand, sortierung, ... |
| product | Product facts for one Otto.de item by variation id — name, brand, price (EUR), condition, availability, aggregate rating, review count, image and URL. Read from the search grid, because Otto does not serve its standalone product page to this endpoint; individual review text, GTIN and the full spec table are therefore not available here. | Catalog enrichment teams call product to get product facts for one Otto.de item by variation id. | id, query, variation_id |
| deals | Current Otto.de deal / sale campaign products (e.g. Deal des Monats, Super Sale). Auto-discovers the active campaigns from the storefront and returns their product cards. Pick a specific campaign with the campaign param. | Retail analysts call deals to get current Otto.de deal / sale campaign products (e.g. | campaign |
| new_arrivals | Newest products on Otto.de — keyword or category sorted newest-first (sortierung=neuheiten). Pass query OR category; supports the same filters and offset pagination. | Pricing teams call new_arrivals to get newest products on Otto.de. | query, category, offset, marke, brand, ... |
| brands | Brand filter links available on an Otto.de category landing page — feed the slug to search/browse as the brand filter. | Marketplace operators call brands to get brand filter links available on an Otto.de category landing page. | category |
| categories | Otto.de department tree — top-level departments from the homepage, or the child categories of a parent path (e.g. parent=technik). Feed paths to browse / brands. | Catalog enrichment teams call categories to get otto.de department tree. | parent, category |
| filters | Discover the sort orders and filter query params supported by Otto.de search/browse, with a live probe showing the available deal campaigns. | Retail analysts call filters to discover the sort orders and filter query params supported by Otto.de search/browse, with a l…. | none |
| reviews | Aggregate customer rating for an Otto.de item — average star rating and total review count from the search-embedded data. Individual review TEXT is only on the standalone product page, which Otto does not serve to this endpoint, so it is not available here (reviews[] is returned empty with text_available=false, honestly). | Pricing teams call reviews to get aggregate customer rating for an Otto.de item. | id, query, variation_id |
Call search from your stack
curl -X POST https://api.reefapi.com/otto/v1/search \
-H "x-api-key: $REEF_KEY" \
-H "content-type: application/json" \
-d '{"query":"iphone"}'import requests
r = requests.post(
"https://api.reefapi.com/otto/v1/search",
headers={"x-api-key": REEF_KEY},
json={
"query": "iphone"
},
)
print(r.json()["data"])const res = await fetch("https://api.reefapi.com/otto/v1/search", {
method: "POST",
headers: {
"x-api-key": process.env.REEF_KEY,
"content-type": "application/json",
},
body: JSON.stringify({
"query": "iphone"
}),
});
const { ok, data, meta, error } = await res.json();Ask your MCP-connected assistant: call reefapi.otto.search with {"query":"iphone"}.Who uses this API and why
- Pricing teams call search and deals to track Otto prices and promotions against competitors.
- Catalog-enrichment tools use product to fill listings with images, specs and availability.
- Market analysts use brands and categories to size a segment in the German market.
Questions developers ask before integrating
Why do most of my Otto search rows have two ids and null everywhere else?
Otto publishes structured product data for only part of the result grid, and the rest of the row lives in markup this tier does not read. A search for 'iphone' returned 55 rows of which 17 carried a name, brand and price; the populated ones were positions 0 through 15 plus position 54. meta.completeness_pct reported 30.91 for that call, so you can detect the situation without inspecting every row.
What is the difference between product_id and variation_id?
product_id is the product page and variation_id is the specific configuration Otto listed - a color, a capacity, a size. They were never identical across 55 measured rows, and on 26 of them variation_id was exactly product_id plus one. The URL carries both, as /p/<slug>-<product_id>/?variationId=<variation_id>. The product and reviews actions take the variation id.
Why does the product action need a query as well as an id?
Otto's standalone product page is not readable on this tier, so the item is located inside the search grid instead, and that means saying which keyword it appears under. Passing id 1786938267 with query 'iphone' returned the full record - name, brand, price 715.52 EUR, rating 5.0 and 654 reviews - with meta.completeness_pct 100.
Does the Otto reviews action return review text?
No, and it says so rather than returning an empty list you might misread. It returns the aggregate - average 5.0 over 654 reviews on a measured item - plus text_available: false and a note explaining that individual review bodies live only on the standalone product page. One quirk to know: meta.record_count on this action echoes the review count, 654, not the number of rows returned, which is zero.
Are sponsored Otto listings included in search results?
Yes, and they are flagged rather than removed. A search for 'iphone' marked 10 of 55 rows sponsored: true, at grid positions 0, 1, 7, 14, 20, 24, 27, 31, 37 and 39. If you are computing price statistics, filter them out yourself, because paid slots are not a random sample of the inventory.
Why does sorting by price not return the prices in order?
The sort does reach Otto and does change the grid, but the subset of rows that arrive with prices is a fixed block near the top of the page rather than the sorted sequence. Browsing technik/computer with sort preis-aufsteigend returned prices 692.27, 1089.00, 439.00, 689.00 and 1634.00 in that order. Treat products[] position as the sorted signal, not the price column. The filters action returns this same caveat as live data.
Why does the deals action sometimes return NOT_FOUND?
Because there was no active sale campaign on the storefront at that moment. The action discovers campaigns live rather than hard-coding slugs, so when Otto is between campaigns you get NOT_FOUND with a message saying none were discovered, and the filters action shows available_campaigns as an empty list. That was the state measured on 2026-08-27.
Is anything in the Otto response in German number format?
Prices are not - they are JSON floats with a decimal point, such as 1349.97. Product names are, because Otto writes them that way and they are passed through verbatim: "Apple iPhone Air Smartphone (16,6 cm/6,5 Zoll, 1000 GB Speicherplatz, 48 MP Kamera)". If you parse specs out of the name, expect the comma.
What is the Otto.de API?
Otto.de API is a ReefAPI endpoint group for otto.de It returns live JSON through POST requests under /otto/v1.
Is the Otto.de API free to try?
Yes. ReefAPI starts with 1,000 free credits, no card required. Otto.de calls use the same shared credit balance as every other ReefAPI engine.
Do I need an Otto.de login or account?
No login to Otto.de is needed for the API response. You call ReefAPI with your x-api-key header, and the playground can run live examples before you create a production key.
How fresh is the Otto.de data?
The page example is captured from a live search call, and production requests fetch live data through ReefAPI rather than a static sample.
How many credits does the Otto.de API use?
Otto.de actions currently cost 1 credit per successful call. Failed or blocked calls are free, and all APIs draw from one credit pool.
Can I call Otto.de from an AI assistant or MCP client?
Yes. Connect ReefAPI once through MCP and your assistant can call otto actions with the same key, credit pool and JSON envelope used by normal REST requests.