Contextual product feeds allow [sales channels](/docs/apps/build/sales-channels) to sync merchant catalogs to their platforms. You can configure distinct feeds for the different language and country pairs that a merchant supports. ## What you'll learn In this tutorial, you'll learn how to do the following tasks: - Identify a merchant's supported markets - Create product feeds for specific country/language contexts - Subscribe to product feed webhooks - Initiate a full product sync ## Requirements - Your app has the `read_product_listings` [access scope](/docs/api/usage/access-scopes). Learn more about requesting access scopes when your app is installed using [authorization code grant](/docs/apps/build/authentication-authorization/access-tokens/authorization-code-grant). ## Step 1: Identify the merchant's supported markets Query the merchant's [markets](/docs/api/admin-graphql/latest/queries/markets) to obtain localization details, including each country's supported languages and currencies. > Note: > Definitions for "primary region" might differ for each channel. Shopify exposes the following fields to help channels identify which regions and markets the merchant is based in: > - The merchant's [primary market](/docs/api/admin-graphql/latest/objects/Market#field-market-primary). > - The `country` of the merchant's [primary fulfillment location](/docs/api/admin-graphql/latest/queries/location)'s [`address`](/docs/api/admin-graphql/2024-07/objects/Location#field-address). > - The merchant's [default currency](/docs/api/admin-graphql/latest/objects/Shop#field-shop-currencycode). The following example shows how to retrieve language, currency and country details from a shop's markets. [Inline fragments](/docs/apps/build/graphql/basics/advanced#inline-fragments) are used to simply the GraphQL query. ```graphql { markets(first: 10, enabled: true) { edges { node { name primary enabled currencySettings { baseCurrency { currencyCode enabled } localCurrencies } webPresence { subfolderSuffix domain { host } defaultLocale { ...LocaleFields } alternateLocales { ...LocaleFields } } regions(first: 20) { edges { node { id name ... on MarketRegionCountry { code } } } } } } } } fragment LocaleFields on ShopLocale { locale name primary published } ``` ```json { "data": { "markets": { "edges": [ { "node": { "name": "Canada", "primary": true, "enabled": true, "currencySettings": { "baseCurrency": { "currencyCode": "CAD", "enabled": false }, "localCurrencies": true }, "webPresence": { "subfolderSuffix": null, "domain": { "host": "my-store.myshopify.com" }, "defaultLocale": { "locale": "en", "name": "English", "primary": true, "published": true }, "alternateLocales": [ { "locale": "fr", "name": "French", "primary": false, "published": true } ] }, "regions": { "edges": [ { "node": { "id": "gid://shopify/MarketRegionCountry/144848879672", "name": "Canada", "code": "CA" } } ] } } }, { "node": { "name": "USA", "primary": false, "enabled": true, "currencySettings": { "baseCurrency": { "currencyCode": "USD", "enabled": false }, "localCurrencies": false }, "webPresence": { "subfolderSuffix": "us", "domain": null, "defaultLocale": { "locale": "en", "name": "English", "primary": true, "published": true }, "alternateLocales": [] }, "regions": { "edges": [ { "node": { "id": "gid://shopify/MarketRegionCountry/145364320312", "name": "United States", "code": "US" } } ] } } } ] } } } ``` ### Subscribe to market updates Market configurations might change after [onboarding](/docs/apps/build/sales-channels#sales-channel-app-flow), to which your app may need to respond. Stay updated on any market changes by using the [`webhookSubscriptionCreate` mutation](/docs/api/admin-graphql/latest/mutations/webhookSubscriptionCreate) to subscribe to the [`MARKETS_CREATE`](/docs/api/admin-graphql/latest/enums/WebhookSubscriptionTopic#value-marketscreate), [`MARKETS_DELETE`](/docs/api/admin-graphql/latest/enums/WebhookSubscriptionTopic#value-marketsdelete) and [`MARKETS_UPDATE`](/docs/api/admin-graphql/latest/enums/WebhookSubscriptionTopic#value-marketsupdate) webhook topics. Events will be triggered whenever key changes occur, such as when: - A new market is created. - An existing market is updated. - An existing market is deleted. ## Step 2: Create product feeds Use the [`productFeedCreate` mutation](/docs/api/admin-graphql/latest/mutations/productFeedCreate) to create a `ProductFeed` for each country/language pair that both the channel and the merchant support. Save the `productFeed`'s `id` value. You'll use it it initiate and identify webhook payloads in a [subsequent step](#step-4-initiate-a-full-sync) when you synchronize products. The following example shows how to create a product feed for the `US-EN` context. ```graphql mutation { productFeedCreate(input: {country: US, language: EN}) { productFeed { id country language } userErrors { field message } } } ``` ```json { "data": { "productFeedCreate": { "productFeed": { "id": "gid://shopify/ProductFeed/1343510", "country": "US", "language": "EN" }, "userErrors": [] } } } ``` ### (Optional): Use the primary context only If your app only requires access to the merchant's default localization, then use the [`productFeedCreate` mutation](/docs/api/admin-graphql/latest/mutations/productFeedCreate#argument-input) without its `input` argument. This creates a feed in the merchant's primary context, similar to how the [`ProductListings`](/docs/api/admin-rest/latest/resources/productlisting) reference on the REST Admin API behaves. ## Step 3: Subscribe to product feed webhooks Subscribe to product feed webhooks to receive initial [full sync payloads](#step-4-initiate-a-full-sync) and [async events](#subscribe-to-incremental-sync-events) whenever products are updated. ### Subscribe to full sync events Full sync events are triggered when a product feed full sync is initiated. Full sync events contain individual product payloads and allow you to sync a merchant's entire catalog during channel setup. Use the [`webhookSubscriptionCreate` mutation](/docs/api/admin-graphql/latest/mutations/webhookSubscriptionCreate) to subscribe to the [`PRODUCT_FEEDS_FULL_SYNC`](/docs/api/admin-graphql/latest/enums/WebhookSubscriptionTopic#value-productfeedsfullsync) webhook topic. ```graphql mutation { webhookSubscriptionCreate( topic: PRODUCT_FEEDS_FULL_SYNC webhookSubscription: { callbackUrl: "{your-callback-url}" format: JSON } ) { webhookSubscription { id topic endpoint { __typename ... on WebhookHttpEndpoint { callbackUrl } } createdAt } userErrors { field message } } } ``` Individual product listing payloads are sent when a [full sync is initiated](#step-4-initiate-a-full-sync). ```json { "metadata": { "action": "CREATE", "type": "FULL", "resource": "PRODUCT", "fullSyncId": "gid://shopify/ProductFullSync/1123511235", "truncatedFields": [ ], "occurred_at": "2024-01-01T00:00:00.000Z" }, "productFeed": { "id": "gid://shopify/ProductFeed/12345", "shop_id": "gid://shopify/Shop/12345", "country": "CA", "language": "EN" }, "product": { "id": "gid://shopify/Product/12345", "title": "Coffee", "description": "The best coffee in the world", "onlineStoreUrl": "https://example.com/products/coffee", "createdAt": "2024-12-31T19:00:00-05:00", "updatedAt": "2024-12-31T19:00:00-05:00", "isPublished": true, "publishedAt": "2024-12-31T19:00:00-05:00", "productType": "Coffee", "vendor": "Cawfee Inc", "handle": "", "images": { "edges": [ { "node": { "id": "gid://shopify/ProductImage/394", "url": "https://cdn.shopify.com/s/files/1/0262/9117/5446/products/IMG_0022.jpg?v=1675101331", "height": 3024, "width": 4032 } } ] }, "options": [ { "name": "Title", "values": [ "151cm", "155cm", "158cm" ] } ], "seo": { "title": "seo title", "description": "seo description" }, "tags": [ "tag1", "tag2" ], "variants": { "edges": [ { "node": { "id": "gid://shopify/ProductVariant/1", "title": "151cm", "price": { "amount": "100.00", "currencyCode": "CAD" }, "compareAtPrice": null, "sku": "12345", "barcode": null, "quantityAvailable": 10, "availableForSale": true, "weight": 2.3, "weightUnit": "KILOGRAMS", "requireShipping": true, "inventoryPolicy": "DENY", "createdAt": "2024-12-31T19:00:00-05:00", "updatedAt": "2024-12-31T19:00:00-05:00", "image": { "id": "gid://shopify/ProductImage/394", "url": "https://cdn.shopify.com/s/files/1/0262/9117/5446/products/IMG_0022.jpg?v=1675101331", "height": 3024, "width": 4032 }, "selectedOptions": [ { "name": "Title", "value": "151cm" } ] } } ] } }, "products": null } ``` ### (Optional): Subscribe to full sync completion events You can subscribe to the [`PRODUCT_FEEDS_FULL_SYNC_FINISH`](/docs/api/admin-graphql/latest/enums/WebhookSubscriptionTopic#value-productfeedsfullsyncfinish) webhook topic to be notified whenever a full sync finishes. This is a useful signal for scenarios where the merchant hasn't published any products to the channel. ```json { "metadata": { "action": "CREATE", "type": "FULL", "resource": "PRODUCT", "fullSyncId": "gid://shopify/ProductFullSync/1123511235", "truncatedFields": [ ], "occurred_at": "2024-01-01T00:00:00.000Z" }, "productFeed": { "id": "gid://shopify/ProductFeed/12345", "shop_id": "gid://shopify/Shop/12345", "country": "CA", "language": "EN" }, "fullSync": { "createdAt": "2024-12-31 19:00:00 -0500", "errorCode": null, "status": "completed", "count": 12, "url": null } } ``` ### Subscribe to incremental sync events Incremental sync events are triggered whenever changes occur relative to app's feeds, such as when: - Product fields are updated. - Product variant fields are added, updated, or deleted. - Product translations are updated. - Product market price is updated. - Products are published to the app. - Products are unpublished from app. Use the [`webhookSubscriptionCreate` mutation](/docs/api/admin-graphql/latest/mutations/webhookSubscriptionCreate) to subscribe to the [`PRODUCT_FEEDS_INCREMENTAL_SYNC`](/docs/api/admin-graphql/latest/enums/WebhookSubscriptionTopic#value-productfeedsincrementalsync) webhook topic. ```graphql mutation { webhookSubscriptionCreate( topic: PRODUCT_FEEDS_INCREMENTAL_SYNC webhookSubscription: { callbackUrl: "{your-callback-url}" format: JSON } ) { webhookSubscription { id topic endpoint { __typename ... on WebhookHttpEndpoint { callbackUrl } } createdAt } userErrors { field message } } } ``` ```json { "metadata": { "action": "CREATE", "type": "INCREMENTAL", "resource": "PRODUCT", "truncatedFields": [ ], "occured_at": "2024-12-31T19:00:00-05:00" }, "productFeed": { "id": "gid://shopify/ProductFeed/12345", "shop_id": "gid://shopify/Shop/12345", "country": "CA", "language": "EN" }, "product": { "id": "gid://shopify/Product/12345", "title": "Coffee", "description": "The best coffee in the world", "onlineStoreUrl": "https://example.com/products/coffee", "createdAt": "2024-12-31T19:00:00-05:00", "updatedAt": "2024-12-31T19:00:00-05:00", "isPublished": true, "publishedAt": "2024-12-31T19:00:00-05:00", "productType": "Coffee", "vendor": "Cawfee Inc", "handle": "", "images": { "edges": [ { "node": { "id": "gid://shopify/ProductImage/394", "url": "https://cdn.shopify.com/s/files/1/0262/9117/5446/products/IMG_0022.jpg?v=1675101331", "height": 3024, "width": 4032 } } ] }, "options": [ { "name": "Title", "values": [ "151cm", "155cm", "158cm" ] } ], "seo": { "title": "seo title", "description": "seo description" }, "tags": [ "tag1", "tag2" ], "variants": { "edges": [ { "node": { "id": "gid://shopify/ProductVariant/1", "title": "151cm", "price": { "amount": "100.00", "currencyCode": "CAD" }, "compareAtPrice": null, "sku": "12345", "barcode": null, "quantityAvailable": 10, "availableForSale": true, "weight": 2.3, "weightUnit": "KILOGRAMS", "requireShipping": true, "inventoryPolicy": "DENY", "createdAt": "2024-12-31T19:00:00-05:00", "updatedAt": "2024-12-31T19:00:00-05:00", "image": { "id": "gid://shopify/ProductImage/394", "url": "https://cdn.shopify.com/s/files/1/0262/9117/5446/products/IMG_0022.jpg?v=1675101331", "height": 3024, "width": 4032 }, "selectedOptions": [ { "name": "Title", "value": "151cm" } ] } } ] } }, "products": null } ``` ### Subscribe to product feed updates Product feeds might change as the merchant modifies their market settings. For example, if a merchant un-publishes a language, then any dependant feeds will become inactive. Subscribe to the [`PRODUCT_FEEDS_UPDATE`](/docs/api/admin-graphql/latest/enums/WebhookSubscriptionTopic#value-productfeedsupdate) webhook topic to stay updated on [feed `statuses`](/docs/api/admin-graphql/latest/objects/ProductFeed#field-status). ```json { "id": "gid://shopify/ProductFeed/1285521464", "country": "CA", "language": "FR", "status": "inactive" } ``` ## Step 4: Initiate a full sync After all relevant feeds are created and your app has subscribed to the necessary webhook topics, you can initiate full syncs for each feed using the [`productFullSync`](/docs/api/admin-graphql/latest/mutations/productFullSync). The [`id`](/docs/api/admin-graphql/latest/mutations/productFullSync#field-productfullsyncpayload-id) returned can be used to attribute the webhooks and identify the sync completion event. Your app should trigger a full sync once during initialization to fetch the store's entire catalog. You can trigger additional full syncs on a regular schedule for [reconciliation](/docs/apps/build/webhooks/best-practices#implement-reconciliation-jobs). When a full sync is initiated, individual [`PRODUCT_FEEDS_FULL_SYNC` webhook payloads](#subscribe-to-full-sync-events) are fired for each of the merchant's published products. The webhook payloads contain localized data according the feed's context. A confirmation event is sent to the [`PRODUCT_FEEDS_FULL_SYNC_FINISH` subscription](#optional-subscribe-to-full-sync-completion-events) once the full sync has completed. The payload's `fullSyncId` is the feed's identifier. An initial full sync should be triggered during app onboarding, and regular reconciliation syncs should be scheduled after. The optional `beforeUpdatedAt` and `updatedAtSince` parameters allow you to specify a time range for the sync. ```graphql mutation { productFullSync(id: "gid://shopify/ProductFeed/1164017720") { id userErrors { field message } } } ``` ```json { "data": { "productFullSync": { "id": "gid://shopify/ProductFullSync/2523324778944713391", "userErrors": [] } } } ``` ### (Optional):Full Sync by download An alternative solution is to download a single [`JSONL` file](https://jsonlines.org/) containing all products that would normally be sent through individual [`PRODUCT_FEEDS_FULL_SYNC`](#subscribe-to-full-sync-events) payloads. This can be useful in scenarios where the amount of webhook traffic is difficult to manage. When full sync by download is enabled, the `PRODUCT_FEEDS_FULL_SYNC_FINISH` webhook payload contains a `JSONL` file URL. This functionality is similar to [bulk operations](/docs/api/usage/bulk-operations/queries). ```json { "metadata": { "action": "CREATE", "type": "FULL", "resource": "FULL_SYNC", "fullSyncId": "gid://shopify/ProductFullSync/23104717021717166290741", "truncatedFields": [] }, "productFeed": { "id": "gid://shopify/ProductFeed/2310471702", "shop_id": "gid://shopify/Shop/59810447382", "country": "US", "language": "EN" }, "fullSync": { "createdAt": "2024-05-31T14:38:10Z", "errorCode": "", "status": "completed", "count": 16, "url": "https://storage.googleapis.com/shopify-tiers-assets-prod-us-east1/bulk-operation-outputs/dyfkl3g72empyyoenvmtidlm9o4g?bulk-4258087174166.jsonl&response-content-type=application%2Fjsonl" } } ``` ## Next steps - For other channel product sync solutions, refer to the [unidirectional product synchronization](/docs/apps/build/sales-channels/product-sync) guide.