Sends your OpenCart 4 catalogue to Google Merchant Center through the Merchant API (the successor of the Content API for Shopping) with your own Google Cloud project (OAuth 2.0 or a service account): prices, stock and status changes reach Google within minutes, Google's issues are listed in your admin, and a scheduled Google Shopping XML / TSV feed is the fallback. Targets per country, language and currency, category mapping to the Google product taxonomy, per-product exclusions and overrides, variants as item_group_id, a job queue with retries. OpenCart 4, no OCMOD. Google, Google Merchant Center and Google Shopping are trademarks of Google LLC. This extension is not affiliated with or endorsed by Google.
Tested on 4.1.0.4Version 1.0.0Compatible with OpenCart 4.1.0.4 No core file edits
Built forOpenCart
Your catalogue in Merchant Center1 of 6
What it does
Sends your OpenCart 4 catalogue to Google Merchant Center through the Merchant API (the successor of the Content API for Shopping) with your own Google Cloud project (OAuth 2.0 or a service account): prices, stock and status changes reach Google within minutes, Google's issues are listed in your admin, and a scheduled Google Shopping XML / TSV feed is the fallback. Targets per country, language and currency, category mapping to the Google product taxonomy, per-product exclusions and overrides, variants as item_group_id, a job queue with retries. OpenCart 4, no OCMOD. Google, Google Merchant Center and Google Shopping are trademarks of Google LLC. This extension is not affiliated with or endorsed by Google.
What it changes for your store
Google Merchant Center without the retired API. Google's Content API for Shopping was switched off from 2026-08-18. This extension sends your OpenCart 4 products to Merchant Center through the Merchant API, with your own Google Cloud project, and keeps a scheduled Shopping feed as the fallback.
Know the limits. You need a Merchant Center account with a verified website and a Google Cloud project of your own; Google requires the project to be registered once for the Merchant API. The Merchant API has no tax field: prices carry the VAT you choose per target, US sales tax is set in Merchant Center. Option combinations of a single product are not split into separate offers (OpenCart 4 variant products are). OpenCart has no availability date for back orders, so those products go out as out of stock unless you choose otherwise. Tested against a stand-in of the Merchant API built from Google's documentation, not against a live Merchant Center account.
Features
Merchant API sync
Products, prices, stock and sale prices with dates as product inputs, one API data source per country and language
A full sync first, then every change within minutes: saving a product, an order that moves stock, a variant edit, a delete
Safe retries: a lost answer never creates a second offer; unchanged products cost no call; disabled, excluded and deleted products are removed from Google
Quota aware: a 429 waits as long as Google asks, batches and a job queue with back-off
Your own Google Cloud project
OAuth 2.0 with PKCE or a service account key; secrets stored encrypted, the log masked
You come back to your admin still signed in after the Google consent screen
Developer registration of the Cloud project in one click, with its state shown
Google's issues in your admin
Approved, pending, partly approved and disapproved counts per target
Every item level issue with Google's description, severity, attribute, countries and the help link
Filters for disapproved products, errors, queued and excluded
Targets, mapping and overrides
One target per store, language, country and currency: multi-currency and multi-language, tax class rates or a fixed percent, shipping price hint, category filter
GTIN from EAN, UPC, JAN or ISBN (check digit verified), MPN from MPN, SKU or model, brand from the manufacturer, colour, size, gender and age group from attributes
Your categories mapped to the Google product taxonomy with a search box
Exclude a product or override its title, category, identifiers, condition, colour, size, gender and age group
OpenCart 4 variants share their master product's id as item_group_id
Fallback Shopping feed
XML and TSV files in Google's product data specification, per target, behind a secret address
Rebuilt by cron in batches, written to a temporary file and moved into place when complete
Reliable
Cron guard (key or OpenCart cron only), one queue run at a time, Jobs page with Retry, log with CSV export
Settings, targets, mapping and overrides kept on update and uninstall
Events and cron only, no OCMOD, no core edits; OpenCart 4.1, tested on 4.1.0.4
What you get
Download & installOne installer package (.ocmod.zip) for OpenCart 4.1.0.0 – 4.1.x. There is no OpenCart 3 edition. Download it from My licenses right after payment.
12 months of updates & supportNew versions and help for 12 months (24 or 36 months at checkout). After that the extension keeps working; renew anytime at 50%.
License for your storeOne production domain with all its subdomains; staging and local development hosts are free. How licensing works
30-day refundFull refund within 30 days of your first purchase, no reason needed. Refund policy
Tested buildsOpenCart 4.1.0.4. Tested on OpenCart 4.1.0.4.
Documentation & supportFull guide (Installation and setup, Configuration, Usage, Developer notes) on this page. Tickets from your account, same-day reply on weekdays (Mon-Fri 09:00-18:00 UTC+3).
Compatibility
Google Merchant Center Sync works with OpenCart 4.1.0.4.
We list the builds we install and test on. “Tested” means the test plan ran on that build; “Supported” means the build is inside the supported range.
Compatibility
Version
Range
Runs on
Status
OpenCart 4.1.0.x
4.1.0.0 – 4.1.0.4
PHP 8.1 – 8.3
Tested on 4.1.0.4
1.0.0
Latest
First release: OpenCart 4.1 port of the CS-Cart Google Merchant API Sync 1.0.0 with the scheduled file feed of the CS-Cart Product Feed Pro merged in. Merchant API product sync (product inputs into API data sources, status and item level issues read back, developer registration, OAuth 2.0 with PKCE through the cookie-less return page extension/kv_google_merchant/return.php, or a service account), job queue with retries and a cron guard, sealed credentials, settings and licence kept across updates.
OpenCart additions: targets per store, language, country and currency (tax class rates, fixed percent or none), category filter, per-product exclusions and overrides, attribute sources from OpenCart 4.1 identifier codes (EAN, UPC, JAN, ISBN, MPN, SKU), Google product taxonomy search, OpenCart 4 variants as item_group_id, special prices with dates in the shop time zone (salePriceEffectiveDate), XML and TSV feed files per target (batches of 200, atomic write, secret address), change events from the product form, variants and orders.
Tested on OpenCart 4.1.0.4 (PHP 8.3 store; unit tests on PHP 8.1 and 8.3) against a stand-in of the Merchant API, not a live Merchant Center account.
How do I install the extension?
Upload. Extensions > Installer: upload the .ocmod.zip. It is for OpenCart 4 (4.1.0.0 to 4.1.x).
Install and enable. Extensions > Extensions > Modules: install, open the settings and switch it on. Events register themselves; nothing is patched.
Enter the license key. Paste the key from My licenses. Subdomains, staging and local hosts are included; you have 14 days to do it.
OpenCart 4.1.0.4. Tested on OpenCart 4.1.0.4. The Compatibility tab lists every version range.
Do the extensions change core files?
No. They work through OpenCart events and standard extension types (modules, order totals, themes). No core file edits, no OCMOD patches, no theme template edits, so OpenCart updates and other extensions keep working.
What does the license cover?
One production store domain with every subdomain (www, shop., staging.), plus local development hosts such as localhost, *.local and *.test. A store on another domain needs its own license; a second-store license is 40% off. You have 14 days after installing to enter the key. 12 months of updates and support are included; after that the extension keeps working and renewals are 50% off. How licensing works
Can I get a refund?
Yes. Ask for a full refund within 30 days of your first purchase, no reason needed: open a ticket or write to [email protected] with your order number. The money goes back to the card or bank account you paid with within 5 business days of approval. Refund policy
How do I get help?
Open a ticket from your account, so your license details are attached. We answer with a same-day reply on weekdays (Mon-Fri 09:00-18:00 UTC+3). Support policy. Not a customer yet? Ask a pre-sales question.
Google, Google Merchant Center and Google Shopping are trademarks of Google LLC. This extension is not affiliated with or endorsed by Google.
Sends your OpenCart 4.1 catalogue to Google Merchant Center through the Merchant API (the successor of the Content API for Shopping, which reached its sunset on 2026-08-18 and is being shut down) and, as a fallback, serves a scheduled Google Shopping XML / TSV feed that follows Google's product data specification. Prices, stock and status changes reach Google within minutes, and Google's own issues are listed in your admin. OpenCart 4, events only, no OCMOD, no core file changed.
What it does
Merchant API sync. Products are sent as product inputs into an API data source per country and language (target): full sync first, then every price, stock, status and text change, in small batches, with safe retries (a lost answer never creates a copy). Disabled, excluded and deleted products are removed from Google.
Your own Google Cloud project. Sign in with OAuth 2.0 (with PKCE) or use a service account key. Secrets are stored encrypted and masked in the log. Google sends you back through a small page of the extension, so you stay signed in to your admin (OpenCart 4.1 keeps its session cookie strictly same-site).
Developer registration in one click. Every Cloud project must be registered once for the Merchant API; the Connection page does it and shows the state.
Targets. One target per country, language and currency (multi-currency and multi-language), with tax and shipping hints, a category filter and "exclude out of stock".
Mapping. GTIN (EAN, UPC, JAN, ISBN), MPN (the MPN code; SKU or model as an explicit choice), brand (manufacturer), condition, colour, size, gender and age group from product attributes, fields or fixed values; your categories mapped to the Google product taxonomy with a search box.
Per-product exclusions and overrides. Exclude a product or override its title, Google category, identifiers, condition, colour, size, gender and age group.
Sale prices with dates. OpenCart specials become salePrice with the effective interval, in your shop's time zone.
Variants. OpenCart 4 variant products share the master product's id as item_group_id.
Status and issues from Google. Approved, pending, partly approved and disapproved counts per target, and every item level issue with Google's description, severity, attribute and help link.
Fallback feed. XML and TSV files per target behind a secret address, rebuilt by cron in batches and moved into place only when complete.
Queue and cron guard. Jobs with retries and back-off (a 429 waits as long as Google asks), one run at a time, a cron address that refuses requests without its key.
Updates keep your data
Updating the extension (OpenCart 4.1 updates are uninstall plus install) keeps your targets, mapping, overrides, credentials, settings and licence. Uninstalling never deletes tables.
Pages
Version
1.0.0, OpenCart 4.1.0.0 - 4.1.x (tested on 4.1.0.4). There is no OpenCart 3 edition. Tested against a stand-in of the Merchant API built from Google's documentation, not against a live Merchant Center account.
Installation and setup
Requirements
OpenCart 4.1.0.0 - 4.1.x (tested on 4.1.0.4), PHP 8.1 or later, administrator access.
A Google Merchant Center account (a production account with a verified website; test accounts cannot register for the Merchant API) and a Google Cloud project of your own.
Your shop on https (Google sends you back to an https address).
The OpenCart cron (System > Maintenance > Cron Jobs runs when your host calls cron.php, for example every 5 minutes), or the web address shown on the overview page.
Extensions > Extensions > Modules: install Google Merchant Center Sync. Events, the cron job and the admin permissions are created.
Open Marketing > Google Merchant.
Connect (OAuth 2.0)
In Google Cloud console create or pick a project and enable the Merchant API.
APIs and services > OAuth consent screen: set it up (internal or external, add yourself as a test user while it is in testing).
Credentials > Create credentials > OAuth client ID > Web application. Under Authorized redirect URIs add the address shown on the Connection page (https://your-shop/extension/kv_google_merchant/return.php).
On the Connection page enter the Merchant Center ID, the client ID and the client secret, then click Authorize with Google and agree. You come back to the admin and the page shows the connection.
Click Register API access. Google needs about 5 minutes before calls work. Registration needs administrator access to the Merchant Center account.
Connect (service account)
In the same Cloud project create a service account and download its JSON key.
In Merchant Center, Settings > Access and services, add the service account's e-mail address as a user with administrator access.
On the Connection page choose Service account, paste the JSON key (it is stored encrypted and never shown again) and click Save and test.
Create a target
Targets > Add target: name, store, language, the two-letter content language, the feed label (usually the country code, for example US), the country, the currency, tax and shipping hints. Save and run a full sync. The first push creates the API data source for you.
Keep Google from receiving the same offer twice
Use the extension as the channel for a country and language, not next to another feed or app for the same one. If ANOTHER feed or app feeds the same country and language, set an offer ID prefix in the settings so the offer ids differ.
Configuration
Settings (Extensions > Modules > Google Merchant Center Sync, or the gear on the extension pages)
Setting
Default
Meaning
Enabled
on
When off nothing new is sent to Google and the feed addresses answer 404. Your data stays.
Push interval (minutes)
5
Changed products are sent in batches at most this often. A change is queued within seconds.
Status check (hours)
6
How often Google's status and issues are read back.
Feed rebuild (hours)
6
How often the XML and TSV files are rebuilt by cron.
Offer ID
Product ID
Product ID, SKU or model; fixed per product when it is first queued. A duplicate SKU or model falls back to the product ID.
Offer ID prefix
empty
Letters, digits and . _ - (up to 12), to keep this extension's offers apart from ANOTHER feed or app that feeds the same country and language. It does not separate this extension's own feed from its API sync (both use the same ids).
Back-ordered products
out of stock
The specification asks for an availability date with backorder and OpenCart has none; "send as backorder" sends them anyway.
Remove disabled products from Google
on
Disabled, excluded or filtered-out products are deleted from Google.
Storefront host
empty
Optional host for product links and image URLs.
SEO URLs in product links
off
Use the store's SEO keywords (SEO URLs must be on).
Log retention, debug log
90 days, off
Debug stores request and response bodies (secrets are masked).
License key
empty
Works for 14 days without a key; without a valid key the storefront features (feed, cron work) switch off, the admin pages never do.
Target fields
Field
Meaning
Store, store language
The OpenCart store and language the texts and links come from.
Content language, feed label, country
Google's identity of the offers: two-letter language, label of up to 20 capital letters, digits or dashes, target country. Language plus label is unique.
Currency
Prices are converted with the OpenCart currency values.
Tax in prices
None (as stored), the percentage tax class rates for the target country (rules based on the shipping address, only geo zones that cover the whole country (an "All Zones" row or every zone of the country listed), default customer group; rates of single states or provinces and fixed-amount rates are not added), or a fixed percent. For the US and Canada Google wants prices without sales tax, so a target in those countries always keeps None.
Shipping
None, flat price, or free from an amount. Products that need no shipping send 0. Sent as the shipping attribute of the target country.
Only these categories
Restricts the target to categories and their subcategories.
Exclude out of stock
Leaves products without stock out.
Mapping
Attributes. Per attribute (GTIN, MPN, brand, condition, colour, size, gender, age group): Auto, a product field (EAN, UPC, JAN, ISBN, MPN, SKU, model, manufacturer), a product attribute, a fixed value or none.
Google product category. Per OpenCart category the numeric id or the full path from the Google taxonomy (type to search). A subcategory uses its parent's entry when it has none.
Products page
Every product that is known to a target with its state, Google status and issue count. The sliders open the per-product form: exclude, title, Google category, GTIN, MPN, brand, condition, colour, size, gender, age group, "no unique identifier".
Usage
Daily work
You do nothing. Save a product, change a price or let an order move stock: the product is queued for every active target and sent a few minutes later; unchanged products cost no call. The overview shows per target how many products Google approved, left pending, partly approved or disapproved.
Buttons
Send queued now (per target) queues a push of everything waiting.
Full sync re-checks every product of the target (unchanged ones are skipped by their hash) and removes the ones that are no longer eligible.
Build now rebuilds the feed files of the target.
Run the queue now (overview, Jobs) works the queue once, like the cron.
Remove all from Google (target form) deletes every offer of the target at Google and pauses the target.
Google's issues
Issues lists every product that has item level issues, with Google's description, the severity (DISAPPROVED, DEMOTED, NOT_IMPACTED), the attribute, the countries and Google's "How to fix" link, and a summary of the most frequent issues. Status is read back every few hours; Read Google status now does it at once.
Feed in Merchant Center
In Merchant Center add a scheduled fetch feed and paste the XML or TSV address of the target from the overview (the address contains a secret). Use the feed only where you do not also send the same country and language through the API: an offer ID prefix does not separate them (both use the same ids). "New secret address" on the target form turns the old address off at once.
Cron
The extension registers an OpenCart cron job that runs every minute and decides what is due. The server must call OpenCart's cron.php (for example every 5 minutes). Without OpenCart's cron, call the web address shown on the overview from any cron service.
Uninstall
Extensions > Modules > Uninstall removes the events, the cron job and the permissions. Tables, settings and the licence stay, so a reinstall or update continues where it was. Products at Google stay until you use "Remove all from Google" first.
Developer notes
Technical reference for developers who customize Google Merchant Center Sync for OpenCart 4.1. Google, Google Merchant Center and Google Shopping are trademarks of Google LLC. This extension is not affiliated with or endorsed by Google.
Extension identity
Item
Value
Extension code
kv_google_merchant (OpenCart 4.1.0.0 - 4.1.x, tested on 4.1.0.4, PHP 8.1+)
Settings
module_kv_google_merchant_*
Admin pages
extension/kv_google_merchant/channel/merchant (menu: Marketing > Google Merchant)
extension/kv_google_merchant/module/kv_google_merchant.cron, .feed, .authorize (catalog controller) and extension/kv_google_merchant/return.php
PHP namespace of the shared core
KavioLabs\KvGoogleMerchant (system/library of the extension)
Tables
oc_kv_google_merchant_accounts, _targets, _product_map, _product_cfg, _attr_map, _category_map, _removals, _jobs, _log (plus the shared oc_kv_keep and oc_kv_seal)
No core file is changed and there is no OCMOD: everything runs through events and the OpenCart cron.
OpenCart 4 only.extension.json has no oc3 target, so kv package builds no OpenCart 3 zip; the oc3/ folder is only the scaffold tree that kv validate requires and is never packaged.
Offer ids. Default [prefix] + product id; with the source SKU or model, the code. Every id, including the fallback, is checked against the other map rows of the target; the fallback has its own shape (p<id>, then p<id>-2 ...), so a SKU of 123 and the product with the id 123 never meet. UNIQUE (target_id, offer_id) backs it in the database. The feed uses the id of the product's map row.
Removal. A row whose send was attempted (maybe_at, written before the call) is treated like a sent one: delete at Google (404 = done) before the row is forgotten. Remove all, target delete and an identity change take the same push-<target> lock as the push job.
Why this extension exists (official sources)
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 (the dates may change). The replacement is the Merchant API. Source: https://developers.google.com/shopping-content/guides/sunset (read 2026-11-07).
Verification status of every Google fact used
Each row says where it comes from and whether it was read on the official page. verified = read on the page named, on the date given. flagged = not confirmed on an official page: it is implemented as stated and must be checked before relying on it. Nothing here is guessed silently.
Merchant API (base https://merchantapi.googleapis.com)
OAuth scope for all calls: https://www.googleapis.com/auth/content.
Purpose
Call
Source
Status
Insert / replace an offer
POST /products/v1/accounts/{account}/productInputs:insert?dataSource=accounts/{account}/dataSources/{ds} (only API data sources)
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 extension sends the plain form only for parts made of A-Z a-z 0-9 . _ -
reference productInputs/delete
verified 2026-11-07
Same identity again = replace
insert with the same language, label, offer id and data source replaces the entry (a retry after a timeout cannot duplicate)
guide products/add-manage
verified 2026-11-07
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 2026-11-07
Read processed products
GET /products/v1/accounts/{account}/products (pageSize max 1000, pageToken; answer products[], nextPageToken) and .../products/{language}~{feedLabel}~{offerId}
POST /datasources/v1/accounts/{account}/dataSources, body DataSource with displayName and primaryProductDataSource {feedLabel, contentLanguage, countries[]}; DataSource.input is output only and "only generic data sources can be created through the API"
GET /datasources/v1/accounts/{account}/dataSources (pageSize max 1000, pageToken); used as the connection test and to adopt a data source after a timeout
reference datasources_v1/accounts.dataSources
verified 2026-11-07
Price
{amountMicros: "15990000", currencyCode: "USD"}; amountMicros is a string, 1 unit = 1,000,000 micros
reference products_v1/ProductAttributes (field names and types re-read 2026-11-08: salePriceEffectiveDate is a Shared.Types/Interval, identifierExists is a boolean "defaults to true", availabilityDate is an ISO 8601 timestamp, shippingWeight is {value: number, unit: string})
verified 2026-11-08
Sale price dates
salePriceEffectiveDate {startTime, endTime}: startTime "Inclusive start of the interval", endTime "Exclusive end of the interval", both "string in the protobuf Timestamp format". The extension sends RFC 3339 UTC (2026-11-10T23:00:00Z), the shop day boundaries converted from the shop time zone; both bounds are the shop-zone midnights of date_start and date_end exactly as stored: OpenCart applies a special while date_start < NOW() AND date_end > NOW() (catalog/model/catalog/product.php), so date_end is the first day WITHOUT the sale, which is also the exclusive end of Google's interval (fixed after the 2026-11-08 audit: it used to add a day)
reference Shared.Types/Interval (read 2026-11-08)
verified; the page links the protobuf Timestamp reference and does not name RFC 3339 itself: flagged (RFC 3339 is the protobuf JSON mapping)
Shipping object
shipping[] {price, country, region, service, locationId, locationGroupName, postalCode, loyaltyProgramLabel, loyaltyTierLabel, minHandlingTime, maxHandlingTime, minTransitTime, maxTransitTime, handlingCutoffTime, handlingCutoffTimezone}; only price and country are sent
The Merchant API ProductAttributes has no taxes field (re-read 2026-11-08, no match in the whole page). Prices are sent with VAT added by the target's "Tax in prices" setting when the market asks for it; US sales tax is configured in Merchant Center, not sent per product
reference products_v1/ProductAttributes
verified 2026-11-08 (intentional)
Title / description limits
150 / 5000 characters; offer id 50; mpn 70; brand 70; colour and size 100; item_group_id 50; product_type 750; up to 10 additional images; 2000 characters per URL
data specification support.google.com/merchants/answer/7052112
brand 70 and gtin lengths verified 2026-11-08; flagged: the title, description and id limits were read on 2026-11-07 for the CS-Cart add-on, the sections were not in the excerpt read on 2026-11-08
Authentication
OAuth 2.0 web server flow (https://accounts.google.com/o/oauth2/v2/auth, access_type=offline, prompt=consent, state, PKCE code_challenge S256; 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
The service account address is added as a user in Merchant Center (Settings, Access and services)
guide authorization/access-your-account
verified 2026-11-07
Developer registration
MANDATORY, once per Cloud project: POST /accounts/v1/accounts/{account}/developerRegistration:registerGcp, optional body {developerEmail}; unregistered projects get 401 UNAUTHENTICATED on every other call; calls work about 5 minutes after registering; production account with a verified homepage and ADMIN access, test accounts are not eligible. Status: GET /accounts/v1/accounts/{account}/developerRegistration
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
guide quotas-limits/quotas
verified 2026-11-07
User-Agent
KavioGoogleMerchantApiSync/1 on every call; no User-Agent or contact requirement was found in the guides read
guides read 2026-11-07 / 2026-11-08
flagged: not looked up as a dedicated rule
Batching
The Merchant API has no custom batch; requests are sent one by one (200 products per database batch, one HTTP call each); the push stops at the first 429 and the job retries later
guide compatibility/refactor-concurrent-requests
verified 2026-11-07
Notifications / push subscriptions, local and regional inventories, promotions, reviews, supplemental data sources
not used in 1.0 (status is polled every 6 hours by default)
-
not in scope
Google Shopping feed (data specification, support.google.com/merchants/answer/7052112, read 2026-11-08)
Fact
Used as
Status
Required: id, title, description, link, image_link, availability, price; brand required for new products (except movies, books, music); gtin strongly recommended; mpn required only when there is no manufacturer GTIN; availability_date required only for preorder
The file feed writes id, title, description, link, image_link, availability, price, condition always; entries without a title, link, image or price are left out and counted in the report
availability_date: ISO 8601 YYYY-MM-DDThh:mm[+hhmm] or ...Z, max 25 characters, up to one year ahead
2026-12-01T12:30+0100 (shop offset)
verified
sale_price_effective_date: ISO 8601, start and end separated by /, optional, start before end, max 51 characters; omitted = the sale price always applies
2026-11-10T00:00+0100/2026-11-17T00:00+0100; a special without an end date sends no range
verified
identifier_exists: yes / no, default yes; no when there is no GTIN, or no MPN plus brand; never no for products that have identifiers
Written as no only when there is no valid GTIN and (no brand or no MPN) or the product override says so
verified
item_group_id: required for variants in Brazil, France, Germany, Japan, UK, US and for free listings; max 50 alphanumeric characters; unique per group, stable over time
The OpenCart 4 master product id of every variant product
verified
additional_image_link: optional, max 2000 characters, up to 10 images by repeating the attribute
Up to 10 repeated elements (TSV: comma-separated)
verified (repeat form); the comma-separated TSV form is flagged: the text-file section was outside the excerpt read, it is the form of Google's published text feed examples
gtin: up to 14 digits per value, valid lengths 8, 12, 13, 14 (and 10 / 13 for ISBN), no dashes or spaces
The extension checks length and GS1 check digit before sending a GTIN
verified
shipping_weight: number + unit, units lb, oz, g, kg
Product weight with the unit of its OpenCart weight class when that is one of the four; other units are not sent
verified
shipping: country required (ISO 3166), price "Fixed shipping cost, including VAT if required", example US:CA:Overnight:16.00 USD:1:1:2:3
verified (example form); the TSV empty-region form is flagged
tax attribute (US only)
Not written
flagged: the page section of the tax attribute was not reachable; the Merchant API has no taxes field either; set US sales tax in Merchant Center
Google product taxonomy
src/resources/taxonomy-with-ids.en-US.txt, the file Google publishes at https://www.google.com/basepages/producttype/taxonomy-with-ids.en-US.txt; the numeric id is sent; the version in its first line is shown on the mapping page (2021-09-21 when this release was built)
verified 2026-11-08 (downloaded, same version)
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, PKCE helpers)
Mapper
Pure translation: ProductInput, price micros, GTIN check, sale interval, status and issues from products.list, data source body
Items
Product selection per target (store, categories, stock, per-product exclusion) and the normalized item (price in the target currency plus tax, availability, images, identifiers, variants) from plain SQL
Feed
The XML and TSV writers (batches of 200, temp file + atomic rename, per-target lock)
Links
Canonical product and image URLs from the saved store URL, always https
Mapping, Taxonomy
Attribute sources and Google product category per category; the bundled taxonomy search
Targets
Targets (= API data sources: language + feed label + country), duplicate guard, data source create / adopt, remove all, orphan removal queue
ProductMap
One row per target and product: offer id (fixed before the first call), state, payload hash, Google status, issues, a version counter
Sync, Jobs, Queue, JobException
Jobs push, full_sync, poll_status, cleanup, feed; back-off 5 min / 15 min / 1 h / 3 h / 12 h; 429 waits the time the API asks for
Accounts, Secret
Credentials sealed with AES-256-GCM, key per installation in oc_kv_seal
Keep
Settings and licence state kept across updates (oc_kv_keep); tables are never dropped by uninstall
Sync rules
Identity. Offer id = product id (or SKU / model, setting) stored in the map row BEFORE any call. The Merchant Center identity is contentLanguage~feedLabel~offerId plus 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 (under a lock). 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).
Events.admin/model/catalog/product.addProduct|editProduct|addVariant|editVariant|editVariants|deleteProduct/after, catalog/model/checkout/order.addHistory/after (stock moves of orders, also fired for admin order changes), admin/view/common/column_left/before (menu), admin/controller/common/header/before (licence check). A daily full check re-evaluates every product (unchanged ones cost no call) and catches changes made behind the events (imports, SQL, scheduled special prices).
Eligibility. Enabled products with an availability date that has come, assigned to the target's store, passing the category filter and "exclude out of stock", not excluded on the Products page. Products that leave the selection are deleted at Google when "Remove disabled products from Google" is on.
Errors. HTTP 400/404/409/422 from a PRODUCT call belong to that product (state E with the message, the run continues). A refused LOGIN (token endpoint 400 invalid_grant / invalid_client, 401) is an AuthException: 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 (deleted in Merchant Center) clears the stored data source, resets the rows and retries in a minute. 429 and 5xx / network stop the run and retry with back-off; quota/daily_limit_exceeded waits until 12:00 UTC.
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.
Time zone. Special-price dates are shop days: start = 00:00 of date_start, end = 00:00 of date_end (OpenCart's own rule is date_end > NOW(), so a special with date_end = today is already over and is not sent), both in the shop time zone (setting config_timezone), converted to UTC for the API and written with the shop offset in the feed. Cron buckets (daily check) use the shop day.
Text. OpenCart stores text HTML-encoded; every text that goes to Google is decoded first, cut by characters (mb_substr), never bytes.
Cron guard. The cron route accepts the OpenCart cron runner (five arguments) or the secret key compared with hash_equals; a web request without the key gets 403. One queue run at a time (GET_LOCK).
Variants. OpenCart 4 variant products (master id) are sent with itemGroupId = master id; colour and size come from the variant's option values when the option is named Color / Colour / Size, or from the Products page overrides. Option-based combinations of a single product are not expanded into separate offers in 1.0.
Identifiers. OpenCart 4.1 keeps SKU / UPC / EAN / JAN / ISBN / MPN in product_code; the auto mapping uses EAN, UPC, JAN, ISBN (first valid GTIN) for the GTIN and ONLY the MPN code for the MPN (SKU and model are shop codes, not manufacturer part numbers: they are explicit choices on the Mapping page). The product override "no GTIN / MPN" sends identifierExists = false without gtins and mpn on the API path and in the feed.
Test the HTTP layer without Google
The base URL can be overridden for tests with the setting module_kv_google_merchant_base_url (development only); the same address then also serves /token and /auth.
node tests/mock-server/server.js 11932 starts a stand-in that follows the reference (validates the ProductInput, replaces by identity, 404 on delete, paging, Google error bodies, OAuth with PKCE and JWT grants, developer registration); node tests/e2e.js runs the browser end-to-end test on a pool store and REFUSES to run unless the base URL points at 127.0.0.1 or localhost; php tests/unit.php runs the unit tests (fixtures in tests/fixtures, modeled on the official examples). No test reaches a real Google endpoint.
Troubleshooting
Every call answers 401 UNAUTHENTICATED. The Cloud project is probably not registered. Connection > Register API access, then wait about 5 minutes. A test Merchant Center account cannot register; use the production account.
"invalid_grant" / the job stops as dead. The authorization was revoked or expired. Authorize again on the Connection page. The queue keeps your changes and sends them afterwards.
403 PERMISSION_DENIED. The Google account or service account has no administrator access to the Merchant Center account (Settings > Access and services).
HTTP 429 / the push waits. Google's quota. The job waits as long as Google asks (a daily limit waits until 12:00 UTC) and continues; nothing is lost.
A product shows "no usable main image" or another error state. The product misses a title, image, link or price. Fix it and click the re-send button; the Products page shows the message.
The feed address answers 404. The extension is switched off, the licence is not valid, the target is paused, or the key in the address is old ("New secret address" turns old ones off).
The feed address answers 503 "being prepared". The first build is running or failed; check Jobs and the log, or click Build now.
Nothing is sent after a save. Check that OpenCart's cron runs (the overview shows the last run) and that the Jobs page shows no dead job. "Run the queue now" works the queue once.
Prices differ from the shop. Check the target's currency, the tax setting and that a special price with a future start date is sent with its start date (Google applies it then).
Redirect URI mismatch. The address on the Connection page must be added exactly (https, no trailing slash change) to the OAuth client's authorized redirect URIs.
Every action is in the log (Marketing > Google Merchant > Log); secrets are masked there.
FAQ
Is this the Merchant API or the old Content API?
The Merchant API. Google's Content API for Shopping reached its sunset on 2026-08-18 and is being shut down (requests fail intermittently from 2026-09-01, decommissioning is planned for early 2027); this extension never uses it.
Do I need my own Google Cloud project?
Yes. OAuth client or service account, both are yours; nothing runs through our servers. The extension only talks to Google's API hosts and, for the licence, to our licence server.
Why register the Cloud project?
Google requires it once per project; until then every call answers 401. The Connection page registers it with one click.
Can it replace my feed file?
Yes: use the API for a country and language, or the scheduled feed, not both for the same one. The offer ID prefix does NOT make them compatible: it is applied to the API and to this extension's own feed alike, so both would send the same identities. It only keeps this extension's offers apart from ANOTHER feed or app that feeds the same country and language.
Which products are sent?
Enabled products whose availability date has come, assigned to the target's store, in the target's categories, not excluded on the Products page.
How are variants handled?
OpenCart 4 variant products are sent with the master product's id as item_group_id. Colour and size come from the variant's Color / Size option or the product override. Option combinations of one product are not split into separate offers in 1.0.
What about taxes?
The Merchant API has no tax field. Choose "Tax in prices" per target: as stored, the percentage rates of the tax class for the target country, or a fixed percent. Rates that belong to single states or provinces are never added up into one figure; US and Canadian targets keep prices without sales tax, as Google asks, and US sales tax is set in Merchant Center.
GTIN missing?
The product is sent with identifierExists = false when it has no valid GTIN and no brand plus MPN, or when you tick "no GTIN / MPN" for the product (then no GTIN and no MPN are sent). The MPN is only the MPN code of the product: SKU and model are not used unless you choose them on the Mapping page. Invalid GTINs (wrong length or check digit) are never sent.
Does it work with multiple stores, languages and currencies?
Yes: one target per store, language and currency combination.
What happens to my offers when the extension is switched off or the licence is not valid?
Nothing is sent to Google while it is off, including removals: offers that are already there stay as they are (Google lets them expire after 30 days without a refresh) until the extension is active again. The admin pages stay open.
Does an update delete my settings?
No. Targets, mapping, overrides, credentials, settings and the licence are kept.
Is it tested against Google?
It is tested against a stand-in of the documented Merchant API (unit tests on PHP 8.1 and 8.3, browser end-to-end on OpenCart 4.1.0.4), not against a live Merchant Center account.
Server-side tracking for OpenCart 4.x and 3.0.x: purchases, refunds, add to cart, checkout, product views and leads go from your server to the free official GA4 Measurement Protocol, Meta Conversions API and TikTok Events API, deduplicated with the browser pixels through shared event ids. Consent-aware, SHA-256 hashed customer data, order status mapping, queue with retries, test mode and an event log. No paid relay, events only, no OCMOD.
The department store theme for OpenCart 4.x and 3.0.x: colour-coded departments, a department list beside the hero slider with two side promos, a big search with a department selector, tabbed product blocks, a 2 + 1 + 2 offer grid, deals of the day with real countdowns, a department showcase, a promotions band and a three-column product page with a sticky buy box. Four styles (Meridian, Harbor, Citrus, Night) and your own colours on top. Events only, no core changes.
Stop AI crawlers, scrapers, vulnerability scanners and unwanted countries before OpenCart 4.x or 3.0.x builds a page. Googlebot and Bingbot are verified by DNS and always allowed; 50+ bots get one switch each; country rules use free GeoIP databases and can close only the checkout; IP and network lists, rate limits, honeypots and the cart flood fix; a one-second challenge instead of a captcha; learning mode, dashboard, log, IP tester and Cloudflare export. No core changes, no OCMOD.