To manage where a Shopify product appears, you work with publications — one per sales channel — and two mutations: publishablePublish to make a product live on a channel and publishableUnpublish to remove it. Channel publication state is separate from product status: an ACTIVE product still won't show in a channel's storefront or feed unless it's published there. This guide covers listing a product's resourcePublications, publishing and unpublishing across the Online Store and other channels, and why this is the first thing to check when a product goes missing from a feed.
This is the publication-control deep-dive that the Shopify Product Catalog API guide points to. It covers sales-channel publication only — B2B and Markets catalogs with their own price lists are a separate model, covered in B2B and Markets catalogs. Everything here uses the GraphQL Admin API at version 2026-01; publication management ships to GraphQL and REST is legacy for new work (Shopify: API versioning).
The Publication object
A Publication represents a product's relationship to a single sales channel — Online Store, Point of Sale, Shop, the Google & YouTube channel, or a third-party channel app. Each channel a store has installed owns exactly one publication. Publishing a product to a channel creates a publication record linking the two; unpublishing removes it. The publishable mutations operate on any resource that implements the Publishable interface — products and collections — so the same calls work across catalog resource types (Shopify: Publishable interface).
The key mental model: status answers "is this product sellable at all?"; publication answers "is this product live on this specific channel?" Both must be true for the product to appear. This distinction is the root of most "my product is active but not showing" tickets.
| Concept | Field / mutation | Question it answers |
|---|---|---|
| Product status | Product.status (ACTIVE / DRAFT / ARCHIVED) | Is the product sellable anywhere? |
| Publication | Publication (one per channel) | Which channels can carry it? |
| Channel state | resourcePublications → isPublished | Is it live on a given channel now? |
| Make live | publishablePublish | Publish to one or more channels |
| Remove | publishableUnpublish | Unpublish from one or more channels |
List a product's current publications
Before you change anything, read the product's resourcePublications connection. Each node gives you the publication (with its name and id) plus an isPublished flag and the publishDate, so you see both where the product is live and where it merely exists as an unpublished draft (Shopify: resourcePublications).
query ProductPublications($id: ID!) {
product(id: $id) {
id
title
status
resourcePublications(first: 25) {
nodes {
isPublished
publishDate
publication {
id
name
}
}
}
}
}
To enumerate every channel the store has — not just the ones this product touches — query the top-level publications connection. You'll need those publication IDs to publish to a channel the product isn't on yet.
query AllChannels {
publications(first: 25) {
nodes {
id
name
}
}
}
Cache this channel list; the IDs are stable per store, and you'll reference them in every publish call. If you only need a yes/no answer for a single channel, the narrower publishedOnPublication(publicationId:) field on the product returns a boolean without pulling the whole connection — cheaper when you're checking one channel across many products.
One caveat on access: your app can only publish to, unpublish from, or read publications for channels it has permission for. A private integration querying publications sees the channels its scopes allow, so if a channel you expect is missing from the list, check the app's granted access before assuming the store lacks that channel.
Publish to the Online Store and other channels
publishablePublish takes the resource id and an input array of publication targets. Pass one entry per channel you want the product live on — you can publish to the Online Store and a marketplace channel in the same call.
mutation PublishProduct($id: ID!, $input: [PublicationInput!]!) {
publishablePublish(id: $id, input: $input) {
publishable {
publishedOnCurrentPublication
resourcePublications(first: 25) {
nodes {
isPublished
publication { name }
}
}
}
userErrors { field message }
}
}
// Publish one product to the Online Store + the Google channel
const input = [
{ publicationId: "gid://shopify/Publication/1234567" }, // Online Store
{ publicationId: "gid://shopify/Publication/7654321" }, // Google & YouTube
];
const res = await fetch(`https://${shop}/admin/api/2026-01/graphql.json`, {
method: "POST",
headers: {
"X-Shopify-Access-Token": token,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: PUBLISH_PRODUCT,
variables: { id: productId, input },
}),
});
const json = await res.json();
const errors = json.data.publishablePublish.userErrors;
if (errors.length) throw new Error(JSON.stringify(errors));
Always read userErrors — a publish can fail silently at the HTTP layer (200 OK) while reporting a channel-level problem, such as trying to publish a DRAFT product to a channel that requires ACTIVE. The mutation does not change status; it only creates the publication link (Shopify: publishablePublish).
publishablePublishToCurrentChannel
If your code runs inside a sales-channel app, you often don't need to know other channels' IDs — you just want the product live on your channel. publishablePublishToCurrentChannel publishes to the publication tied to the requesting app, no publication ID required.
mutation PublishHere($id: ID!) {
publishablePublishToCurrentChannel(id: $id) {
publishable { publishedOnCurrentPublication }
userErrors { field message }
}
}
Use publishablePublish when you're orchestrating across channels from a central integration, and publishablePublishToCurrentChannel when a channel app self-publishes (Shopify: publishablePublishToCurrentChannel).
Unpublish from a channel
publishableUnpublish mirrors the publish call: same id, same input array of publication targets. It removes the product from those channels only. It does not delete the product, change its status, or touch channels you didn't name.
mutation UnpublishProduct($id: ID!, $input: [PublicationInput!]!) {
publishableUnpublish(id: $id, input: $input) {
publishable {
resourcePublications(first: 25) {
nodes {
isPublished
publication { name }
}
}
}
userErrors { field message }
}
}
To pull a product from every channel — for a seasonal delisting, say — read its resourcePublications, collect the publication IDs where isPublished is true, and pass them all to publishableUnpublish. To remove it from the catalog entirely, archive or delete it instead; those are separate operations from channel publication.
Why channel publication decides whether a product appears in a feed
This is the payoff. A sales-channel feed — an Online Store export, a Google Merchant feed, a marketplace sync — reflects the products published to that channel, not merely the products that are ACTIVE. A common failure: a product is set ACTIVE, everyone assumes it's live, but it was never published to the channel (or was auto-unpublished by a bulk edit or an app), so it silently never enters the feed.
When you build feed generation with the ProductFeed API, the channel's publication set is the source list. And because publication state changes independently of the product record, you want to react to it in near-real time — subscribe to catalog change webhooks so a publish/unpublish event triggers a feed refresh instead of waiting for a nightly full sync.
For debugging, the diagnostic order is always: check status first, then check resourcePublications for the channel in question. If status is ACTIVE but the channel's isPublished is false (or the publication is absent entirely), you've found the missing-product cause — one publishablePublish call fixes it.
This is also where publication hygiene meets ad performance: products that aren't published to your Google or Meta channel simply can't be advertised through it, no matter how good the campaign. Auditing publication coverage across channels is part of the catalog work AdsX does for Shopify brands, because a product that isn't in the feed is a product the ads can't sell.
Next steps
- Building the feed those publications feed into? See generate feeds with the ProductFeed API.
- Reacting to publish/unpublish events in real time? See catalog change webhooks.
- Selling to B2B or across Markets with per-catalog price lists? That's a separate model — see B2B and Markets catalogs.
- Full surface overview: the Shopify Product Catalog API guide.