Meta Catalog

Layer 2 destination model for the Meta (Facebook/Instagram) product catalog. Thin projection from product_catalogue_canonical with Meta feed field names, custom_label product sets, and Shopify-native merch dedup.

product_catalogue_meta projects product_catalogue_canonical into the Meta product catalog feed format. It is the metadata counterpart to the Facebook CAPI conversion model: the catalogue id here is the same value the CAPI event sends as content_ids.

Meta is the reference implementation — TikTok and Reddit feeds are close variants of this field set.

How the canonical catalogue maps to Meta

Layer 1 column Meta feed field Notes
item_id id Must match content_ids on the pixel/CAPI event
title title ≤ 200 chars
description description
availability availability in stock / out of stock / preorder
condition condition new / refurbished / used
price price 19.99 USD format (amount + ISO currency)
link link Product/landing URL
image_link image_link ≥ 500×500
brand brand
custom_label_0 custom_label_0 = group_id — the product-set key
custom_label_1..4 custom_label_1..4 optional segmentation

Template

{{ config(materialized='view', tags=['meta', 'catalogue', 'outputs']) }}

SELECT
    item_id                       as id,
    title,
    description,
    availability,
    condition,
    -- Meta wants "<amount> <currency>" as a single string
    CAST(price AS VARCHAR) || ' ' || currency as price,
    link,
    image_link,
    brand,
    custom_label_0,
    custom_label_1,
    custom_label_2,
    custom_label_3,
    custom_label_4
FROM {{ ref('product_catalogue_canonical') }}
-- Merch already syncs Shopify -> Meta natively; don't double-list it.
WHERE item_type <> 'merch'

Design notes

idcontent_ids

Meta matches a catalogue row to an event by comparing the feed id to the event's content_ids. The CAPI model (marketing_conversions_facebook) must emit the same value. Because both derive from the canonical item_id, they agree by construction — keep it that way and never let the two pipelines compute the id differently.

Product sets ride on custom_label_0

Targeting in Meta uses product sets (a saved filter on catalogue fields), never a raw pixel parameter. A per-group product set is a single filter, custom_label_0 = '<group_id>'. Because the group is itself a catalogue row (id = group_id), that set unites the group row and all its child rows — enabling group-level DPA (e.g. retarget everyone who engaged with a title, then show them anything tied to that title).

Shopify-native merch dedup

If merch already syncs Shopify → Meta natively, those products are already catalogue rows. Exclude them from this feed (WHERE item_type <> 'merch') so you don't create duplicates. Use this model for the rows Shopify doesn't cover (tickets, VOD, trailers, titles/groups). Confirm the native sync's id matches your canonical item_id, or the conversion join breaks for merch.

product_name on events is not used here

Meta renders DPA creative from the catalogue row, never from an event's product name field. Nothing downstream should depend on the event's name — the catalogue is the source of creative truth.

Coverage

Meta drops unmatched content_ids silently. Enforce the coverage check against marketing_conversions_canonical so every id that can appear on an event exists here before go-live.

Transport

Push the Layer 2 table to the Catalog Batch API via reverse ETL, or publish it as a scheduled hosted feed. Refresh ahead of new items becoming purchasable.

Reference