DEV Community

FlareCanary
FlareCanary

Posted on

The Google Merchant API migration has a silent mispricing trap (Content API shuts down Aug 18, 2026)

If you push product data to Google Shopping through a custom integration — a feed builder, a PIM sync job, an internal Node/Python service that calls shoppingcontent.googleapis.com — you are on a clock. On August 18, 2026, Google permanently shuts down the Content API for Shopping. Every products.insert, every products.list, every inventory and price update against the v2.1 endpoints stops returning data and starts returning errors. There is no soft cutoff and no grace period after that date.

That part is loud. You'll know within minutes because the calls 404, the feed job alerts, and the catalog stops updating.

The part that bites quietly is the migration itself. The Merchant API is not a renamed Content API — it's an architectural rebuild with new resource shapes, a new identifier scheme, and a new money type. A migration done as a search-and-replace of the base URL will compile, authenticate, return 200, and silently write wrong data to your live catalog. Here are the three surfaces where that happens.

What actually changes

Surface Content API for Shopping Merchant API v1
Price amount price.value — decimal string ("15.99") price.amountMicrosint64 (15990000)
Price currency price.currency price.currencyCode
Resource identity merchantId + productId params name: accounts/{account}/products/{product}
Product ID format channel:contentLanguage:feedLabel:offerId contentLanguage~feedLabel~offerId
ID delimiter colon (:) tilde (~)
channel in ID present (online / local) removed
OAuth scope https://www.googleapis.com/auth/content per-sub-API scopes

Four of those rows produce no error when you get them wrong. They produce wrong data that the API happily accepts.

1. The amountMicros trap silently misprices your entire catalog

This is the one to fix before you touch anything else.

In the Content API, a price is a decimal string with a sibling currency:

{ "price": { "value": "15.99", "currency": "USD" } }
Enter fullscreen mode Exit fullscreen mode

In the Merchant API, the amount is an int64 count of micros, where one million micros equals one unit of currency:

{ "price": { "amountMicros": "15990000", "currencyCode": "USD" } }
Enter fullscreen mode Exit fullscreen mode

So $15.99 is 15990000. The conversion is Math.round(parseFloat(value) * 1_000_000).

Now look at what a field-renaming migration does. The natural instinct, when a guide says "the amount field name changed from value to amountMicros", is to map the old field onto the new name:

// WRONG — renames the field, skips the unit conversion
product.price = {
  amountMicros: oldProduct.price.value,   // "15.99"
  currencyCode: oldProduct.price.currency
};
Enter fullscreen mode Exit fullscreen mode

amountMicros is typed as int64, so the string "15.99" either gets coerced to 15 (truncated) or rejected depending on your client library — and 15 micros is $0.000015. Every product in the feed is now listed at effectively zero. Google accepts the write. The products stay "active." Your Shopping ads start serving at a price that doesn't match your landing page, which trips Google's price-mismatch checks days later — long after the deploy, with no stack trace pointing back at the migration.

The inverse mistake is just as quiet and more expensive: a team that knows about micros but applies the multiply twice (once in a mapping layer, once in a helper) ships 15990000000000 and lists a $16 product at $16 million. It won't sell, but it also won't error — it just silently stops converting.

There is no error path here. The only way to catch it is to diff the prices that landed in Merchant Center against the prices you intended to send.

2. The product ID delimiter flips from : to ~ — and drops channel

Content API product IDs are colon-delimited and lead with the channel:

online:en:US:SKU12345
Enter fullscreen mode Exit fullscreen mode

Merchant API product IDs are tilde-delimited and have no channel segment:

en~US~SKU12345
Enter fullscreen mode Exit fullscreen mode

Any code that builds or parses product IDs by string manipulation breaks silently:

// builds a malformed ID against the new API
const id = `${channel}:${lang}:${feedLabel}:${offerId}`;  // still using ':' and channel

// parsing an inbound Merchant API id by the old delimiter
const [channel, lang, feed, offer] = id.split(':');  // no ':' present → channel = whole string, rest undefined
Enter fullscreen mode Exit fullscreen mode

The build path creates a product the API treats as a different offer than the one you meant to update — so instead of updating SKU12345 you insert a duplicate, and the original goes stale until it expires. The parse path silently mis-shards your records, and because offer comes back undefined, downstream lookups quietly miss.

The dropped channel segment is its own trap: in the old format online:... and local:... were distinct products. The Merchant API moves the online/local distinction out of the identifier entirely. If your reconciliation logic keyed products by the full colon-string, your online and local variants now collide on the same key.

3. id becomes name, and scripts reading .id get undefined

The Content API returned a flat id on every product. The Merchant API follows Google's resource-name convention — the unique identifier is name, formatted accounts/{account}/products/{product}. There is no top-level id field on the new product resource.

Every script that does product.id, every database column populated from response.id, every log line keyed on the product ID reads undefined after the cutover. Nothing throws — JavaScript hands you undefined, Python hands you a KeyError only if you used [] instead of .get(), and most feed code uses .get() defensively precisely so a missing field doesn't crash the run. So the field goes missing and the run keeps going, writing null IDs into your sync table.

What to grep for before you cut over

A focused audit on every service that talks to shoppingcontent.googleapis.com:

  1. grep -rn "shoppingcontent.googleapis.com\|content/v2" . — every match is an endpoint that 404s on August 18, 2026.
  2. Search your price-mapping layer for the move to amountMicros. Confirm there is exactly one * 1_000_000 (or * 1e6) on the path from your stored price to the request body — not zero, not two.
  3. grep -rn "\.split(':')\|:\${" . near product-ID construction — colon-delimited ID logic that needs to become tilde-delimited and channel-free.
  4. grep -rn "\.id\b" . in your product sync code — reads that now need to be .name, and any DB column fed from the old flat id.
  5. Check your OAuth flow: the blanket auth/content scope is replaced by per-sub-API scopes. A token minted with only the old scope authorizes nothing on the new endpoints — and if you request a partial set, the calls that lack scope fail individually while the rest succeed, so a half-migrated scope grant looks like an intermittent outage rather than a config error.

The August 18 shutdown will get attention because it's a date on a calendar with a hard error attached. The migration that everyone does in the weeks before it is where the silent damage lives: a catalog that's fully "active" in Merchant Center, priced at $0.00 or $16 million, with duplicate SKUs and null IDs in your sync table — and not one line in the logs to say so.


FlareCanary monitors API responses for schema drift, silent removals, and behavior changes across upstream providers. If you sync product data to Google Shopping, Shopify, or anywhere else that ships breaking changes through migration guides, flarecanary.com catches the drift before your customers do.

Top comments (0)