Tracking & Attribution

Why Do Your Dynamic Product Ads Show Zero Catalog Match?

A 0% catalog match is an ID mismatch, not a tracking outage. Here's why your events and your product feed disagree, and how to line them up.

Quick answer

A 0% catalog match almost never means your tracking is down. It means Meta is comparing the product IDs inside your events against the IDs inside your catalog and finding no overlap at all. The usual cause is grain: your feed is built at variant level — one row per size and colour — while your storefront pixel fires the parent product ID. Both sides are working; they are describing the same item with two different identifiers, so the join returns nothing. Fix the ID space before you touch anything else.

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

A 0% catalog match means Meta compared the product IDs in your events against the IDs in your catalog and found no overlap. Your events are arriving. Your catalog is live. They simply do not speak about products using the same identifier, so the join that powers dynamic product ads returns nothing at all.

That is a reassuring diagnosis and an annoying one. Nothing is offline. But until the two sides agree on what a product ID is, your catalog ads cannot retarget anyone with the item they looked at.

What Does a 0% Catalog Match Actually Mean?

It means the share of your catalog-relevant events whose product IDs Meta could find in your catalog is zero. The join is a plain string comparison: the values in an event’s content_ids against the retailer IDs of the items in your catalog. No fuzzy matching, no fallback to product name.

This is why the number is so often exactly 0% rather than a disappointing 14%. A partial mismatch shows as a partial rate. An exact zero means every event points at an ID space the catalog has never heard of — a systematic difference, not data-quality drift.

What you lose downstream is specific. Dynamic product ads and Advantage+ catalog ads build their creative by looking up the item a person viewed or added to cart. With no match, there is no item to show, so the campaign serves broad creative, under-delivers, or sits in learning without ever personalising.

The tell is healthy event volume beside a dead match rate. If your events were genuinely missing, both numbers would fall together.

Split dashboard mockup showing an event payload with a product-level content_ids value on the left, variant-level catalog rows on the right, and a failed join with a 0% match rate between them

Why Do Your Events and Your Catalog Disagree About What a Product Is?

Because every store has at least two IDs for the same thing, and the two systems pick different ones. A product is the thing with a name and a page. A variant is the thing with a price, a stock level and a barcode — a specific size in a specific colour. Ad platforms transact at variant grain, so a well-built feed has one row per variant. Your storefront pixel fires from the product page, and that page’s most obvious identifier is the parent product. Nobody chose this mismatch; it falls out of where each piece of code sits.

Make it concrete. “Merino Crew” is one product with twelve variants. Your feed contains twelve rows, each with its own ID, all sharing an item_group_id that points back at the parent. A visitor opens the product page and ViewContent fires with content_ids: ["7412887654321"] — the parent product ID. That value appears in the item_group_id field of twelve rows and in the ID field of none of them. Match rate: zero.

Different events on the same store can therefore match at different rates — Purchase, built from order data, often knows the variant; ViewContent, fired from theme code, often does not.

Which ID Does Each Side Actually Use?

Where Typical identifier What it describes
Catalog feed, ID field Variant ID or variant SKU One sellable item: size, colour, its own price and stock
Catalog feed, item_group_id Parent product ID The group the variant belongs to
Pixel ViewContent on a product page Parent product ID, often The page, before a variant is chosen
Pixel AddToCart / InitiateCheckout Variant ID, usually The item actually added
Server-side Purchase from order data Variant ID or SKU, per line item What was bought

What Else Drives the Match Rate to Zero?

Grain is the most common cause, not the only one. Work down this list before you rewrite any code.

  • Namespaced versus raw IDs. Some sales-channel apps write something like shopify_US_7412887654321_41998877665544 into the catalog while your events send the raw number.
  • SKU on one side, platform ID on the other. Both are legitimate identifiers; they are not the same string.
  • The wrong content_type. If you deliberately send parent IDs, declare content_type: "product_group" so the platform looks in the group field instead of the ID field. Parent IDs sent as "product" is the silent version of this bug.
  • The catalog is not linked to the pixel or the ad set. The IDs can line up perfectly while the campaign points at a different catalog.
  • Items that no longer render. Feeds normally publish active items only. Archive a variant and its ID leaves the catalog while old events still reference it.
  • Type and whitespace noise. A numeric ID on one side and a quoted string with a stray space on the other will not match, and nothing tells you why.

Checking whether your feed will be rejected is a different exercise from checking whether it matches your events. A feed can be perfectly approved and share no IDs with your pixel.

