Content API for Shopping Is Off: Merchant API v1, After the Fact
Google sunset Content API for Shopping on 18 August 2026. The hostname still answers and the discovery document still updates, so a 200 proves nothing. What is left is extended access, a registration step that fails by returning an empty list, and prices in micros.
Google’s migration overview says it plainly: Content API for Shopping was sunset on 18 August 2026. If a cron job somewhere still POSTs to shoppingcontent.googleapis.com, the symptom is not a stack trace in your logs. It is a client asking why their products came out of Shopping.
Almost everything written about this shutdown was aimed at merchants and amounted to “contact your feed provider.” That is no help if you are the feed provider, or if the uploader is a script somebody wrote in 2019 and nobody has opened since.
First: a 200 does not mean you are fine
On 23 August 2026, five days after the sunset, https://shoppingcontent.googleapis.com/$discovery/rest?version=v2.1 still returns 200. The document is stamped revision 20260819, dated the day after the shutdown, still lists all 28 resources, and carries no deprecation flag on products.insert.
Do not read anything into that. Discovery documents are static artifacts and the host serves plenty of other things. Enforcement here is per account, not per endpoint, which is exactly why the hostname is still up: Google is granting extended access to accounts that ask for it, and that would be impossible if the service had been switched off wholesale.
So check the account, not the host. The API usage report under Merchant Center account settings tells you whether anything has called Content API in the last 30 days. Do that even if the codebase greps clean, because the most common finding is a service you do not own.
rg -n 'shoppingcontent|content/v2\.1|ShoppingContent|shopping_content' \
--hidden --glob '!node_modules'
Client libraries hide the string, so look for those too: google-shopping-content on Node, build('content', 'v2.1') in google-api-python-client, ShoppingContent in google/apiclient-services on PHP, google-api-services-content on Java.
If you are broken and the migration is weeks away, the extension request is the lever, and it is the only one. Google has not published a deadline for requesting it or a maximum length, so treat the answer as unknown rather than assuming generosity.
The registration step that fails by succeeding
Before any v1 method works, the Google Cloud project has to be registered against the Merchant Center account. Once per project:
curl -X POST \
"https://merchantapi.googleapis.com/accounts/v1/accounts/{ACCOUNT_ID}/developerRegistration:registerGcp" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-d '{"developerEmail": "you@example.com"}'
The email has to belong to a user with Admin access on the account. If that user exists they gain the API_DEVELOPER role; if not, an invitation goes out and somebody has to accept it before anything works.
Skip this and your calls still authenticate. You get a 200 and an empty list. No permission error, no hint, so engineers reasonably conclude the query filter is wrong and spend an afternoon in the wrong file. If your first Merchant API call succeeds and returns nothing, check registration before touching anything else. There is a developerRegistration:getDeveloperRegistration method and an accounts:getAccountForGcpRegistration method for confirming it without guessing.
The auth story is otherwise unchanged. The OAuth scope is still https://www.googleapis.com/auth/content, so existing tokens, refresh tokens, and service accounts carry over with no re-consent.
One API becomes a dozen
Content API was one monolithic service. Merchant API is a set of independently versioned sub-APIs, each with its own path segment: accounts, products, inventories, datasources, promotions, reports, quota, notifications, lfp, issueresolution, ordertracking, conversions.
Every URL takes the same shape, https://merchantapi.googleapis.com/{SUB_API}/{VERSION}/{RESOURCE_NAME}:{METHOD}, and resources are addressed by hierarchical names rather than loose id pairs. Where you passed merchantId and productId separately, you now pass accounts/12345/products/en~US~sku123.
That product segment is contentLanguage~feedLabel~offerId, and legacy channel-scoped products carry a four-part variant with the channel in front. Do not assemble it by hand. The v1 resources carry a base64EncodedName field precisely because the tilde-delimited form needs URL encoding, and hand-built keys with the wrong delimiter or field order are the single most common bug in this migration.
What you write and what you read are now different resources
ProductInput is what you write. Product is what Google assembled from your writes after applying data source rules and merging supplemental feeds. The products sub-API offers exactly two methods on Product, get and list, and there is no way to write to it.
This is a genuine improvement once it clicks. Under Content API, “update the price” patched the processed product and your change could be silently clobbered the next time any feed uploaded. Under productInputs.patch you modify one input in one data source, and it persists until you change that input. Which is why dataSource is required on every write: the API is forcing you to say which layer you are editing.
The separate productstatuses service is gone entirely. Disapprovals and item-level issues now live on productStatus inside the Product you already fetched, so a two-call pattern collapses into one.
The hazard is ownership, not errors
A product’s identity is (contentLanguage, feedLabel, offerId), and all three are immutable on the input. Insert that tuple into a different primary data source than the one that owns it today, and the product moves. It leaves the old source and adopts the new one’s rules and settings.
The behavior is intentional and occasionally useful for reorganizing a catalog. It is also exactly what a migration script with a hardcoded dataSource does to a catalog spread across several feeds. No error, no warning. Products that quietly inherit the wrong rules.
List your data sources through datasources/v1/accounts/{account}/dataSources and map every offer to its current owner before you write anything.
Field changes that pass validation and produce wrong data
Prices are integer micros. {"price": {"value": "49.99", "currency": "USD"}} becomes {"price": {"amountMicros": "49990000", "currencyCode": "USD"}}. Do the conversion in integer cents, never floats, because 49.99 * 1_000_000 in a language with IEEE doubles is a coin flip between 49990000 and 49989999, and a rounding error on a price is a disapproval.
targetCountry is not in v1 at all. Advice from the overlap period told you to send both it and feedLabel and keep them consistent; the v1 ProductInput schema has no such field, so that advice is dead. What matters now is that (feedLabel, contentLanguage) lines up with what your Google Ads campaigns target. Get it wrong and there is no disapproval and no error. The product just stops serving, which is the failure people describe as “the migration worked but ads died.”
Product fields such as title and price moved under a productAttributes object rather than sitting at the top level, so anything doing product.title needs a hop. Enums are real enums now: "in stock" becomes IN_STOCK. The mapping is not a uniform search and replace, so read the reference for each field rather than pattern-matching from one example.
Local inventory changed types too. quantity went from string to integer, regionId became region, and the channel field is gone in favor of a legacyLocal boolean. On the plus side, list and delete exist now, which they did not before.
customBatch is gone and nothing replaces it
There is no batch endpoint in v1. Google’s answer is concurrent individual requests, with channel pools if you are on gRPC.
If you were pushing 50,000 products in batches of 1,000, you now need a worker pool with bounded concurrency and real retry logic. Start conservative and raise concurrency while watching for RESOURCE_EXHAUSTED; the quota sub-API exposes your actual limits at quota/v1/accounts/{account}/limits, so read them rather than assuming the old Content API numbers carried over. Retry only on 429 and 5xx with backoff and jitter, since retrying a 400 on a malformed product just burns quota.
The error shape changed as well. customBatch returned one response with per-entry errors; now every request fails independently. A handler that parses the batch envelope needs a rewrite rather than a tweak, and a retry loop written against the old shape will happily swallow real failures.
This is the largest chunk of work in most migrations.
If a vendor owns your feed
If products reach Merchant Center through Shopify, BigCommerce, WooCommerce’s Google listings extension, or a feed tool like Channable, DataFeedWatch, Productsup, or Feedonomics, the vendor did this migration. Your job is confirming it, which means an explicit Merchant API v1 statement in their changelog, not a vague post from the spring.
The risk is custom middleware. The pattern that bites is a platform app handling the main catalog while a small internal service pushes price or stock updates more often than the app does. That service is yours, it is undocumented, and it is the one that broke on 18 August.
If you are already down
Cut scope to the write path. Register the GCP project, port productInputs.insert for the primary catalog with the correct dataSource and feedLabel, and get prices into micros. That keeps products alive.
Everything else can lag. Reporting can go dark for a few weeks and nobody loses revenue over a stale dashboard. Promotions, local inventory, and conversion sources can follow. The API does not require you to move every surface at once, and partial migration is a legitimate state to sit in.
The other August deadline in this shape was the OpenAI Assistants API shutdown, which had the same property: the thing you could not defer was the one with data behind it.
Open the API usage report first. Thirty seconds, and it tells you whether this is a three-week project or a tab you can close.