Implementing product export to Google Merchant Feed
Google Merchant Center accepts feeds in three formats: XML (Atom 1.0 / RSS 2.0), text TSV and API Content v2.1. For website-side automation, XML format or direct REST API integration is most predictable. Feed errors lead to product disapprovals and disabling shopping campaigns in Google Ads.
Formats and requirements
Google uses standard attributes documented in Product data specification. Required attributes for most countries:
| Attribute | Description | Example |
|---|---|---|
id |
Unique identifier | SKU-12345 |
title |
Name up to 150 chars | Samsung Galaxy S24 256GB Black |
description |
Up to 5000 chars | Detailed description |
link |
Full product URL | https://example.com/product/123 |
image_link |
Main image | https://cdn.example.com/img.jpg |
availability |
in_stock / out_of_stock / preorder |
in_stock |
price |
With currency | 29990 RUB |
brand |
Brand | Samsung |
gtin |
Barcode EAN/UPC (if exists) | 4895183805887 |
condition |
new / refurbished / used |
new |
XML feed structure
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:g="http://base.google.com/ns/1.0">
<channel>
<title>My store</title>
<link>https://example.com</link>
<description>Product feed</description>
<item>
<g:id>SKU-12345</g:id>
<g:title>Samsung Galaxy S24 256GB Black</g:title>
<g:description>Flagship Samsung smartphone...</g:description>
<g:link>https://example.com/product/12345</g:link>
<g:image_link>https://cdn.example.com/12345.jpg</g:image_link>
<g:additional_image_link>https://cdn.example.com/12345-2.jpg</g:additional_image_link>
<g:availability>in_stock</g:availability>
<g:price>29990 RUB</g:price>
<g:sale_price>24990 RUB</g:sale_price>
<g:brand>Samsung</g:brand>
<g:gtin>4895183805887</g:gtin>
<g:mpn>SM-S921BZKDSER</g:mpn>
<g:condition>new</g:condition>
<g:product_type>Electronics > Smartphones</g:product_type>
<g:google_product_category>267</g:google_product_category>
<g:color>Black</g:color>
<g:size>One Size</g:size>
<g:item_group_id>GALAXY-S24</g:item_group_id>
<g:shipping>
<g:country>RU</g:country>
<g:service>Courier delivery</g:service>
<g:price>350 RUB</g:price>
</g:shipping>
</item>
</channel>
</rss>
The item_group_id attribute is critical for products with variations — it groups variants (color, size) into one card in Shopping.
Google Product Category (GPC)
Google requires numeric code from official taxonomy. For online store need mapping table from internal categories to GPC codes:
class GoogleCategoryMapper
{
private array $mapping = [
'smartphones' => 267, // Electronics > Communications > Phones
'laptops' => 328, // Electronics > Computers > Laptops
'headphones' => 232, // Electronics > Audio > Headphones
'shoes-men' => 187, // Apparel > Shoes > Men
'refrigerators' => 4356, // Appliances > Kitchen Appliances > Refrigerators
];
public function map(Category $category): ?int
{
// Search by category slug or its ancestors
$slugs = $category->ancestors()->pluck('slug')->push($category->slug);
foreach ($slugs->reverse() as $slug) {
if (isset($this->mapping[$slug])) {
return $this->mapping[$slug];
}
}
return null; // Google allows missing GPC but reduces feed quality
}
}
Feed generator
class GoogleMerchantFeedBuilder
{
public function build(string $outputPath): void
{
$writer = new XMLWriter();
$writer->openUri($outputPath);
$writer->startDocument('1.0', 'UTF-8');
$writer->startElement('rss');
$writer->writeAttribute('version', '2.0');
$writer->writeAttribute('xmlns:g', 'http://base.google.com/ns/1.0');
$writer->startElement('channel');
$this->writeChannelMeta($writer);
Product::with(['category.ancestors', 'brand', 'images', 'variants'])
->active()
->hasPrice()
->chunk(500, function ($products) use ($writer) {
foreach ($products as $product) {
// If product has variants — export each
if ($product->variants->isNotEmpty()) {
foreach ($product->variants as $variant) {
$this->writeVariantItem($writer, $product, $variant);
}
} else {
$this->writeItem($writer, $product);
}
}
});
$writer->endElement(); // channel
$writer->endElement(); // rss
$writer->endDocument();
$writer->flush();
}
private function writeItem(XMLWriter $w, Product $p): void
{
$w->startElement('item');
$w->writeElement('g:id', $p->sku);
$w->writeElement('g:title', $this->sanitizeTitle($p->name));
$w->writeElement('g:description', strip_tags($p->description));
$w->writeElement('g:link', route('product.show', $p->slug));
$w->writeElement('g:image_link', $p->mainImage()?->url ?? '');
$w->writeElement('g:availability', $p->in_stock ? 'in_stock' : 'out_of_stock');
$w->writeElement('g:price', number_format($p->price, 2, '.', '') . ' RUB');
if ($p->sale_price) {
$w->writeElement('g:sale_price', number_format($p->sale_price, 2, '.', '') . ' RUB');
}
$w->endElement();
}
private function sanitizeTitle(string $title): string
{
// Google forbids all caps and promotional special chars
$title = preg_replace('/[!]{2,}/', '!', $title);
return mb_substr($title, 0, 150);
}
}
Upload via Content API v2.1
For large catalogs and instant price/stock updates API is preferred:
use Google\Service\ShoppingContent;
class GoogleContentApiUploader
{
private ShoppingContent $service;
private string $merchantId;
public function batchUpsert(Collection $products): void
{
$entries = $products->map(fn($p) => [
'batchId' => $p->id,
'merchantId' => $this->merchantId,
'method' => 'insert',
'product' => $this->toApiProduct($p),
])->values()->toArray();
// Maximum 1000 products per request
foreach (array_chunk($entries, 1000) as $batch) {
$this->service->products->custombatch([
'entries' => $batch,
]);
}
}
}
API integration allows updating price and stock in minutes without waiting for file re-crawl.
Disapproval diagnostics
Typical product rejection reasons:
-
Missing GTIN — for branded products Google requires GTIN; solution: fill field or set
identifier_exists: false - Promotional overlay on image — image with text/watermark; needs clean product photo
- Price mismatch — feed price differs from page price; need synchronization
- Unsupported language — description not in target country language
Log itemLevelIssues from API response for auto problem detection:
$response = $this->service->products->get($this->merchantId, "online:ru:RU:{$sku}");
foreach ($response->getItemLevelIssues() as $issue) {
Log::warning('GMC issue', [
'sku' => $sku,
'code' => $issue->getCode(),
'servability' => $issue->getServability(), // 'disapproved' | 'demoted'
'description' => $issue->getDescription(),
]);
}
Timeline
- XML feed generator with basic attributes: 1 day
- GPC mapping + variant attributes: +1 day
- Content API v2.1 integration: +1–2 days
- Disapproval logging and alerts: +0.5 day
Typical project without API integration: 2 working days.