How Do You Diagnose This in Ten Minutes?

Take one event and one feed file and compare them by hand. The failure is a string comparison, and you can do a string comparison yourself.

  1. In Events Manager, open a recent ViewContent and copy its content_ids value exactly — including quotes, prefixes and leading zeros.
  2. Open the actual feed file your platform fetches, not the store admin. Search for that exact string.
  3. Read the result. Found in the ID field means the problem is elsewhere: catalog linkage, content_type, or the ad set. Found only inside item_group_id is the grain problem. Not found at all means two different ID spaces — usually a prefix or a SKU-versus-ID split.
  4. Repeat for AddToCart and Purchase. They are often fired by different code and fail differently.
// The event Meta received
{
  "event_name": "ViewContent",
  "custom_data": {
    "content_type": "product",
    "content_ids": ["7412887654321"],   // parent product ID
    "value": 129.00,
    "currency": "AUD"
  }
}

// The catalog row that should have matched
{
  "id": "41998877665544",              // variant ID — the join key
  "item_group_id": "7412887654321",    // parent product ID lives here
  "title": "Merino Crew — Navy / M",
  "price": "129.00 AUD",
  "availability": "in stock"
}

Side by side, the bug is obvious: the event’s ID is present in the catalog, in the wrong field. That is the whole outage.

How Do You Fix It Without Changing Your Whole Stack?

Pick the canonical ID space first, then make everything follow it. Variant is almost always right, because price, availability and identifiers live at variant level, and those are the fields platforms rank and disapprove on.

Then change the events rather than the feed where you have the choice. Rewriting the ID field of an existing catalog effectively replaces those items, resetting their history and sending campaigns that were learning on them back to the start. Changing what your events send costs you nothing historical.

Where the page genuinely does not know the variant yet — a product page before anyone picks a size — send the parent ID and declare content_type: "product_group". That parameter exists for this case; it is a supported match path, not a workaround.

Use the same ID on every event in the funnel, or you will fix the match rate on ViewContent and leave it broken on AddToCart. If you send events through the Conversions API as well as the browser pixel, both paths need the same value.

Two honest expectations. The match rate recovers going forward, not backwards. And the catalog only changes when the platform fetches your feed, so give it a fetch cycle before deciding the fix failed.

How Does PartialLeads Solve This?

PartialLeads owns one side of the comparison outright: it builds the catalog at variant grain, so the ID space your feed publishes is knowable and stable rather than whatever an app decided.

The catalog table holds one row per variant, upserted on the variant ID, and any variant not seen in a completed sync run is swept out. That is the grain the ad platforms transact at, which removes the most common source of the mismatch from the feed side. The feed renders as RSS 2.0 with Google’s g: namespace at a single tokenised URL that both Meta Commerce Manager and Google Merchant Center ingest — one ID space to reconcile against, not two.

The Product Catalog page in the main nav shows what you are publishing: variants, products, stock, price range, top product types. A variant count sitting well above the product count is the fastest confirmation that your feed is at variant grain. Beside it is a 0–100 feed-health score built from six presence checks — missing image, missing identifier, out of stock, no GTIN, no product type, no description — with separate readiness counts per destination, because Meta needs an image and Google needs an image plus an identifier.

Only active variants render into the feed, which explains the disappearing-ID case above: archive a variant and it leaves the catalog while events still reference it.

Then the proof that the two halves line up. PartialLeads joins your order line items back to catalog items over a rolling 90-day window and shows which items sold and which never did. If the items that sell appear against catalog rows, the catalog’s IDs and your real order data are in the same space — the property a match rate is measuring.

On the events side, the Shopify path fires the full ecommerce funnel server-side rather than Purchase alone — ViewContent, AddToCart, Search, InitiateCheckout and AddPaymentInfo — so the events catalog ads personalise on exist even when a browser blocks the pixel. Each carries a deterministic event_id with a unique constraint behind it, so a server event and a browser event describing the same action cannot double-count. Getting purchase value and currency right matters for the same reason the IDs do.

Three constraints, plainly. The feed-health panel is Shopify-only — WooCommerce stores get the feed and the sync without the scoring. The checks test whether fields are present, not whether their values are valid, so the score is a first pass, not a Merchant Center pre-flight. And PartialLeads produces the feed; connecting it to a catalog and auditing what ID your storefront’s own pixel sends stay in your hands — that last one is the ten-minute job above.

