$The symptom
On Amazon FBM (Fulfilled-By-Merchant), the lead_time_to_ship_max_days attribute on each listing tells Amazon how long after order placement the customer should expect the item to ship. For standard inventory: 1-2 days. For made-to-order businesses: 5, 10, 14, or even 21 days depending on the product.
If lead_time_to_ship_max_days is wrong (specifically, if it's lower than reality), three bad things happen:
- Amazon sets a "ship by" date the seller can't meet, and the order ages into "late shipment."
- The seller's Order Defect Rate (ODR) and Late Shipment Rate (LSR) increase. Both feed into Buy Box eligibility.
- If LSR crosses Amazon's threshold (typically 4%), the listing or account gets suppressed.
On the engagement, the client's late-shipment rate had been creeping up for months. Buy Box win rates had been declining. Nobody had connected those two facts to a specific cause. The cause, when I found it, was a wipe-on-every-fulfillment-PATCH that had silently zeroed the lead-time field across the entire active FBM catalog.
$The bug
Most Odoo + Amazon connectors push listing updates via Amazon's SP-API Listings endpoint:
PUT /listings/2021-08-01/items/{seller}/{sku}
{
"productType": "...",
"attributes": {
"lead_time_to_ship_max_days": [{"value": 10, "marketplace_id": "..."}],
"fulfillment_availability": [{...}],
...
}
}
That's the full-listing PUT. Every attribute you want on the listing must be in the payload. If you omit an attribute, Amazon's behavior depends on whether you're using PUT or PATCH:
- PUT = full replace. Anything you omit gets unset.
- PATCH = partial update. Anything you omit stays as-is.
The bug is in connectors that use PATCH but treat the listing payload as "what the integration knows about this listing right now." When the fulfillment integration fires (because an order was packed, or because inventory changed, or because tracking was updated), it constructs a PATCH that includes the fields the fulfillment system cares about, but omits the fields that other systems own.
For PATCH semantics, this is correct: omitted fields stay as-is. Amazon's SP-API, however, sometimes treats specific listing attributes as "all-or-nothing", and lead_time_to_ship_max_days is one of them. If the PATCH includes fulfillment_availability but not lead_time_to_ship_max_days, and the field has been previously set, the PATCH treats the omission as "unset this field." The listing's lead-time reverts to Amazon's default (often 2 days).
The behavior is inconsistent and not documented as such. From the SP-API side, the call returns 200 OK. The processing report shows success. The only way to detect the wipe is to GET the listing afterward and compare the attribute to what you sent.
$How to detect it on your catalog
Pull the current state of all your FBM listings, look at lead_time_to_ship_max_days, group by value:
from collections import Counter
def audit_fbm_lead_times(sp_client, seller_id, marketplace_id):
counter = Counter()
listings = []
for sku in get_active_fbm_skus(seller_id):
r = sp_client.get(
f"/listings/2021-08-01/items/{seller_id}/{sku}",
params={"marketplaceIds": marketplace_id, "includedData": "attributes"},
)
attrs = r.json().get("attributes", {})
lt = attrs.get("lead_time_to_ship_max_days", [])
value = lt[0].get("value") if lt else None
counter[value] += 1
listings.append({"sku": sku, "lead_time": value})
# Compare to expected. If your business is MTO with 7-14 day lead times,
# a cluster at 1 or 2 days is the wipe symptom.
for v, n in counter.most_common():
print(f"lead_time={v} days: {n} listings")
return listings
What you want to see: distribution matching your actual production timeline. A made-to-order business should have most listings at 7-14 days. A standard-inventory business should have most listings at 1-3 days.
What raises a flag: a giant cluster at 1 or 2 days that shouldn't be there. If your products legitimately ship in 1-2 days, no bug. If your business is MTO and you're seeing 90%+ of listings at 1-2 days, you have the wipe.
On the prior engagement: out of 7,000 active FBM listings, 6,838 had been zeroed to 1 or 2 days. The remaining 162 were either correctly set or had been re-pushed by a different code path recently.
$The fix
Two steps. First, re-push correct lead times across the active set. Second, fix the integration so the wipe stops happening.
Step 1: re-push correct lead times
For each affected listing, compute the correct lead time from your Odoo data (typically product.template.sale_delay or a per-SKU override), then issue a focused PATCH that includes ONLY the lead-time field:
def repush_lead_time(sp_client, seller_id, sku, lead_time_days, marketplace_id):
payload = {
"productType": get_product_type(sku), # required on every Listings PATCH
"patches": [{
"op": "replace",
"path": "/attributes/lead_time_to_ship_max_days",
"value": [{
"value": lead_time_days,
"marketplace_id": marketplace_id,
}],
}],
}
r = sp_client.patch(
f"/listings/2021-08-01/items/{seller_id}/{sku}",
params={"marketplaceIds": marketplace_id},
json=payload,
)
return r.json()
The "op": "replace" on a JSON-Pointer path is the JSON Patch idiom that SP-API's Listings PATCH supports. Single attribute, single operation, clean.
Wait 5-10 minutes after the batch completes, then GET each listing back and verify the field is set correctly. If anything still reads wrong, retry. Amazon's processing is async; the field doesn't update instantly.
Step 2: fix the fulfillment integration
The harder part. The wipe is happening because the fulfillment code path doesn't preserve fields it doesn't own. Two patterns to fix it:
- Read-modify-write. Before any PATCH, GET the current listing state. Merge your changes into the existing attribute set. PATCH the full merged set. Slower (extra GET per PATCH) but safe.
- Narrow the PATCH. Restructure the fulfillment integration to PATCH only the specific JSON-Pointer paths it cares about (e.g.,
/attributes/fulfillment_availability) using explicitop: replaceoperations. The fields it doesn't touch stay untouched.
The narrow-PATCH pattern is cleaner. The Listings API's JSON Patch support is exactly designed for this. Most off-the-shelf connectors don't use it; they construct full-attribute payloads and rely on PATCH semantics to leave omitted fields alone. That's the assumption Amazon's API quietly violates for fields like lead-time.
$Specific to MTO businesses
If you're a made-to-order operator on Odoo + Amazon FBM, the wipe is especially expensive. Made-to-order businesses use elevated lead times (7-14 days typically) precisely because they can't ship in 1-2 days. When the wipe drops your lead time to Amazon's 2-day default:
- Customers expect the order in 2 days.
- You actually ship in 7-14 days.
- Every order ages into late-shipment territory.
- Late Shipment Rate climbs. Order Defect Rate climbs. Buy Box win rate drops.
- The business owner sees declining sales and assumes "marketing" or "competition." The actual cause is a silent listing-state corruption.
The pattern compounds because the wipe re-fires every time the fulfillment integration touches a listing, which on an active FBM catalog is multiple times per day per SKU. Re-pushing lead times once isn't enough; the fix has to be at the integration level, otherwise the field gets wiped again within hours of the re-push.
On the prior engagement, this single bug was the largest silent revenue driver I found. Closing it (re-push + integration fix) returned the catalog to its actual production timeline and gave the LSR a path back to compliance.
$The transferable diagnostic
The pattern (Amazon attribute that gets silently wiped by a connector's PATCH that omits it) applies to more than just lead-time. Other fields that exhibit this behavior intermittently:
handling_time(similar to lead-time, different surface)max_order_quantityon bundled listingscondition_typeon used/refurbished listings- Some marketplace-specific compliance fields (e.g.,
battery,hazmat)
The diagnostic is the same in each case: GET the listing, group by the field, look for unexpected distributions. If your catalog has a giant cluster at a value that shouldn't be there, the wipe is running.