Developer notes
Technical reference for developers who customize Google Merchant API Sync.
Add-on identity
Section titled “Add-on identity”| Item | Value |
|---|---|
| Add-on id | kv_google_merchant |
| Function prefix | fn_kv_google_merchant_ |
| Language variables | kv_google_merchant.* |
| CSS scope | .kv-google-merchant |
| PHP namespace | Tygh\Addons\KvGoogleMerchant |
Why this add-on exists (official source)
Section titled “Why this add-on exists (official source)”The Content API for Shopping reached its sunset on 2026-08-18; requests without an approved extension fail intermittently with HTTP 410 from 2026-09-01 and all endpoints are decommissioned in early 2027 (date may change). The replacement is the Merchant API. Source: https://developers.google.com/shopping-content/guides/sunset (read 2026-11-07).
File feeds (the add-ons Product Feed Pro for Google Merchant Center and Multichannel Product Feeds) are not part of that sunset and are not replaced. This add-on is the direct API channel next to them: it pushes changes within minutes, removes products at once, and reads back the Google status and item level issues. It can use the Google category mapping of Product Feed Pro as a read-only fallback.
Structure
Section titled “Structure”| Class | Role |
|---|---|
Client\BaseClient |
HTTP (cURL, TLS verification, timeouts), JSON, error mapping, masked logging, one forced re-login on 401 |
Client\Api |
Every Merchant API call and the Google sign-in (refresh token grant, service account JWT), see the endpoint table |
Mapper |
Pure translation: ProductInput, price micros, GTIN check, status and issues from products.list, data source body |
Items |
Product selection per target (storefront, vendors, categories, stock) and the normalized item (price in the target currency plus tax, availability, images, features) |
Mapping |
Attribute sources (auto / field / feature / fixed / none) and Google product category per category, per storefront |
Targets |
Targets (= API data sources: language + feed label + country), duplicate guard, data source create / adopt |
ProductMap |
One row per target and product: offer id (fixed before the first call), state Q/S/E/R/X, payload hash, Google status, issues |
Sync |
push (insert / delete), pollStatus (products.list), seed (full sync slices) |
Jobs, Queue, JobException |
Jobs push, full_sync, poll_status; back-off 5 min / 15 min / 1 h / 3 h / 12 h; 429 uses the wait the API asks for (at least 2 min) |
Accounts, Secret |
Credentials sealed with AES-256-GCM |
Config, Access |
Settings; the account is the store / marketplace owner’s, vendors have no access |
Log |
Event log with masking (tokens, client secret, private key, assertion), filters, CSV, retention |
Merchant API endpoints used
Section titled “Merchant API endpoints used”Base: https://merchantapi.googleapis.com. OAuth scope for all calls: https://www.googleapis.com/auth/content. Status of each entry: verified = read on the official reference page on 2026-11-07, flagged = not confirmed on an official page (see the note).
| Purpose | Call | Source | Status |
|---|---|---|---|
| Insert / replace an offer | POST /products/v1/accounts/{account}/productInputs:insert?dataSource=accounts/{account}/dataSources/{ds} (only API data sources) |
reference products_v1/accounts.productInputs/insert |
verified |
| Delete an offer | DELETE /products/v1/accounts/{account}/productInputs/{language}~{feedLabel}~{offerId}?dataSource=... (empty body, {} answer) |
reference .../productInputs/delete |
verified |
| Product identity | contentLanguage~feedLabel~offerId. In the get and delete paths the segment is an UNPADDED base64url string when any part holds /, % or ~ (en~US~sku/123 is ZW5-VVN-c2t1LzEyMw); the add-on sends the plain form only for parts made of A-Z a-z 0-9 . _ - |
reference productInputs/delete (“This encoding MUST be used if any part of the product identifier contains characters such as /, %, or ~”) |
verified |
| Same identity again = replace | insert with the same language, label, offer id and data source replaces the entry (this is why a retry after a timeout cannot duplicate) | guide products/add-manage |
verified |
| Refresh every 30 days | “Refresh products at least every 30 days to prevent expiration”: offers are re-sent after 25 days even when unchanged | guide products/add-manage |
verified |
| Read processed products | GET /products/v1/accounts/{account}/products (pageSize max 1000, default 25; pageToken; answer products[], nextPageToken) |
reference .../products/list |
verified |
| Read one | GET /products/v1/accounts/{account}/products/{language}~{feedLabel}~{offerId} |
reference .../products/get |
verified |
| Status and issues | productStatus.destinationStatuses[] {reportingContext, approvedCountries[], pendingCountries[], disapprovedCountries[]}, itemLevelIssues[] {code, severity NOT_IMPACTED/DEMOTED/DISAPPROVED, attribute, description, detail, documentation, applicableCountries[]} |
reference products_v1/accounts.products (ProductStatus) |
verified |
| Create data source | POST /datasources/v1/accounts/{account}/dataSources, body DataSource with displayName and primaryProductDataSource {feedLabel, contentLanguage, countries[]} |
reference datasources_v1/accounts.dataSources/create |
verified (DataSource.input is output only; “Only generic data sources can be created through the API”; the guide says products can only be inserted into data sources of type API, so adoption by name also requires input = API and the same feed label and language) |
| List data sources | GET /datasources/v1/accounts/{account}/dataSources (pageSize, pageToken, dataSources[], nextPageToken) |
reference datasources_v1/accounts.dataSources lists the method list; the path follows the create path |
verified (GET https://merchantapi.googleapis.com/datasources/v1/{parent=accounts/*}/dataSources, pageSize max 1000, pageToken), used as the connection test and to adopt a data source after a timeout |
| Price | {amountMicros: "15990000", currencyCode: "USD"}; amountMicros is a string, 1 unit = 1,000,000 micros |
reference Shared.Types/Price, guide example |
verified |
| Attributes | title, description, link, imageLink, additionalImageLinks[], availability (IN_STOCK, OUT_OF_STOCK, PREORDER, LIMITED_AVAILABILITY, BACKORDER), condition (NEW, USED, REFURBISHED), price, salePrice, gtins[] (up to 10), mpn, brand, googleProductCategory, productTypes[], identifierExists, shipping[], itemGroupId, color, size, shippingWeight {value, unit}, gender (MALE, FEMALE, UNISEX), ageGroup (ADULT, KIDS, TODDLER, INFANT, NEWBORN) |
reference products_v1/ProductAttributes |
verified |
| Shipping object | shipping[] {price, country}; none of the Shipping fields is required (speeds need both maxHandlingTime and maxTransitTime, which are not sent) |
reference products_v1/ProductAttributes (Shipping) |
verified |
| Tax | no taxes attribute is sent; VAT is added to the price by the target’s “Add tax” percentage when needed |
v1 ProductAttributes has no taxes field |
verified (intentional) |
| Title / description limits | 150 / 5000 characters | Google product data specification (support.google.com/merchants/answer/7052112): title 150, description 5000, id 50, mpn 70, brand 70, color 100, size 100, item_group_id 50, product_type 750, up to 10 additional images, 2000 per URL | verified |
| Authentication | OAuth 2.0 (web server flow: https://accounts.google.com/o/oauth2/v2/auth, access_type=offline, prompt=consent, state; token and refresh at https://oauth2.googleapis.com/token) or a service account (JWT RS256, claims iss/scope/aud/iat/exp<=1 h, grant urn:ietf:params:oauth:grant-type:jwt-bearer). API keys are not supported. |
guides authorization/overview, authorization/access-your-account; identity guides oauth2/web-server, oauth2/service-account |
verified |
| Service account access | The service account address is added as a user in Merchant Center (Settings, Access and services) | guide authorization/access-your-account |
verified |
| Developer registration | MANDATORY, once per Cloud project: POST /accounts/v1/accounts/{account}/developerRegistration:registerGcp, optional body {developerEmail}; answer DeveloperRegistration {name, gcpIds[]}. Unregistered projects get 401 UNAUTHENTICATED on every other call; calls work about 5 minutes after registering. Needs a production account with a verified homepage, ADMIN access of the caller; test accounts are not eligible; ALREADY_REGISTERED when the project is tied to another account. Status: GET /accounts/v1/accounts/{account}/developerRegistration |
guide quickstart/registration, reference accounts_v1/accounts.developerRegistration |
verified; called automatically after the first connect and from the Connection page |
| Availability date | availabilityDate (ISO 8601 / RFC 3339 UTC) is sent for PREORDER (required by the specification) and for BACKORDER when a future date is known; a BACKORDER without a date is sent as OUT_OF_STOCK unless the setting says otherwise |
data specification availability_date, ProductAttributes |
verified |
| Item group | itemGroupId = parent product id for variation products |
ProductAttributes, specification (item_group_id) |
verified |
| OAuth hardening | state (random, bound to the session and the database) and PKCE (code_challenge S256, code_verifier on the exchange) |
identity guide oauth2/web-server |
verified |
| Quotas | HTTP 429 RESOURCE_EXHAUSTED, quota/request_rate_too_high (per minute, rolling) and quota/daily_limit_exceeded (daily, resets 12:00 UTC); retry with exponential back-off; spread calls |
guide quotas-limits/quotas |
verified |
| Batching | The Merchant API has no custom batch. Requests are sent one by one; the add-on stops at the first 429 and the job retries later | guide compatibility/refactor-concurrent-requests |
verified |
Not in version 1.0 (and why): notifications / push subscriptions (status is polled instead, every 6 hours by default), local and regional inventories (those are for local stores and regional pricing; online price and stock go through productInputs), promotions, product reviews, accounts and users management, supplemental data sources. They can be added later without changing the data model.
Sync rules
Section titled “Sync rules”- Identity. Offer id = product id (or product code, setting) stored in the map row BEFORE any call. The identity is
contentLanguage~feedLabel~offerIdplus the data source. - Idempotent. A timeout leaves the row queued; the identical insert is sent again and replaces the same offer. A data source is looked up by its display name before it is created, so a timeout during creation does not make a second one. A delete that answers 404 counts as done.
- Change detection. The ProductInput is hashed; an unchanged product is not sent (but is refreshed after 25 days).
- Triggers.
update_product_post(any save, including price, status, category),update_product_amount_post(stock moves from orders),delete_product_post. A daily full check re-evaluates every product (no API calls for unchanged ones) and catches changes made behind the hooks (imports, SQL). - Eligibility. Active products, not downloadable-only types, target filters (storefront, vendors, categories, out of stock). Products that leave the selection are deleted at Google when “Remove disabled products from Google” is on.
- Errors. HTTP 400/404/409/422 from the PRODUCT call belong to one product (state E with the message, the run continues). A refused LOGIN (token endpoint 400
invalid_grant/invalid_client, 401) is anAuthException: never a product error, the job stops as permanent and the Connection page shows the failure. 401/403 from the API stop the job as permanent. A 400/404 that names the data source (the merchant deleted it) clears the stored data source name, resets the rows and retries in a minute (the source is created again). 429 and 5xx/network stop the run and retry with back-off;quota/daily_limit_exceededwaits until 12:00 UTC, and no new push job is queued while a back-off runs. - Links. Always https on the target’s host (Ultimate: the storefront of the target; otherwise the Storefront URL setting; otherwise the default storefront), never dependent on the cron context;
¤cy=XXXis added when the target currency is not the primary one. - Removal queue.
kv_google_merchant_removalsholds offers that must leave Google although no map row points at them (target deleted, identity changed, “Remove all”): data source and identity as they were sent. Thecleanupjob works it off; 404 or a vanished data source count as done. It also runs when the licence is off. - Lost updates. Every map row has a version bumped on each change; a push that was in flight marks the row synced only when the version is unchanged.
Database tables
Section titled “Database tables”| Table | Purpose |
|---|---|
?:kv_google_merchant_accounts |
Connection (secrets sealed), last check, token cache |
?:kv_google_merchant_targets |
Targets / data sources |
?:kv_google_merchant_product_map |
Offers per target |
?:kv_google_merchant_attr_map, ?:kv_google_merchant_category_map |
Mapping per storefront |
?:kv_google_merchant_jobs, ?:kv_google_merchant_log |
Queue, log |
?:kv_google_merchant_removals |
Offers waiting to be deleted at Google |
Hooks used
Section titled “Hooks used”PHP: update_product_post, delete_product_post, update_product_amount_post (init.php); init_templater_post only while an upgrade synchronization is pending. No template hooks in the storefront: the add-on prints nothing there (full page cache safe).
Test the HTTP layer without Google
Section titled “Test the HTTP layer without Google”The base URL can be overridden for tests in config.local.php (development only); the same address then also serves /token and /auth:
$config['kv_google_merchant_base_url'] = 'http://127.0.0.1:11830';node tests/mock-server/server.js 11830 starts a stand-in that follows the reference (validates the ProductInput, replaces by identity, 404 on delete, paging, Google error bodies, OAuth and JWT grants); node tests/e2e/e2e.mjs --store ... --db ... --root ... runs the HTTP end-to-end test; php tests/unit.php runs the unit tests (fixtures in tests/fixtures, modeled on the official examples).
Template overrides
Section titled “Template overrides”Admin templates live in design/backend/templates/addons/kv_google_merchant/. Do not edit them in a store; they are replaced on update.