Catalog sync is also optional and decoupled from tracking. It stays inert until you paste a Shopify Admin API token, so you can run server-side conversions and attribute ecommerce revenue to the ad that started it without ever connecting the catalog side. There is a full walkthrough of Shopify tracking, attribution and catalog, and a narrower guide to fixing Meta CAPI tracking on Shopify if events are the immediate problem.

Dashboard mockup of the PartialLeads Product Catalog page showing variant and product composition counts, a feed-health score gauge, Meta-ready and Google-ready tiles and the six issue checks

Numbers in the mockups are placeholder data.

What breaks The mechanism Where you see it in the dashboard
Events name the parent product, catalog holds variants Catalog built at variant grain, one row per variant, upserted on variant ID Product Catalog, variant and product composition counts
Two feeds with two ID spaces for Meta and Google One RSS 2.0 feed URL with the g: namespace, ingested by both platforms The hosted feed URL, one per connection
IDs that were valid last week and match nothing today Active variants only, with unseen rows swept on each completed run Product Catalog, in-stock and composition counts
No way to prove catalog IDs match real orders Order line items joined to catalog items over a rolling 90-day window Product Catalog, top products and not-selling
Catalog ads starved of the events they personalise on Full ecommerce funnel fired server-side, deterministic event ID with unique dedup CAPI activity log, per-event status

Tell us what's broken. We'll fix your tracking — free.

Describe the tracking/attribution problem you're stuck on and we'll map it to a fix: server-side conversions to Meta, Google, TikTok and Pinterest, plus first-party tracking that survives Safari. No code required.

Sources


Frequently asked questions

QDoes a 0% catalog match mean my pixel is broken?
Usually the opposite. A broken pixel shows as falling event volume; a 0% match shows as healthy event volume that matches nothing. The events are arriving and being recorded — they just carry product IDs the catalog does not contain. Check event counts and match rate side by side. If events look normal and the match rate is zero, you have an identifier problem, not a delivery problem.
QShould content_ids be the product ID or the variant ID?
The variant ID, in almost every case, because that is the grain a well-built feed uses and the grain price and availability live at. The exception is a page where no variant has been chosen yet — a product page before a size is selected. There you can send the parent ID, but you must also set content_type to product_group so the platform looks in the group field rather than the ID field.
QWhat is item_group_id, and why does it not fix my match rate?
It is the field that ties a variant back to its parent product so the platform knows the twelve rows are twelve versions of one item. It is not the join key. Unless you explicitly declare that your events carry group IDs, the platform compares your event IDs against the catalog's ID field only, so a parent ID sitting correctly in item_group_id still produces no match.
QWhy does my Purchase event match but my ViewContent does not?
Because they are usually fired by different code paths. Purchase is typically built from order data, which knows the exact variant bought. ViewContent is often fired from theme code on the product page, which knows only the product being viewed. That split produces exactly this pattern, and it is one of the clearest signals that you are looking at an ID grain problem rather than a tracking failure.
QShould I change my feed IDs or my event IDs?
Change the events where you have the choice. Rewriting the ID field of an existing catalog effectively replaces those items on the platform, which resets their history and sends campaigns that were learning on them back to the start. Changing what your events send carries no such cost. Only change feed IDs when the feed is genuinely at the wrong grain and needs rebuilding anyway.
QHow long until the match rate recovers after I fix it?
It recovers as new events arrive, not retroactively — events that already failed to match stay unmatched. You also need the platform to fetch the feed again before any catalog-side change counts, so give it at least one scheduled fetch cycle plus a day of normal traffic before judging the result. Judging the fix within an hour will tell you nothing.
QDoes PartialLeads rewrite the product IDs my storefront pixel sends?
PartialLeads builds the catalog side at variant grain and fires the ecommerce funnel server-side, so the IDs in your feed are stable and knowable. What your storefront's own pixel puts in content_ids is decided by your theme and whatever apps are installed, and auditing that is a manual ten-minute check against the feed. Fix that once and the join holds.
QDoes catalog sync work with WooCommerce?
WooCommerce catalog sync ships and shares the same variant-grain table and feed output as Shopify, so Woo stores get a working feed at the right grain. The feed-health scoring panel is Shopify-only, though, so WooCommerce stores get the feed without the readiness counts and issue checks. Worth knowing before you plan a workflow around the score.

Find the qualified leads your forms are currently throwing away.

Install PartialLeads on one landing page, send traffic, and compare what your CRM captured against what PartialLeads recovered and qualified.