Integrations
Marketplace API cheatsheet: 17 traps from a Bol.com rollout
A 23-person team in Almere shipped a marketplace agent into three APIs at once. Seventeen REST quirks nearly took the launch down. Ranked, with workarounds.

Friday 18:40, day before go-live. Twenty-three people in an Almere warehouse, the marketplaces agent due to take over Bol.com inbound at 09:00 Monday. Final smoke test: confirm a Belgian-address order, €189 inclusief BTW. Bol.com returns a clean 200 OK. Two hours later the customer's PDF lands without a VAT line. The agent did nothing wrong. The API did.
That order saved us. If it had been a German order at €120 we would have shipped a thousand of them through the new pipeline before the first complaint reached finance. The quirk is silent, the consequence is loud, and it is exactly the class of bug we built the cheatsheet for.
What follows are the seventeen REST quirks that surfaced while wiring one client's marketplace agent into Bol.com, Amazon SP-API, and Marktplaats Pro at the same time. Ranked, briefly explained, with the workaround we now ship by default.
How the ranking works
Every quirk gets one question: when it fires, does the integrator find out?
Tier S quirks return a 2xx and tell you nothing. By the time you notice, the damage is in the warehouse, on a customer invoice, or — worst case — in a tax filing. Tier A quirks fail loudly but for the wrong reason; you fix the symptom, the cause comes back next week. Tier B quirks are documented somewhere, badly, and cost a junior engineer an afternoon.
The cheatsheet lives in the repo as MARKETPLACE_QUIRKS.md and the test suite asserts against the worst of them.
Tier S: silent data loss
1. Bol.com variant-merge collapses the EAN to the parent
When two offers point at EANs that the catalog team later merges into one product (because they really are variants of the same SKU), the Retailer API silently re-assigns subsequent /retailer/orders responses to the surviving EAN. If you key inventory or shipping notes by EAN — and your downstream systems usually do — the orphaned EAN starts arriving in webhooks with stock counts that no longer match anything in your WMS.
The fix: don't trust an EAN as a primary key on the customer side. Always join through the offer ID, which is stable. Our agent now stores both and reconciles on mismatch.
2. SP-API loses BTW-inclusief on cross-border orders above €150
This is the one that caught us at 18:40. The IOSS (Import One-Stop Shop) threshold for low-value consignments sits at €150. Above that, SP-API's getOrderItems response stops emitting the ItemTax field for the buyer-facing total on EU cross-border orders, and ItemPrice becomes exclusive of VAT without changing the field name. The response code is still 200.
If your order confirmation template renders ItemPrice as the inclusief total, every German, Belgian, or French order above €150 ships with the wrong invoice. The fix is to check three fields together: ShipFromAddress.CountryCode, BuyerInfo.BuyerCountry, and the line-item total, then re-derive VAT from the marketplace's Orders API v0 tax classification rather than trusting ItemTax to be present.
3. Bol.com process-status webhooks double-fire on retry
Bol.com's async operations return a process-status URL; you poll or subscribe. The webhook variant retries on a non-2xx. Fine. The undocumented behaviour: a 2xx that arrives more than 4.8 seconds after dispatch is treated as a timeout and the webhook re-fires. So if your handler is slow, your "order accepted" event is processed twice. The second copy carries the same eventId, but only the first carries the dispatch timestamp.
Idempotency by eventId, always.
4. SP-API Listings v2021 patch overwrites the attribute array
Listings v2021 ships with a JSON Patch endpoint. The documentation reads as if a replace operation on an attribute array merges. It does not. It overwrites. If you patch /attributes/bullet_point with one new bullet, you delete the other four.
{
"productType": "PRODUCT",
"patches": [{
"op": "replace",
"path": "/attributes/bullet_point",
"value": [
{ "value": "New bullet copy", "marketplace_id": "A1805IZSGTT6HS" }
]
}]
}
The fix is to GET first, splice locally, and PATCH the full array. The endpoint accepts an add op with an array index that some account types ignore. Use the GET-modify-PATCH dance, even on simple fields. Yes, it doubles your call count.
5. Marktplaats Pro PATCH /adverts drops priceDecimal when currency is omitted
The Admarkt advert endpoint accepts a price as { "priceDecimal": 12.50 }. If you omit the currency field, which is documented as "defaults to EUR", the price is silently discarded server-side. The response is 200 OK and includes the advert with the previous price. Diff your PATCH against the response or you will keep wondering why the price never changes.
Tier A: loud failure, wrong reason
6. SP-API NextToken expires after 30 seconds
Pagination tokens on getOrders and getOrderItems are advertised as opaque and reusable. They are not. If your worker pauses for more than ~30 seconds between pages — say it dispatched the first page to a queue and is waiting for an ack — the next call returns InvalidInput. The error message points at the marketplace IDs, not the token. We chased the wrong rabbit for two days.
7. Bol.com rejects ISO 8601 timestamps with milliseconds
The Retailer API accepts 2026-06-19T08:00:00+02:00 and rejects 2026-06-19T08:00:00.000+02:00 as malformed. The error body says "invalid date-time". Most modern client libraries serialise with milliseconds by default. Strip them or you get 400s on every order-search.
8. Bol.com inventory parallel updates return 409 only sometimes
The /offers/{offer-id}/stock endpoint applies optimistic concurrency. Two updates for the same offer ID within roughly 250ms will sometimes return a 409 and sometimes return two 200s with the second one silently dropped. We have not reproduced a deterministic pattern. Serialise per-offer-id at the queue layer and the 409 disappears entirely.
9. SP-API Feeds API silently drops UTF-8 BOM
The XML feed processor expects UTF-8. If you upload UTF-8 with a byte order mark, the feed is accepted (202), the processing report comes back with zero errors, and zero records are processed. No warning. Send BOM-less or send UTF-16 with the documented marker.
10. Marktplaats Pro vatRate is a string, not a number
{ "vatRate": 21 } returns 400. { "vatRate": "21" } returns 200. Documented nowhere we could find. If you generate request bodies from a typed model, override the serialiser for this one field or your CI will be green and your prod calls will be 400s.
11. SP-API GetMyFeesEstimate excludes the referral fee on Prime SKUs
The endpoint returns a structure that looks complete. For SKUs enrolled in FBA Prime, the referral component is omitted. The total comes back lower than reality. If you base your pricing engine on this number, every Prime SKU is under-priced. The workaround is to look up the category's referral percentage from a static table and add it yourself.
Tier B: documented, just badly
12. Bol.com 404s when an EAN exists but lives on a parent SKU
Inventory lookups by EAN return 404 if the EAN belongs to a parent product without offers of its own. You have to traverse to children. The 404 is not a "not found in your account" — it is a "this EAN exists, but not where you think". We log them at warn level now and trigger a catalog-walk.
13. Marktplaats Pro bid daily limits reset at midnight UTC
The dashboard shows "today" as CET. The API enforces it as UTC. Between 00:00 CET and 02:00 CET, your daily-limit counter on the dashboard says zero while the API still rejects with "limit exceeded". If you run a 23:55 budget top-up, schedule it for 01:55 CET instead and avoid the two-hour blind spot.
14. Bol.com return labels return base64, then URL
First call to /shipments/return-labels for an order returns { "label": "JVBERi0xLj..." }, a base64-encoded PDF. Every subsequent call for the same return ID returns { "labelUrl": "https://..." }, a signed link with no PDF body. Handle both shapes or your archive misses every label after the first.
15. SP-API refresh_token rotates silently on some scopes
The LWA refresh flow returns a new refresh_token in the response on roughly one in twelve refreshes when the request includes the sellingpartnerapi::notifications scope. The old token continues to work for about an hour. If your token store overwrites on every refresh, you are fine. If it stores the original and only updates the access_token, you will get an invalid_grant the next morning. Always persist whatever refresh_token comes back.
16. Bol.com offer stock null is parsed as zero
Sending { "stock": { "amount": 0 } } sets the offer to out-of-stock. Omitting the stock field on a PATCH leaves the existing value. So far, expected. The quirk: sending { "stock": null } is parsed as out-of-stock, not as "do not touch". If your ORM emits null for absent fields, every PATCH zeroes the inventory.
17. Marktplaats Pro advert status case-sensitivity in search
POST /adverts with "status": "active" and "status": "ACTIVE" both return 200 and both create the advert. GET /adverts?status=active returns only the lowercase ones. Pick a case at the boundary and enforce it; we lowercase everything on the way in.
The two-line check that catches most of these
The cheatsheet is fine on paper. The version that runs in production is a contract test that asserts the response shape against what we expect, not what the docs promise. For Tier S quirks, that means asserting fields we know the API can silently drop are still present.
// 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}`)
}
}
That assert ran on every fixture, every PR, for three months before go-live. It caught quirk #2 twice, both times from a transient SP-API regression that the Amazon team later fixed quietly. We kept the assert.
If a marketplace API returns 200 OK on a cross-border EU order above €150, do not trust the VAT total. Re-derive it from country code and item-level fields before the invoice is rendered. Two of seventeen quirks here fail this exact shape.
What we ship by default now
Every marketplace integration we build starts with three things in the repo before a single endpoint is called:
- An idempotency layer keyed on the marketplace's eventId, with a 14-day dedup window.
- A response-shape contract test per endpoint, asserting fields the docs say are optional but our pipeline depends on.
- A shadow-run mode where the agent reads webhook traffic, writes nothing, and produces a diff report against the previous integration for a full week before takeover.
That shadow week is what saved the Almere launch. It is not glamorous. It catches the quirks the docs do not.
When we built the marketplaces agent for the Almere client, the quirk that nearly broke us was Tier-S #2: the silent BTW drop above €150. We ended up adding the contract test above and a finance reconciliation step that compares our invoice totals against the marketplace's settlement report nightly. The same playbook lives behind our AI agents work for clients running multi-marketplace inventory.
Today's smallest move: open your API client log from the last 24 hours, grep for orders where ShipFromCountry differs from BuyerCountry, and check whether the VAT line on every one of them above €150 is non-zero. Five minutes. If any are zero, you have already shipped quirk #2.
Key takeaway
If a marketplace API returns 200 OK on a cross-border EU order above EUR 150, re-derive VAT yourself. Two of seventeen quirks fail this exact shape.
FAQ
Why rank marketplace API quirks by silence instead of frequency?
A quirk that returns 200 OK while losing a VAT flag costs more per occurrence than one that fails loudly a hundred times. Silent failures hit invoices and tax filings before anyone notices.
Does the SP-API ItemTax drop above EUR 150 affect domestic Dutch orders?
No. We have only reproduced it on EU cross-border orders where ShipFromCountry differs from BuyerCountry. Domestic NL-to-NL orders return ItemTax consistently in our fixture set.
Can the Bol.com variant-merge quirk be reversed once it has flipped the EAN?
Not from the integrator side. You have to ask the catalog team to split the variants again, which generates new offer IDs. Easier to never key on EAN downstream in the first place.
Is the Marktplaats Pro vatRate-as-string behaviour scheduled to change?
Not that we have heard. We override the serialiser for that one field and move on. If you spot a docs update that contradicts this, we will retest.