Een eigen back-end, in elke taal (PHP, .NET, Java, Node, …): de API is alleen server-to-server bereikbaar.
1. Architectuur — de ene harde regel
De browser krijgt nooit een token. De API is uitsluitend server-to-server bereikbaar: geen CORS, geen tokens in de
browser of in URL's. Je bouwt daarom altijd een eigen back-end-proxy:
TEXT
browser (no token)
│ same-origin
▼
your back-end proxy ──▶ Authentik
│ (token)
│ HTTPS + bearer token
▼
Flow API
De browser praat same-origin met jouw proxy; alleen de proxy kent de inloggegevens en het token.
De proxy stuurt uitsluitend een vaste, kleine set GET-endpoints door (§3) en zet zelf de
Authorization: Bearer <token>-header erop. Verwijder een Authorization-header en een merchant-parameter van de
browser.
Inloggegevens en tokens horen alleen in de serveromgeving of het geheugen: nooit in logs, responses of URL's.
2. Authenticatie
Je ontvangt van EuroStocks een serviceaccount (Authentik, machine-to-machine) met: de API-basis-URL
(https://flow-api.eurostocks.com), de token-URL, een client-id en de gebruikersnaam + het app-wachtwoord van het
serviceaccount.
Een token vraag je aan met OAuth2 client_credentials: een POST naar de token-URL met de formuliervelden
grant_type, client_id, username, password en scope (openid memberships). Het antwoord bevat
access_token en expires_in (seconden).
Dit is Authentik's serviceaccount-variant van client_credentials: hij gebruikt bewust username + password in
plaats van het standaardpaar client_id + client_secret. "Corrigeer" dit niet naar de standaardvorm, want dan
faalt de authenticatie.
Merchant-koppeling: het serviceaccount is lid van precies één merchant: jouw storefront. De Flow API leidt die zelf
af; stuur dus nooit een merchant-parameter mee.
Tokenbeheer (verplicht om goed te doen):
Cache het token in het geheugen en hergebruik het tot ~30 seconden voordat het verloopt; vraag dan een nieuw aan.
Laat gelijktijdige verzoeken één aanvraag delen (single-flight): vraag niet per request een token aan.
Krijg je toch een 401 van de API: vraag één keer een vers token aan en herhaal het verzoek één keer.
Aanrader: vraag het eerste token aan bij het opstarten van je server, zodat de eerste bezoeker er niet op wacht.
3. Het API-oppervlak
Alle paden zitten onder /api/v1 (oude paden zonder versie geven 404). Alles is GET. Let op: het
storefront/-segment zit alléén op search en het voertuigdetail.
Het contract: filters, sorteringen, labels, groepen (§5)
GET /api/v1/catalog
Referentietaxonomie: merk → modelgroep → model, met canonieke URL-slugs (§6)
Je proxy stuurt alleen deze vier door.
Getypeerde client:GET /api/v1/openapi.json levert het OpenAPI 3.1-document van de API (met hetzelfde token,
zonder extra recht). Genereer je client daaruit in plaats van velden over te typen (bijvoorbeeld met
openapi-typescript) en gebruik de operaties met de tag storefront: precies de vier hierboven. Haal het op bij je
build, niet vanuit de browser. De labels in het document zijn de standaardteksten; de actuele staan in
/api/v1/attributes.
Wat je storefront aanbiedt. Je merchant heeft instellingen die de API afdwingt; ze gelden voor elke zoekopdracht,
ook voor de facet-tellingen:
Minimaal registratiejaar: elke zoekresponse bevat merchant.min_first_registration_year. Is het gezet, dan geeft
zoeken nooit oudere auto's en levert een gekozen jaar daaronder nul resultaten. Begrens je jaarfilter erop; null
betekent geen ondergrens.
Merken en modellen: een storefront kan beperkt zijn tot bepaalde merken of modellen uitsluiten. De catalog
(§6) bevat dan alleen wat je aanbiedt; een merk of model daarbuiten geeft nul resultaten, en
in het zoekvak wordt het niet toegepast (outside_scope, §4).
Prijsweergave:merchant.price_display (final, lease of both) bepaalt welke prijzen en prijsfilters het
contract adverteert.
Deze grenzen gelden voor zoeken: een directe link naar een voertuig-id werkt ook voor een auto daarbuiten.
Rate limiting (aanrader): al het bezoekersverkeer bereikt de Flow API via jouw proxy met één serviceaccount. De API
kan individuele bezoekers dus niet onderscheiden, en misbruik door één bezoeker telt als verkeer van jou. Limiteer
daarom per bezoekers-IP in je eigen proxy, op de doorgestuurde /api/*-paden. Een verstandige instelling: een
token-bucket met een burst van ~10 requests en ~1–2 requests/seconde aanhoudend per IP (± 60–120/min). Dat zit ruim
boven het drukste legitieme gedrag (snel filters wisselen piekt kort op enkele requests per seconde), maar stopt
geautomatiseerde excessen. Antwoord bij overschrijding met 429 + een Retry-After-header. Dit hoeft niet per se in
de proxy: een edge- of CDN-laag vóór je proxy (bijv. Bunny.net of Cloudflare) kan dezelfde per-IP-limiet afdwingen,
vaak eenvoudiger te beheren, en houdt het verkeer al buiten de deur. De gecachte naslagdocumenten (attributes,
catalog) raken je limiet nauwelijks: die beantwoordt je proxy uit eigen geheugen.
Daarnaast hanteert de Flow API zelf een rate limit per merchant, gebaseerd op het afgenomen abonnement. Overschrijd
je die, dan krijgt je proxy een 429 (code: "RATE_LIMITED") met een Retry-After-header (seconden): respecteer
die en probeer niet agressief opnieuw. Je eigen per-IP-limiter is dus ook zelfbescherming: hij voorkomt dat één
bezoeker (of bot) het quotum van je hele storefront opmaakt.
Cache-advies:attributes en catalog zijn naslagdocumenten. Haal ze server-side op, bewaar ze in het geheugen en
hervalideer met If-None-Match: beide leveren een ETag en antwoorden met een goedkope 304 zolang er niets veranderd
is. De catalog stuurt Cache-Control: private, max-age=30; houd je eigen TTL kort, zodat een wijziging in je aanbod
snel zichtbaar is. Stuur ze gecomprimeerd naar de browser. Zoekopdrachten stuur je per request door.
4. Zoeken
Verzoekparameters (querystring op /api/v1/storefront/search):
q — het zoekvak: herkende woorden worden filters (zie Het zoekvak hieronder).
description — "Uitvoering / omschrijving bevat": woorden die altijd als tekst worden gezocht, nooit als filter
(trekhaak als woord, AMG Line), samen met de tekstwoorden van q in één zoekvraag; maximaal 100 tekens.
page, per_page — paginering. De server begrenst per_page op maximaal 50 (standaard 24) en page op maximaal
200; een waarde buiten bereik wordt stil geklemd, nooit een foutmelding. Neem de paginaberekening daarom over uit de
response (page, per_page) en begrens je eigen paginering op pagina 200.
sort — een sorteertoken uit het contract (§5); codeer geen
sorteringen hard. De standaardsortering is de eerste in de lijst (prijs, laag → hoog). Het laatste token,
_text_match:desc ("Beste match"), rangschikt op hoe goed de zoektekst matcht; zonder tekst valt het terug op
prijs.
facets — kommagescheiden lijst velden waarvan je tellingen wilt (bijv. facets=make,fuel,transmission,year); een
herkende lijst vervangt de standaardset. Vraag alleen aan wat je toont. Blader, sorteer of wissel je de
paginagrootte met ongewijzigde filters, stuur dan facets=none: geen tellingen (facet_counts: []).
Filtervelden — elk filter in het contract adverteert zijn eigen queryparameters (params); gebruik die namen
letterlijk en leid zelf geen namen af. Enum-velden gaan als kommagescheiden contractcodes
(fuel=PETROL,HYBRID; de codes staan per filter in het contract), bereiken als het geadverteerde paar
(price_min/price_max, mileage_min/mileage_max, seats_min/seats_max).
year_min / year_max is het registratiejaar van–tot (beide inclusief); year is een exacte meerkeuze
(year=2024,2025).
make, model, model_group filteren de taxonomie; model vindt beide spellingen die de feed gebruikt (de
catalog-sleutel en het label).
Een allowlist aan de API-kant bepaalt welke parameters geaccepteerd worden; een prijs- of leaseparameter die je
storefront niet adverteert, is een 400.
Het zoekvak (q)
Herkende woorden worden gewone filterparameters. De overige woorden zoeken als tekst in de modelregel van de dealer
(titel en model): elk woord moet matchen, met tolerantie voor tikfouten en schrijfvarianten (c200 = c 200,
s line = sline). Een filter dat je zelf meestuurt, wint altijd.
trekhaak (afneembare trekhaak / vaste trekhaak: alleen die soort), panoramadak, leer, stoelverwarming, navi, hud, cruise control, acc, led, camera, pdc, en de Nederlandse naam van elke optie in het contract
het filter van die optie
Kleur
zwart, zwarte golf; naast een interieurwoord de interieurkleur: zwart leer, beige interieur
color; interior_color
Lak
metallic, matte, mat zwart
metallic, matte_color
Eerste eigenaar
eerste eigenaar, 1e eigenaar
previous_owners_max=1
Schadevrij
geen schade, zonder schadeverleden, schadevrij
accident_damaged=false (zoals de dealer het opgeeft)
Model zonder merk: een modelnaam van 4+ tekens die bij één merk hoort, werkt ook zonder merk (golf =
Volkswagen Golf); kortere namen (x5, a4) alleen met het merk erbij. Een code die bij één merk hoort (tfsi,
quattro → Audi; xdrive → BMW; 4motion → Volkswagen) laat ook een kort model van dat merk meetellen:
a4 tfsi = Audi A4 + benzine.
Uitvoeringen blijven tekst:gti, gtd, gte, rs, amg, m sport, r-line, s-line, st-line (een
golf gte is één uitvoering, niet elke plug-in Golf), en ook grijs kenteken.
Getallen: een kaal jaartal is dat ene jaar (2020). pk en kW zonder voorzetsel betekenen "vanaf"; km, €
en een maandbedrag "tot". Een getal zonder eenheid (tot 20000) wordt een prijs zolang je storefront prijzen toont;
tot 20000 km is kilometerstand. Motorinhoud (1.5, 2,0) blijft tekst.
Tikfouten: een duidelijke tikfout telt als het bedoelde woord: één letter naast een merk, een woord uit deze lijst
of een model van een al bekend merk, bij 5+ letters, zonder cijfer en met maar één betekenis (mercdes,
panoramdak, vw tiguen). De chip noemt het echte woord, from het getypte.
Verbindingswoorden die overblijven (en, of, met, een, de, het, in, op, van, und, mit)
vallen weg.
interpret=false zoekt q als platte tekst: alleen schrijfvarianten, tikfouttolerantie en merkaliassen
(volkswagen = VW), geen filters en geen interpretation-blok.
Het interpretation-blok. Elke response op een niet-lege q bevat interpretation:
applied — de chips. Per chip: params precies zoals je ze zelf zou sturen (enumcodes en true als tekst,
bereikgrenzen als getal), label / label_nl en from (de getypte woorden).
text — de woorden die als tekst zijn gezocht (null = geen).
not_applied — herkend maar niet toegepast, met een reason:
conflicts_with_filter: je verzoek zet die parameter al op een andere waarde; suggestions bevat de vervanging;
outside_scope: een merk of model buiten je aanbod (§3);
not_available: een prijs- of leasegrens die je storefront niet toont;
ambiguous: een getal zonder eenheid dat hier geen prijs kan zijn (bijv. max 300); suggestions biedt de
lezingen aan, de woorden blijven in text.
Zo verwerk je het: neem applied over in je filterstatus en URL zonder opnieuw te zoeken (de response bevat de
resultaten al), zet text terug in het zoekvak en toon de not_applied-meldingen. Pagina 2 stuurt dan de filters
plus q=<text> en geeft hetzelfde resultaat. Laat de bezoeker de hele getypte zin zien en bewerken (de
referentie-storefront doet dat): verwijdert de bezoeker een chip, haal dan de woorden uit zijn from uit het zoekvak;
verstuurt de bezoeker de zin opnieuw, haal dan eerst de chips van de vorige zin weg (zelf gekozen filters blijven,
behalve een waarde die ook uit de zin kwam). Zoek bij verzenden (Enter of zoekknop), niet bij elke toetsaanslag: dat
spaart je rate-limitquotum en voorkomt flikkerende resultaten.
Responsevorm (de velden waar de referentie-storefront op bouwt):
found — het exacte totaal; hits — de resultaten van deze pagina.
Per hit o.a.: id, title, make, model, make_slug, model_slug, final_price, vat_scheme (de
btw-context van de prijs), optioneel lease_price (€/maand, met bijbehorende lease_terms), image_url,
image_urls (de eerste ~5 galerij-URL's) en image_count.
facet_counts — een lijst van { field_name, counts: [{ value, count }] } per aangevraagd veld.
merchant — { id, name, price_display, min_first_registration_year }: de instellingen van je storefront
(§3).
interpretation — alleen bij een niet-lege q (zie hierboven).
Let op de naamgeving: zoekhits gebruiken snake_case (make_slug, final_price), het voertuigdetail camelCase
(makeSlug, finalPrice, leaseTerms); houd daar in je mapping rekening mee.
Twee valkuilen:
Facet-tellingen zijn kort gecachet. Tellingen worden exact berekend, maar server-side kort (~60 s) gecachet, en
kunnen dus iets afwijken van de werkelijke resultaten van dat moment. Je mag ze tonen; houd er alleen rekening mee
dat een getoonde telling en het exacte found-totaal niet altijd precies op elkaar aansluiten.
Prijsweergave volgt het contract. Welke prijs je toont, bepaalt het contract (price_display: final / lease /
both); een lease-only storefront levert bijvoorbeeld geen final_price. Toon bij elke prijs de bijbehorende
btw-context (vat_scheme) en bij een maandbedrag de leasevoorwaarden (lease_terms / leaseTerms). De API levert
uitsluitend deze publieke projectie; er is geen kostenopbouw of marge-informatie, en probeer die ook niet af te
leiden.
Afbeeldingen — CDN-representaties en het AVIF-formaat
Alle afbeeldings-URL's die de API levert (image_url, image_candidates, image_urls en de galerij van het
voertuigdetail) zijn representatie-URL's van het mobile.de-CDN (img.classistatic.de), in de vorm
…?rule=mo-<breedte>.jpg. Gebruik uitsluitend de geleverde URL's en leid zelf geen URL's of breedtes af: welke
representaties bestaan, is kennis van de feed, en de API levert per doel al de juiste maat (zoekkaart, galerij,
thumbnails).
Er is één gedocumenteerde bewerking op een geleverde URL (zie ook de veldbeschrijvingen in het OpenAPI-document): laat
je de extensie .jpg uit de rule-parameter weg (?rule=mo-1024 in plaats van ?rule=mo-1024.jpg), dan levert het
CDN dezelfde afbeelding als AVIF, doorgaans 40–55 % kleiner. Twee belangrijke kanttekeningen:
Het formaat ligt vast, er is geen content-negotiation. De extensieloze URL levert altijd AVIF (ongeacht de
Accept-header, er is geen Vary). Wissel dus nooit kaal de URL om: een browser zonder AVIF-ondersteuning krijgt
dan een niet-decodeerbaar bestand. Gebruik altijd een <picture> met een getypeerde source en de .jpg-URL als
fallback:
Houd og:image en andere metadata voor scrapers op de .jpg-vorm: AVIF-ondersteuning is daar onbetrouwbaar.
Dit is waargenomen CDN-gedrag van een derde partij (geverifieerd 2026-08-06), geen garantie die deze
handleiding kan geven. De type-gate hierboven begrenst het risico: valt de AVIF-variant ooit weg, dan faalt hooguit
één request en kun je met een error-handler op de <img> terugvallen op de .jpg-source (verwijder de
AVIF-sources en de browser kiest opnieuw).
5. Het attributes-contract — bouw je UI hierop
GET /api/v1/attributes is het contract dat je filters, sorteeropties, labels en lay-out aanstuurt. Render vanaf
dit contract, niet vanaf hardgecodeerde lijsten: dan volgt je site automatisch wijzigingen in het aanbod.
version — controleer op "v1". Wijkt hij af, toon dan een (interne) waarschuwing: je filters kunnen verouderd
zijn.
filters / sorts — welke filtervelden en sorteertokens er bestaan, inclusief de soort (enum, range, int,
bool, date_range, text; het bredere attributenregister kent ook tri_state), de bijbehorende
queryparameters (params) en per enum de toegestane waardecodes. Negeer onbekende toekomstige soorten netjes (niet
renderen) in plaats van erop te breken. De geadverteerde parameters horen bij jouw storefront: leaseparameters
alleen als je lease toont, en vat_reclaimable verdwijnt bij een storefront zonder margeauto's.
attributes — het register van uitrusting en specificaties. Elk attribuut is een zoekfilter; de waarden staan
alleen in het voertuigdetail (attributes), niet op zoekhits.
Labels: gebruik label_nl / group_nl / disclaimer_nl met de Engelse velden als terugval. De formuleringen
dragen betekenis (bijv. btw-verrekenbaarheid): verzin geen eigen bewoordingen. Codes zijn stabiel, labels niet:
EuroStocks kan een label corrigeren zonder nieuwe release.
groups — de secties waarin je filters en de voertuigdata op de detailpagina groepeert.
response_only-velden + detail_field-koppelingen — welke responsevelden je als datarijen op de detailpagina
toont; een veld met values (zoals vat_scheme) vertaal je via die codetabel.
Disclaimers: velden met een disclaimer (zoals CO₂, een ≈-schatting) toon je mét die disclaimer. Bijzonderheid
CO₂: bij elektrische voertuigen levert de feed co2_emissions: 0; onderdruk de rij daar.
6. Catalog en URL's
GET /api/v1/catalog levert de referentietaxonomie (merk → modelgroep → model) inclusief canonieke, opgeslagen
URL-slugs, beperkt tot wat jouw storefront aanbiedt (§3): bouw je merk- en
modelkeuzelijsten erop, dan volgen ze je aanbod vanzelf.
Maak nooit zelf slugs. Inkomende URL-slugs zet je via de catalog om naar filterwaarden; uitgaande links bouw je
met de slugvelden die op de hit (make_slug / model_slug) en het detail (makeSlug / modelSlug) meekomen.
De referentie-storefront gebruikt paden als /{make}/{model} (lijst) en /{make}/{model}/{id} (detail): een goed
deelbaar patroon om over te nemen (wat je ervan laat indexeren: zie §7).
Escape alles. Merk, model en id uit de URL zijn door de bezoeker te manipuleren: HTML-escape ze overal waar je ze
in de pagina schrijft (titels, meta-tags). Laat het voertuig-id zijn ruwe URL-encoding houden wanneer je het in het
upstream-pad invoegt.
7. SEO en crawlers
De voorraad is groot en wisselt snel. Je wilt Google dus wél op de stabiele lijstpagina's, maar niet op
honderdduizenden kortlevende detailpagina's (dode links in de zoekresultaten, dunne content, en elke crawl kost jouw
quotum).
Indexeer alleen de taxonomie-laag: de homepage, de ongefilterde resultatenpagina en de schone merk- en
modelpaden (/{make}, /{make}/{model}). Alleen die URL's horen in je sitemap.xml (goed te genereren uit de
catalog).
Detailpagina's: noindex, follow via een meta-tag in de HTML, niet via robots.txt: Google moet de pagina kunnen
ophalen om de noindex te zien. Links erheen blijven volgbaar, dus je lijstpagina's houden hun linkwaarde.
Query-URL's (filters, sortering, paginering) geef je een canonical naar het schone pad zonder query; anders
wordt gefacetteerd zoeken een crawl-val van eindeloze filtercombinaties.
robots.txt:Disallow: /api/ (crawlen daarvan kost alleen quotum) en verwijs naar je sitemap.
Verkochte of verdwenen auto's: een echte 404 of 410 (of een redirect naar de modellijst), nooit een lege
pagina met status 200. Soft-404's op schaal schaden het hele domein, en óók met noindex blijven bezoekers via
gedeelde links binnenkomen. De API antwoordt dan zelf 404 (§8).
Canonical en og:url zijn absolute URL's op je eigen origin.
Dit is de juiste standaard voor een storefront op de volledige importvoorraad. Toon je maar een kleine, stabielere
deelvoorraad (honderden auto's, langere statijd), dan kunnen detailpagina's wél mee in de index; de overige regels
blijven gelden.
8. Foutcodes
Fouten zijn JSON {error, code?}. Reageer op de machinecode code, niet op de tekst in error:
Status
code
Betekenis
401
AUTH_REQUIRED
Token ontbreekt of is ongeldig: vraag één keer een vers token aan en herhaal (§2).
403
NO_ROLES
Het token hoort bij geen enkele merchant.
403
MODULE_FORBIDDEN
Het serviceaccount heeft geen recht op de storefront-API.
403
MERCHANT_FORBIDDEN
Er is een merchant-parameter meegestuurd die niet bij je token hoort: stuur nooit merchant.
400
MERCHANT_REQUIRED
Het serviceaccount hoort bij meer dan één merchant; neem contact op met EuroStocks.
429
RATE_LIMITED
Rate limit overschreden: wacht de seconden uit Retry-After (§3).
Zonder code: een ongeldige parameter is 400 {error} (bijv. een prijsparameter die je storefront niet adverteert),
een onbekend voertuig-id 404 {error: "vehicle not found"}, een onbekend /api/*-pad 404 {error: "unknown endpoint"} en een onverwachte storing 500 {error: "internal error"}.
9. Startchecklist
Ontvang van EuroStocks: de API-basis-URL, de token-URL, een client-id en de inloggegevens van het serviceaccount.
Bouw de proxy: tokenbeheer (§2) en uitsluitend de endpoints uit §3
doorsturen; verwijder merchant en de Authorization-header van de browser; rate-limit per bezoekers-IP;
niet-toegestane /api/*-paden → 404.
Laad en cache attributes en catalog; controleer version === "v1".
Render de zoekpagina en filters vanaf het contract; begrens het jaarfilter op
merchant.min_first_registration_year; bouw URL's met catalog-slugs.
Verwerk het interpretation-blok van het zoekvak: chips overnemen, text terug in het vak, meldingen tonen
(§4).
Toon de prijzen die het contract adverteert (price_display), met vat_scheme, leasevoorwaarden en de
contractlabels en -disclaimers erbij.
Regel de crawlerkant (§7): een sitemap met alleen lijstroutes, noindex op detailpagina's,
Disallow: /api/ en een echte 404 voor verkochte auto's.
Verifieer: geen token of inloggegevens in de browser, logs of URL's; 401 → één keer vernieuwen en herhalen; de
foutcodes uit §8 afgehandeld, bij 429 Retry-After gerespecteerd.
Volgende stappen
Genereer een getypeerde client uit GET /api/v1/openapi.json (§3).