Upgrade your app for discount rollouts
Merchants can include a discount in a rollout to coordinate a campaign launch, test the discount with a share of buyers, or schedule a temporary event alongside other store changes.
A rollout has its own schedule. While it's serving, a treatment can add the discount for the buyers it reaches, or remove it for them. A serving rollout doesn't change the discount's own start and end dates, or its configuration.
Version 2026-10 of the GraphQL Admin API adds a read-only rollouts connection to discounts. Your existing queries keep working. What changes is that a discount's own fields don't tell you what a rollout is doing to it.
Anchor to What's changingWhat's changing
A discount in a rollout is returned like any other discount. Its status still comes from its own startsAt and endsAt, so while a rollout is serving, status and what buyers get can differ:
| Treatment behavior | Discount status | What buyers get |
|---|---|---|
| A treatment activates the discount | SCHEDULED or EXPIRED | The buyers that treatment reaches get it |
| Treatments expire it for every buyer | ACTIVE | No buyer gets it |
| A treatment expires it for some buyers | ACTIVE | The buyers that treatment reaches don't get it |
A treatment that makes no discount change leaves its buyers on the discount's own dates. Read the rollouts connection when you need a discount's effective availability or reach.
Only a serving rollout changes what buyers get. While a rollout is draft, scheduled, paused, concluded, or archived, the discount's status is accurate.
Only a serving rollout changes what buyers get. While a rollout is draft, scheduled, paused, concluded, or archived, the discount's status is accurate.
Anchor to Apps on earlier API versionsApps on earlier API versions
Earlier versions can't represent a rollout, so they leave out the discounts that the three cases above apply to. Every other discount in a rollout is returned as usual, so being in a rollout doesn't make a discount unreadable.
An excluded discount is missing from discountNode lookups by ID, from the discountNodes, codeDiscountNodes, automaticDiscountNodes, and automaticDiscounts connections, from discountNodesCount, and from the discount connections and counts on Market. Nothing in the response says why, and those versions have no rollouts connection to explain it. The discount returns when the rollout stops affecting it.
Upgrading to 2026-10 removes the exclusion. The discount stays readable, and the rollouts connection tells you what the rollout is doing to it.
Anchor to What's not changingWhat's not changing
Nothing in the schema is removed, renamed, or retyped. A serving rollout doesn't change a discount's startsAt, endsAt, or configuration, or the behavior of the status filters on the discount connections.
Anchor to Who needs to take actionWho needs to take action
Find your app's behavior below, and follow the path for each one that applies:
| If your app... | Action required |
|---|---|
| Caches, mirrors, or syncs discounts | Re-read a discount when its rollout starts or stops serving. Its own fields don't change while the rollout serves, so a stale copy still looks current. Refer to Keep your app in sync. |
| Shows discounts to merchants on its own surface | Show rollout information next to the discount, because status alone can contradict what buyers get. Refer to Show rollout information. |
| Promotes a discount to buyers, such as a sales channel or a marketing surface | Only promote a discount that reaches every buyer. One in a serving rollout might not. Refer to If your app promotes discounts. |
Acts on a discount's status | Check for a serving rollout before you trust status. Refer to Find the rollouts on a discount. |
| Reports on discount performance | Read the rollout's effective traffic allocation first. A discount in a rollout can reach only part of the store, so crediting it with store-wide results overstates it. Refer to Read the traffic split. |
| Deletes local records when a read stops returning a discount | Before 2026-10, a rollout can take a discount out of your reads. Treat an absent discount as unavailable, not deleted. Refer to Keep your app in sync. |
| Only writes discounts | No changes required. |
Anchor to Show rollout informationShow rollout information
A discount can belong to a rollout that hasn't started yet, or to one that has already concluded. Where a merchant benefits from seeing that, such as a discount list or a performance report, show the rollout's name, status, and traffic allocation next to the discount.
Where your surface tells a buyer what they get, such as a sales channel or a storefront display, don't fall back on the discount's status: a serving rollout can activate a discount whose own dates say it's over, or expire one whose dates say it's live. Refer to If your app promotes discounts.
Anchor to Find the rollouts on a discountFind the rollouts on a discount
The rollouts connection lists the rollouts that a discount belongs to, and their scheduled start and end times. Each of these types implements the HasRollouts interface, so you can write the selection once as a fragment and reuse it. Query it on:
DiscountAutomaticAppDiscountAutomaticBasicDiscountAutomaticBxgyDiscountAutomaticFreeShippingDiscountCodeAppDiscountCodeBasicDiscountCodeBxgyDiscountCodeFreeShipping
GraphQL query
Anchor to Add the ,[object Object], access scopeAdd the read_rollouts access scope
read_rollouts access scopeThis path requires the read_rollouts access scope, on top of the read_discounts scope that your app already has. Refer to Manage access scopes. A merchant has to grant the new scope, so existing installations might need to be reauthorized.
Add the rollouts selection behind a check for the scope, rather than into a query that your app already depends on. Without the scope, the request returns an ACCESS_DENIED error, and because the fields in that path are non-null, the error propagates up to discountNode, which comes back as null. A missing scope takes the discount fields with it.
Anchor to Filter the connectionFilter the connection
Unfiltered, rollouts isn't limited to the rollouts serving right now: it also returns drafts and concluded ones. Use query to filter by status, as in status:ACTIVE,SCHEDULED. For the values, refer to RolloutStatus. A comma-separated list is an OR, so status:ACTIVE,SCHEDULED and status:ACTIVE OR status:SCHEDULED return the same rollouts, and either form combines with AND, OR, and NOT.
Anchor to Page through the resultsPage through the results
rollouts is a connection, and how many rollouts it returns can change, so don't read the first node and stop.
Request the page size that suits your app, then read pageInfo.hasNextPage and fetch the next page with pageInfo.endCursor for as long as it's true. With one rollout, hasNextPage is false and you make no second request. For the pattern, refer to Paginating results with GraphQL.
Anchor to Read a rollout's scheduleRead a rollout's schedule
Read status to tell whether a rollout is serving now, and check it against schedule.concludeAt: for a short window after the conclude time has passed, status can still read ACTIVE for a rollout that has stopped applying its changes. To schedule your own refresh for a rollout that hasn't started yet, read schedule, which holds the planned activateAt and concludeAt and is null for a rollout with no planned dates. Those are the planned dates, separate from startedAt and concludedAt, which record what actually happened. For every field on a rollout, refer to Rollout.
Anchor to Tell an activation from an expirationTell an activation from an expiration
A rollout includes a discount through a change on one of its treatments. Each treatment lists the changes it makes, and each change has a type. Read a change's __typename:
RolloutDiscountActivateChange: The treatment activates the discount for the buyers it reaches, whatever the discount's ownstartsAt,endsAt, orstatus.RolloutDiscountExpireChange: The treatment expires the discount for the buyers it reaches.
Not every treatment carries a discount change, and a treatment can change resources other than discounts, such as catalogs, themes, and checkout and accounts configuration. Filter the changes to the discount you're reading, and check the type of what comes back:
GraphQL query
Anchor to Build the change filterBuild the change filter
Build $changeFilter in your own code. discount_id takes a numeric ID unquoted, as in discount_id:1234567890, or a GID in quotes, as in discount_id:'gid://shopify/DiscountNode/1234567890'. type takes a change typename, such as type:RolloutDiscountActivateChange; it's case-sensitive and can be quoted or not.
To return only the changes that affect the discount you're reading:
Combine terms with AND, OR, or whitespace, which reads as AND, and group them with parentheses. To narrow that to one kind of change:
Negation, ranges, and wildcards aren't supported. A malformed expression returns an error rather than an unfiltered list.
A discount_id filter matches only discounts your app can read and that are available, so an inaccessible discount returns no changes rather than an error. A type filter has no such restriction and keeps the retained identity of a change whose resource your app can't read, so use type when you need to see that a change exists at all.
A discount_id filter matches only discounts your app can read and that are available, so an inaccessible discount returns no changes rather than an error. A type filter has no such restriction and keeps the retained identity of a change whose resource your app can't read, so use type when you need to see that a change exists at all.
Anchor to Read the traffic splitRead the traffic split
A Rollout carries two percentages, and the difference matters. trafficAllocation is what the merchant configured, and split on each RolloutTreatment divides it between the treatments. effectiveTrafficAllocation is the share of new buyer assignments the rollout actually receives once Shopify resolves conflicts with other active rollouts on the same resources, so it can be lower.
Neither is a measured share of buyers, and neither accounts for buyer eligibility or how a discount applies in a particular market. Use them to tell a merchant that a discount is partial, not to calculate exact reach. To decide whether a discount reaches every buyer, refer to If your app promotes discounts.
Anchor to If your app promotes discountsIf your app promotes discounts
If your app tells buyers that a discount is available, only promote one that every buyer can use. A discount in a serving rollout can reach part of the store, and nothing on the discount itself says so.
A discount is in at most one serving rollout at a time. When it's in one, treat the discount as safe to promote only when both of these hold. These conditions are about the rollout, so the discount's own eligibility rules still decide whether a given buyer can use it:
- The rollout's
effectiveTrafficAllocationis100. Read the effective allocation, nottrafficAllocation: a rollout configured at 100% can still come out lower once Shopify resolves conflicts with other active rollouts. - One treatment activates the discount, and that treatment's
splitis100.
Those two conditions are the simple safe case, and the allocation alone isn't enough to establish it: a rollout running a control against a treatment can sit at 100 and still hand the discount to one arm's buyers only.
They also assume a rollout that changes discounts and nothing else. effectiveTrafficAllocation covers everything a rollout changes, not the discount on its own, so where showing a discount to a buyer who can't use it is costly, treat these conditions as a strong signal rather than a guarantee.
Beyond that case, work out what each arm does, because a buyer is assigned to exactly one treatment. An arm that expires the discount denies it to its buyers. An arm that makes no change leaves them on the discount's own dates, so they still get it if the discount is active in its own right.
When you can't see the whole picture, treat the discount as restricted rather than assuming. That includes hasNextPage being true on rollouts or on a treatment's changes, and a change whose resource your app can't read.
Anchor to Keep your app in syncKeep your app in sync
On 2026-10, a discount in a rollout stays readable, and while the rollout is serving, none of the discount's own fields change. A cached copy looks current after what buyers get has changed.
Anchor to Subscribe to rollout webhooksSubscribe to rollout webhooks
Version 2026-10 adds six rollout topics. Subscribe to them to hear about a change as it happens:
| Topic | Occurs when |
|---|---|
rollouts/create | A rollout is created, including its initial treatments and attached changes. |
rollouts/update | A rollout changes lifecycle status, or its configuration, treatments, or attached changes are updated. A rollout also receives this topic when a competing rollout changes its effective traffic allocation, even though nothing about the rollout itself changed. |
rollouts/delete | A rollout is deleted. Archiving a rollout produces rollouts/update instead. |
rollouts/resource_change_added | A change is added to a treatment. This doesn't mean the resource was created or activated. |
rollouts/resource_change_removed | A change is removed from a treatment. Other changes can still reference the same resource. |
rollouts/resource_change_updated | A change in a treatment is updated, which today means it targets a different resource. The change ID stays the same. |
These topics need the read_rollouts access scope, and only an app can subscribe to them: they don't appear in a store's notification settings. To subscribe, refer to Subscribe to webhook topics.
Treat a payload as a notification rather than a source of truth. Every payload identifies the rollout with admin_graphql_api_id, and there are three shapes behind that:
rollouts/createandrollouts/updatecarry the rollout's own fields, such as its name, status, schedule, and both its configured and effective traffic allocation.- A
resource_change_*payload carries identifiers only: the treatment, the change, the resource it points at, andoccurred_at. It has no rollout fields, so don't decode it with the reader you use for a rollout snapshot. rollouts/deletecarriesadmin_graphql_api_idanddeleted_at.
No payload carries the rollout's treatments or its changes. Re-read the discount's rollouts connection to see what the rollout now does to the discount.
Anchor to Reconcile as wellReconcile as well
A delivery can be missed, so don't let a webhook be the only thing that updates your copy. Reconcile periodically. If you don't subscribe, use the rollout's schedule instead: query rollouts with status:ACTIVE,SCHEDULED and refresh at schedule.activateAt and schedule.concludeAt. A merchant can also pause or conclude a rollout early, which the authored schedule won't tell you. Effective traffic allocation is worked out against the other rollouts running at the time, so don't assume a stored copy is still current when you haven't processed a recent rollouts/update.
Never delete a local record because a discount stopped appearing in a read. An absent discount isn't a deleted one.
On an API version earlier than 2026-10, you can't tell the two apart: a rollout takes the discount out of your reads, and nothing in the response says so. Upgrading to 2026-10 is the fix, because the discount stays readable and the rollouts connection tells you what the rollout is doing to it. Until you upgrade, keep the local record and re-read later instead of deleting it.
Anchor to Test your integrationTest your integration
- Create a dev store and install your app on it.
- Set your app's API version to
2026-10. - Create a discount, then put it in a rollout from the Shopify admin. Configure the rollout to serve part of the store's traffic.
- Read the discount and confirm that your app handles the
rolloutsconnection and each treatment's changes. - Activate the rollout with a discount whose own dates make it scheduled or expired, then confirm that your app still reports the discount as reaching buyers.
- Pause the rollout, then conclude it. Confirm that your app re-reads the discount instead of trusting a cached response.