Integrations
Marketplace API cheatsheet: 17 valkuilen uit een Bol-launch
Een team van 23 in Almere zette een marketplace-agent live op drie API's tegelijk. Zeventien REST-quirks haalden bijna de launch onderuit. Gerangschikt, met workarounds.

Vrijdag 18:40, dag voor go-live. Drieëntwintig man in een Almeers magazijn, de marketplaces-agent moet maandag om 09:00 de Bol.com-inbound overnemen. Laatste smoke test: een Belgische order bevestigen, €189 inclusief BTW. Bol.com geeft een nette 200 OK terug. Twee uur later valt de PDF van de klant binnen zonder BTW-regel. De agent deed niets fout. De API wel.
Die order heeft ons gered. Was het een Duitse order van €120 geweest, dan hadden we er duizend door de nieuwe pipeline geduwd voordat de eerste klacht bij finance lag. De quirk is stil, het gevolg is luid, en het is precies het soort bug waarvoor we de cheatsheet hebben gebouwd.
Hieronder de zeventien REST-quirks die boven kwamen toen we de marketplace-agent van één klant tegelijk op Bol.com, Amazon SP-API en Marktplaats Pro aansloten. Gerangschikt, kort uitgelegd, met de workaround die we nu standaard meeleveren.
Hoe de ranking werkt
Iedere quirk krijgt één vraag: als hij afgaat, merkt de integrator het dan?
Tier S-quirks geven een 2xx terug en vertellen je niets. Tegen de tijd dat je het doorhebt, zit de schade in het magazijn, op een klantfactuur of — worst case — in een belastingaangifte. Tier A-quirks falen hard maar om de verkeerde reden; je fixt het symptoom, de oorzaak komt volgende week terug. Tier B-quirks staan ergens slecht gedocumenteerd en kosten een junior engineer een middag.
De cheatsheet staat in de repo als MARKETPLACE_QUIRKS.md en de test suite assertet tegen de ergste.
Tier S: stil dataverlies
1. Bol.com variant-merge collapst de EAN naar de parent
Als twee offers verwijzen naar EAN's die het catalogteam later samenvoegt tot één product (omdat het echt varianten van dezelfde SKU zijn), wijst de Retailer API daaropvolgende /retailer/orders-responses stilletjes toe aan de overlevende EAN. Als je voorraad of verzendnotities keyt op EAN — en je downstream-systemen doen dat meestal — komt de wees-EAN in webhooks binnen met stock-aantallen die nergens meer in je WMS matchen.
De fix: vertrouw een EAN niet als primary key aan de klantkant. Join altijd via de offer ID, die is stabiel. Onze agent slaat nu beide op en reconciliëert bij een mismatch.
2. SP-API verliest BTW-inclusief bij cross-border orders boven €150
Dit is degene die ons om 18:40 te pakken had. De IOSS (Import One-Stop Shop)-drempel voor low-value zendingen ligt op €150. Daarboven stopt de SP-API getOrderItems-response met het uitsturen van het ItemTax-veld voor het bedrag dat de koper ziet bij EU cross-border orders, en wordt ItemPrice exclusief BTW zonder dat de veldnaam verandert. De responsecode blijft gewoon 200.
Als je orderbevestigingstemplate ItemPrice als inclusief totaal rendert, gaat elke Duitse, Belgische of Franse order boven €150 met een verkeerde factuur de deur uit. De fix: check drie velden samen — ShipFromAddress.CountryCode, BuyerInfo.BuyerCountry en het line-item totaal — en bereken de BTW dan opnieuw uit de Orders API v0 tax classification van de marketplace, in plaats van erop te vertrouwen dat ItemTax aanwezig is.
3. Bol.com process-status webhooks vuren dubbel bij retry
Bol.com's async operaties geven een process-status URL terug; je polled of subscribed. De webhook-variant retried op een non-2xx. Prima. Het ongedocumenteerde gedrag: een 2xx die meer dan 4,8 seconden na dispatch binnenkomt wordt als timeout behandeld en de webhook vuurt opnieuw. Dus als je handler traag is, wordt je "order accepted"-event twee keer verwerkt. De tweede kopie draagt hetzelfde eventId, maar alleen de eerste draagt de dispatch timestamp.
Idempotency op eventId, altijd.
4. SP-API Listings v2021 patch overschrijft de attribute array
Listings v2021 komt met een JSON Patch endpoint. De documentatie leest alsof een replace-operatie op een attribute array merget. Dat doet hij niet. Hij overschrijft. Als je /attributes/bullet_point patcht met één nieuwe bullet, verwijder je de andere vier.
{
"productType": "PRODUCT",
"patches": [{
"op": "replace",
"path": "/attributes/bullet_point",
"value": [
{ "value": "New bullet copy", "marketplace_id": "A1805IZSGTT6HS" }
]
}]
}
De fix is GET eerst, lokaal splicen, dan de volledige array PATCHen. Het endpoint accepteert een add-op met een array-index die sommige account types negeren. Doe de GET-modify-PATCH-dans, zelfs op simpele velden. Ja, dat verdubbelt je call count.
5. Marktplaats Pro PATCH /adverts dropt priceDecimal als currency ontbreekt
Het Admarkt advert-endpoint accepteert een prijs als { "priceDecimal": 12.50 }. Als je het currency-veld weglaat, dat gedocumenteerd staat als "defaults to EUR", wordt de prijs server-side stilletjes weggegooid. De response is 200 OK en bevat het advert met de oude prijs. Diff je PATCH tegen de response, anders blijf je je afvragen waarom de prijs nooit verandert.
Tier A: hard falen, verkeerde reden
6. SP-API NextToken verloopt na 30 seconden
Pagination tokens op getOrders en getOrderItems worden gepresenteerd als opaque en herbruikbaar. Dat zijn ze niet. Pauzeert je worker langer dan ~30 seconden tussen calls — bijvoorbeeld omdat hij de eerste page in een queue heeft gezet en wacht op een ack — dan geeft de volgende call InvalidInput terug. De error message wijst naar de marketplace IDs, niet naar het token. We hebben twee dagen het verkeerde konijn achternagezeten.
7. Bol.com weigert ISO 8601-timestamps met milliseconden
De Retailer API accepteert 2026-06-19T08:00:00+02:00 en weigert 2026-06-19T08:00:00.000+02:00 als malformed. De error body zegt "invalid date-time". De meeste moderne client libraries serialiseren standaard mét milliseconden. Strip ze, of je krijgt 400's op elke order-search.
8. Bol.com voorraad-parallel-updates geven soms maar 409 terug
Het /offers/{offer-id}/stock-endpoint past optimistic concurrency toe. Twee updates voor dezelfde offer ID binnen ongeveer 250ms geven soms een 409 en soms twee keer 200, waarbij de tweede stilletjes wordt weggegooid. We hebben geen deterministisch patroon kunnen reproduceren. Serialiseer per-offer-id op de queue layer en de 409 verdwijnt volledig.
9. SP-API Feeds API dropt stilletjes UTF-8 BOM
De XML feed processor verwacht UTF-8. Upload je UTF-8 mét een byte order mark, dan wordt de feed geaccepteerd (202), komt het processing report terug met nul errors, en zijn er nul records verwerkt. Geen waarschuwing. Stuur zonder BOM of stuur UTF-16 met de gedocumenteerde marker.
10. Marktplaats Pro vatRate is een string, geen number
{ "vatRate": 21 } geeft 400. { "vatRate": "21" } geeft 200. Nergens gedocumenteerd dat we hebben kunnen vinden. Genereer je request bodies vanuit een typed model, override dan de serialiser voor dit ene veld, anders is je CI groen en zijn je prod-calls allemaal 400's.
11. SP-API GetMyFeesEstimate laat de referral fee weg op Prime SKU's
Het endpoint geeft een structuur terug die compleet lijkt. Voor SKU's die in FBA Prime zitten ontbreekt de referral-component. Het totaal komt lager terug dan in werkelijkheid. Baseer je je pricing engine op dit getal, dan is elke Prime SKU te laag geprijsd. De workaround: zoek het referral-percentage van de categorie op uit een statische tabel en tel het er zelf bij op.
Tier B: gedocumenteerd, alleen slecht
12. Bol.com geeft 404 als een EAN bestaat maar op een parent SKU zit
Voorraad-lookups op EAN geven 404 als de EAN bij een parent product hoort dat zelf geen offers heeft. Je moet doortraverseren naar de children. De 404 is geen "not found in your account" — het is "deze EAN bestaat wel, alleen niet waar je denkt". We loggen ze nu op warn-level en triggeren een catalog-walk.
13. Marktplaats Pro bid-daglimieten resetten om middernacht UTC
Het dashboard toont "vandaag" als CET. De API forceert het als UTC. Tussen 00:00 CET en 02:00 CET staat je daglimiet-counter in het dashboard op nul, terwijl de API nog steeds afwijst met "limit exceeded". Draai je een 23:55 budget top-up? Plan hem op 01:55 CET en omzeil het blinde gat van twee uur.
14. Bol.com retourlabels komen eerst als base64, dan als URL terug
De eerste call naar /shipments/return-labels voor een order geeft { "label": "JVBERi0xLj..." } terug, een base64-encoded PDF. Elke volgende call voor hetzelfde retour-ID geeft { "labelUrl": "https://..." } terug, een signed link zonder PDF body. Handel beide vormen af, anders mist je archief elk label na de eerste.
15. SP-API refresh_token roteert stilletjes op sommige scopes
De LWA refresh flow geeft op ongeveer één op de twaalf refreshes een nieuwe refresh_token terug in de response, als de request de sellingpartnerapi::notifications-scope bevat. Het oude token blijft nog ongeveer een uur werken. Overschrijft je token store bij elke refresh, dan zit je goed. Slaat hij het origineel op en update hij alleen het access_token, dan krijg je de volgende ochtend een invalid_grant. Persist altijd elke refresh_token die terugkomt.
16. Bol.com offer stock null wordt geparsed als nul
{ "stock": { "amount": 0 } } sturen zet het offer op out-of-stock. Het stock-veld weglaten in een PATCH laat de bestaande waarde staan. Tot zover verwacht. De quirk: { "stock": null } sturen wordt geparsed als out-of-stock, niet als "niet aanraken". Emit je ORM null voor afwezige velden, dan zet elke PATCH je voorraad op nul.
17. Marktplaats Pro advert-status case-sensitivity in search
POST /adverts met "status": "active" én "status": "ACTIVE" geven beide 200 en maken beide het advert aan. GET /adverts?status=active geeft alleen de lowercase terug. Kies een case aan de boundary en handhaaf het; wij lowercasen alles binnenkomend.
De check van twee regels die de meeste hiervan vangt
De cheatsheet is prima op papier. De versie die in productie draait is een contract test die de response shape assertet tegen wat wij verwachten, niet tegen wat de docs beloven. Voor Tier S-quirks betekent dat: assert dat velden waarvan we weten dat de API ze stilletjes kan droppen, nog steeds aanwezig zijn.
// Minimal contract assertion for SP-API getOrderItems
export function assertOrderItemsShape(r: GetOrderItemsResponse, order: Order) {
for (const item of r.OrderItems) {
if (order.isCrossBorder && order.orderTotalEUR > 150) {
assert(item.ItemTax !== undefined,
`Tier-S quirk #2: ItemTax dropped on ${order.AmazonOrderId}`)
}
assert(item.SellerSKU,
`Tier-A quirk: SellerSKU missing on ${order.AmazonOrderId}`)
}
}
Die assert draaide op elke fixture, elke PR, drie maanden lang voor go-live. Hij ving quirk #2 twee keer, beide keren door een tijdelijke SP-API-regressie die het Amazon-team later stilletjes heeft gefixt. We hebben de assert laten staan.
Geeft een marketplace API 200 OK terug op een EU cross-border order boven €150? Vertrouw het BTW-totaal dan niet. Bereken het opnieuw uit landcode en item-level velden voordat de factuur wordt gerenderd. Twee van zeventien quirks hier falen precies in deze vorm.
Wat we nu standaard meeleveren
Elke marketplace-integratie die we bouwen begint met drie dingen in de repo voordat er één endpoint wordt aangeroepen:
- Een idempotency-laag op het eventId van de marketplace, met een dedup-window van 14 dagen.
- Een response-shape contract test per endpoint, die assertet op velden die volgens de docs optioneel zijn maar waar onze pipeline op leunt.
- Een shadow-run modus waarin de agent webhook-traffic leest, niets schrijft, en een week lang voor takeover een diff-rapport produceert tegen de vorige integratie.
Die shadow-week heeft de Almeerse launch gered. Het is niet glamoureus. Het vangt de quirks die de docs niet vangen.
Toen we de marketplaces-agent voor de Almeerse klant bouwden, was de quirk die ons bijna brak Tier-S #2: de stille BTW-drop boven €150. We hebben uiteindelijk de contract test hierboven toegevoegd, plus een finance-reconciliatiestap die onze factuurtotalen 's nachts vergelijkt met het settlement report van de marketplace. Dezelfde aanpak zit achter ons werk met AI-agents voor klanten met multi-marketplace voorraad.
De kleinste actie voor vandaag: open je API-clientlog van de afgelopen 24 uur, grep op orders waar ShipFromCountry verschilt van BuyerCountry, en check of de BTW-regel bij elke order boven €150 niet-nul is. Vijf minuten. Staat er ergens nul, dan heb je quirk #2 al live.
Kern
Geeft een marketplace API 200 OK op een EU cross-border order boven €150? Bereken de BTW dan zelf opnieuw. Twee van zeventien quirks falen precies in deze vorm.
FAQ
Waarom marketplace API-quirks ranken op stilte in plaats van op frequentie?
Een quirk die 200 OK teruggeeft terwijl hij een BTW-vlag verliest kost per geval meer dan een die honderd keer luid faalt. Stille fouten raken facturen en belastingaangiftes voordat iemand het doorheeft.
Treft de SP-API ItemTax-drop boven €150 ook binnenlandse Nederlandse orders?
Nee. We hebben hem alleen weten te reproduceren op EU cross-border orders waar ShipFromCountry verschilt van BuyerCountry. Binnenlandse NL-naar-NL orders geven ItemTax consistent terug in onze fixture set.
Is de Bol.com variant-merge-quirk om te draaien als hij de EAN al heeft omgezet?
Niet vanaf de integrator-kant. Je moet het catalogteam vragen om de varianten opnieuw te splitsen, wat nieuwe offer ID's genereert. Makkelijker om sowieso nooit downstream op EAN te keyen.
Staat het Marktplaats Pro vatRate-as-string-gedrag op de planning om te veranderen?
Niet dat wij weten. We overriden de serialiser voor dat ene veld en gaan door. Zie je een docs-update die dit tegenspreekt, dan testen we opnieuw.