Discounts
Discount pages display information about price reductions and promotional offers, including discount codes, automatic discounts, and usage limits. Extensions on these pages help merchants manage promotions and customize discount workflows.
Anchor to Use casesUse cases
- Sync discounts externally: Push discount codes and rules to external marketing platforms, POS systems, or affiliate networks.
- Track promotion performance: Display analytics and conversion metrics from external tracking systems alongside discount details.
- Bulk discount management: Enable merchants to update, duplicate, or archive multiple discounts at once from the index page.
- Validate discount rules: Check discount configurations against business rules or inventory levels before activation.
- Generate unique codes: Create batches of unique discount codes for influencer campaigns or customer loyalty programs.

Anchor to Discount details targetsDiscount details targets
Use action targets to extend the discount details page with workflows. Action targets open as modal overlays from the More actions menu.
The examples demonstrate fetching data from Shopify's direct API or your app's backend.
Anchor to Discount details action ,[object Object]Discount details action target
admin.discount-details.action.render
Renders an admin action extension on the discounts details page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on discounts data, such as syncing with external systems, exporting discounts information, or managing credit terms.
Extensions at this target can access discount data through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.
Supported components
- Admin action
- Avatar
- Badge
- Banner
- Box
- Button
- Button group
- Checkbox
- Chip
- Choice list
- Clickable
- Clickable chip
- Color field
- Color picker
- Date field
- Date picker
- Divider
- Drop zone
- Email field
- Grid
- Heading
- Icon
- Image
- Link
- Menu
- Money field
- Number field
- Ordered list
- Paragraph
- Password field
- Query container
- Search field
- Section
- Select
- Spinner
- Stack
- Switch
- Table
- Text
- Text area
- Text field
- Thumbnail
- Tooltip
- Url field
- Unordered list
Available APIs
Supported components
- Admin action
- Avatar
- Badge
- Banner
- Box
- Button
- Button group
- Checkbox
- Chip
- Choice list
- Clickable
- Clickable chip
- Color field
- Color picker
- Date field
- Date picker
- Divider
- Drop zone
- Email field
- Grid
- Heading
- Icon
- Image
- Link
- Menu
- Money field
- Number field
- Ordered list
- Paragraph
- Password field
- Query container
- Search field
- Section
- Select
- Spinner
- Stack
- Switch
- Table
- Text
- Text area
- Text field
- Thumbnail
- Tooltip
- Url field
- Unordered list
Available APIs
jsx
Examples
Description
Add an action extension that syncs the current discount to an external POS system. This example demonstrates calling an app backend API to push discount details to point-of-sale terminals.
jsx
import {render} from 'preact'; import {useState} from 'preact/hooks'; export default async () => { render(<Extension />, document.body); }; const Extension = () => { const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(null); const [selectedLocations, setSelectedLocations] = useState({ mainStore: true, warehouse: false, popup: false, }); const handleSync = async () => { setLoading(true); setError(null); const discountId = shopify.data.selected[0].id; const locations = Object.entries(selectedLocations) .filter(([_, enabled]) => enabled) .map(([loc]) => loc); try { const response = await fetch('https://your-app.com/api/pos/sync-discount', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({discountId, locations}), }); if (response.ok) { setSuccess(true); shopify.close(); } else { const data = await response.json(); setError(data.message || 'Failed to sync discount'); } } catch (err) { setError('Connection error. Please try again.'); } finally { setLoading(false); } }; const handleLocationChange = (location) => (event) => { setSelectedLocations(prev => ({ ...prev, [location]: event.currentTarget.checked, })); }; const anySelected = Object.values(selectedLocations).some(Boolean); return ( <s-admin-action heading="Sync Discount to POS"> <s-stack direction="block" gap="base"> {success && ( <s-banner tone="success" dismissible={false}> Discount synced to POS terminals successfully! </s-banner> )} {error && ( <s-banner tone="critical" dismissible={false}> {error} </s-banner> )} <s-section heading="Select POS Locations"> <s-stack direction="block" gap="small"> <s-checkbox label="Main Store" checked={selectedLocations.mainStore} onChange={handleLocationChange('mainStore')} /> <s-checkbox label="Warehouse Terminal" checked={selectedLocations.warehouse} onChange={handleLocationChange('warehouse')} /> <s-checkbox label="Pop-up Location" checked={selectedLocations.popup} onChange={handleLocationChange('popup')} /> </s-stack> </s-section> <s-text color="subdued"> The discount will be available at selected POS terminals within 5 minutes. </s-text> <s-button-group> <s-button variant="primary" onClick={handleSync} disabled={loading || success || !anySelected} > {loading ? 'Syncing...' : 'Sync to POS'} </s-button> <s-button onClick={() => shopify.close()}>Cancel</s-button> </s-button-group> </s-stack> </s-admin-action> ); };Description
Add an action extension that displays performance analytics for the current discount by fetching usage data from the GraphQL Admin API.
jsx
import {render} from 'preact'; import {useState, useEffect} from 'preact/hooks'; export default async () => { render(<Extension />, document.body); }; const Extension = () => { const [loading, setLoading] = useState(true); const [analytics, setAnalytics] = useState(null); const [error, setError] = useState(false); useEffect(() => { fetchAnalytics(); }, []); const fetchAnalytics = async () => { const discountId = shopify.data.selected[0].id; try { const response = await fetch('shopify:admin/api/graphql.json', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ query: `query GetDiscount($id: ID!) { discountNode(id: $id) { id discount { ... on DiscountCodeBasic { title usageLimit asyncUsageCount startsAt endsAt } ... on DiscountCodeFreeShipping { title usageLimit asyncUsageCount startsAt endsAt } } } }`, variables: {id: discountId}, }), }); const {data} = await response.json(); const discount = data?.discountNode?.discount; if (discount) { const usageRate = discount.usageLimit ? Math.round((discount.asyncUsageCount / discount.usageLimit) * 100) : null; setAnalytics({...discount, usageRate}); } } catch (err) { setError(true); } finally { setLoading(false); } }; if (loading) { return ( <s-admin-action heading="Discount Analytics"> <s-stack direction="block" gap="base"> <s-spinner /> <s-text>Loading analytics...</s-text> </s-stack> </s-admin-action> ); } return ( <s-admin-action heading="Discount Analytics"> <s-stack direction="block" gap="base"> {error && ( <s-banner tone="critical" dismissible={false}> Failed to load discount analytics. </s-banner> )} {analytics && ( <s-section heading={analytics.title}> <s-stack direction="block" gap="small"> <s-stack direction="inline" gap="base"> <s-text color="subdued">Total Redemptions:</s-text> <s-badge tone="success">{analytics.asyncUsageCount}</s-badge> </s-stack> <s-stack direction="inline" gap="base"> <s-text color="subdued">Usage Limit:</s-text> <s-text>{analytics.usageLimit || 'Unlimited'}</s-text> </s-stack> {analytics.usageRate !== null && ( <s-stack direction="inline" gap="base"> <s-text color="subdued">Usage Rate:</s-text> <s-badge tone={analytics.usageRate > 80 ? 'warning' : 'info'}> {analytics.usageRate}% </s-badge> </s-stack> )} <s-divider /> <s-text color="subdued"> Active: {new Date(analytics.startsAt).toLocaleDateString()} - {analytics.endsAt ? new Date(analytics.endsAt).toLocaleDateString() : 'No end date'} </s-text> </s-stack> </s-section> )} <s-button-group> <s-button onClick={() => shopify.close()}>Close</s-button> </s-button-group> </s-stack> </s-admin-action> ); };
Anchor to Discount details action (should render) ,[object Object]Discount details action (should render) target
admin.discount-details.action.should-render
Controls the render state of an admin action extension on the discounts details page. Use this target to conditionally show or hide your action extension based on the discount's properties, such as status, configuration, or specific business requirements.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
jsx
Examples
Description
Show the analytics action only when the app backend confirms the discount has sufficient redemption data and analytics available for display.
jsx
export default async () => { const discountId = shopify.data.selected[0].id; try { const response = await fetch('https://your-app.com/api/discount-analytics/check', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ discountId, checkType: 'performance-data-available', }), }); if (!response.ok) { return { display: false }; } const result = await response.json(); // Show action if discount has been tracked and has redemption data const hasPerformanceData = result.isTracked && result.redemptionCount > 0; return { display: hasPerformanceData }; } catch (err) { console.error('Failed to check discount performance eligibility:', err); return { display: false }; } };Description
Display the action only when the discount has specific products or collections assigned, querying the GraphQL Admin API to check the discount's product eligibility configuration.
jsx
export default async () => { const discountId = shopify.data.selected[0].id; try { const response = await fetch('shopify:admin/api/graphql.json', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: `query GetDiscountEligibility($id: ID!) { discountNode(id: $id) { discount { ... on DiscountCodeBasic { customerGets { items { ... on DiscountProducts { productVariantsCount { count } collectionsCount { count } } } } } ... on DiscountAutomaticBasic { customerGets { items { ... on DiscountProducts { productVariantsCount { count } collectionsCount { count } } } } } } } }`, variables: { id: discountId }, }), }); const { data } = await response.json(); const discount = data?.discountNode?.discount; const items = discount?.customerGets?.items; // Show action if discount targets specific products or collections const hasProducts = items?.productVariantsCount?.count > 0; const hasCollections = items?.collectionsCount?.count > 0; return { display: hasProducts || hasCollections }; } catch (err) { console.error('Error checking discount eligibility:', err); return { display: false }; } };
Anchor to Discount index targetsDiscount index targets
Use action targets to extend the discount index page with bulk operations and workflows that help merchants manage multiple discounts efficiently.
Anchor to Discount index action ,[object Object]Discount index action target
admin.discount-index.action.render
Renders an admin action extension on the discounts index page. Merchants can access this extension from the More actions menu. Use this target to provide workflows that operate on discounts data, such as syncing with external systems, exporting discounts information, or managing credit terms.
Extensions at this target can access discount data through the data property in the Action Extension API. The action renders in a modal overlay, providing space for multi-step workflows, forms, and confirmations.
Supported components
- Admin action
- Avatar
- Badge
- Banner
- Box
- Button
- Button group
- Checkbox
- Chip
- Choice list
- Clickable
- Clickable chip
- Color field
- Color picker
- Date field
- Date picker
- Divider
- Drop zone
- Email field
- Grid
- Heading
- Icon
- Image
- Link
- Menu
- Money field
- Number field
- Ordered list
- Paragraph
- Password field
- Query container
- Search field
- Section
- Select
- Spinner
- Stack
- Switch
- Table
- Text
- Text area
- Text field
- Thumbnail
- Tooltip
- Url field
- Unordered list
Available APIs
Supported components
- Admin action
- Avatar
- Badge
- Banner
- Box
- Button
- Button group
- Checkbox
- Chip
- Choice list
- Clickable
- Clickable chip
- Color field
- Color picker
- Date field
- Date picker
- Divider
- Drop zone
- Email field
- Grid
- Heading
- Icon
- Image
- Link
- Menu
- Money field
- Number field
- Ordered list
- Paragraph
- Password field
- Query container
- Search field
- Section
- Select
- Spinner
- Stack
- Switch
- Table
- Text
- Text area
- Text field
- Thumbnail
- Tooltip
- Url field
- Unordered list
Available APIs
jsx
Examples
Description
Add an action extension that syncs selected discounts to an external POS or marketing system. This example demonstrates batch syncing with configurable options for location targeting and overwrite behavior.
jsx
import {render} from 'preact'; import {useState} from 'preact/hooks'; export default async () => { render(<Extension />, document.body); }; const Extension = () => { const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(null); const [syncAllLocations, setSyncAllLocations] = useState(true); const [posSystem, setPosSystem] = useState('square'); const handleSync = async () => { setLoading(true); setError(null); try { const response = await fetch('https://your-app.com/api/pos/sync-discount', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ discountIds: shopify.data.selected.map(item => item.id), posSystem, syncAllLocations, }), }); if (response.ok) { setSuccess(true); shopify.close(); } else { const data = await response.json(); setError(data.message || 'Failed to sync discount'); } } catch (err) { setError('Connection error. Please try again.'); } finally { setLoading(false); } }; const selectedCount = shopify.data.selected.length; return ( <s-admin-action heading="Sync to POS"> <s-stack direction="block" gap="base"> {success && ( <s-banner tone="success" dismissible={false}> {selectedCount} discount(s) synced to POS successfully! </s-banner> )} {error && ( <s-banner tone="critical" dismissible={false}> {error} </s-banner> )} <s-text> Sync {selectedCount} selected discount(s) to your POS system for in-store use. </s-text> <s-section heading="POS Settings"> <s-stack direction="block" gap="small"> <s-select label="POS System" value={posSystem} onChange={(e) => setPosSystem(e.currentTarget.value)} > <s-option value="square">Square</s-option> <s-option value="clover">Clover</s-option> <s-option value="lightspeed">Lightspeed</s-option> </s-select> <s-checkbox label="Sync to all store locations" checked={syncAllLocations} onChange={(e) => setSyncAllLocations(e.currentTarget.checked)} /> </s-stack> </s-section> <s-button-group> <s-button variant="primary" onClick={handleSync} disabled={loading || success} > {loading ? 'Syncing...' : `Sync ${selectedCount} Discount(s)`} </s-button> <s-button onClick={() => shopify.close()}>Cancel</s-button> </s-button-group> </s-stack> </s-admin-action> ); };Description
Add an action extension that lets merchants tag selected discounts with a campaign name for organization. This example demonstrates working with selected items from the index page and sending batch updates to your app backend.
jsx
import {render} from 'preact'; import {useState} from 'preact/hooks'; export default async () => { render(<Extension />, document.body); }; const Extension = () => { const [campaignTag, setCampaignTag] = useState(''); const [loading, setLoading] = useState(false); const [success, setSuccess] = useState(false); const [error, setError] = useState(null); const selectedCount = shopify.data.selected?.length || 0; const discountIds = shopify.data.selected?.map((item) => item.id) || []; const handleTagDiscounts = async () => { if (!campaignTag.trim()) { setError('Please enter a campaign tag'); return; } setLoading(true); setError(null); try { const response = await fetch('https://your-app.com/api/discounts/tag', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ discountIds, tag: campaignTag.trim(), }), }); if (response.ok) { setSuccess(true); shopify.close(); } else { const result = await response.json(); setError(result.message || 'Failed to tag discounts'); } } catch (err) { setError('Network error. Please try again.'); } finally { setLoading(false); } }; return ( <s-admin-action heading="Tag Discounts by Campaign"> <s-stack direction="block" gap="base"> {success && ( <s-banner tone="success" dismissible={false}> Successfully tagged {selectedCount} discount{selectedCount !== 1 ? 's' : ''} with "{campaignTag}" </s-banner> )} {error && ( <s-banner tone="critical" dismissible={false}> {error} </s-banner> )} <s-text> Add a campaign tag to {selectedCount} selected discount{selectedCount !== 1 ? 's' : ''} for easier organization and filtering. </s-text> <s-text-field label="Campaign tag" value={campaignTag} onChange={(e) => setCampaignTag(e.currentTarget.value)} placeholder="e.g., Summer Sale 2025, Black Friday" /> <s-button-group> <s-button variant="primary" onClick={handleTagDiscounts} disabled={loading || success || !campaignTag.trim()} > {loading ? 'Tagging...' : `Tag ${selectedCount} Discount${selectedCount !== 1 ? 's' : ''}`} </s-button> <s-button onClick={() => shopify.close()}>Cancel</s-button> </s-button-group> </s-stack> </s-admin-action> ); };
Anchor to Discount index action (should render) ,[object Object]Discount index action (should render) target
admin.discount-index.action.should-render
Controls the render state of an admin action extension on the discounts index page. Use this target to conditionally show or hide your action extension based on the discount's properties, such as status, configuration, or specific business requirements.
This target returns a boolean value that determines whether the corresponding action extension appears in the More actions menu. The extension evaluates each time the page loads.
Supported components
Available APIs
Supported components
Available APIs
jsx
Examples
Description
Show the discount performance action only when the app backend confirms the selected discounts are being tracked for analytics and have sufficient redemption data available.
jsx
export default async () => { const selectedDiscounts = shopify.data.selected; if (!selectedDiscounts || selectedDiscounts.length === 0) { return { display: false }; } try { const discountIds = selectedDiscounts.map(discount => discount.id); const response = await fetch('https://your-app.com/api/discounts/check-tracking', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ discountIds, checkType: 'performance_analytics', }), }); if (!response.ok) { return { display: false }; } const result = await response.json(); // Show action if discounts are tracked and have redemption data return { display: result.isTracked && result.hasRedemptionData, }; } catch (err) { console.error('Failed to check discount tracking status:', err); return { display: false }; } };Description
Display the bulk action only when the selected discount is currently active, checking the discount status using the GraphQL Admin API.
jsx
export default async () => { const discountId = shopify.data.selected[0].id; try { const response = await fetch('shopify:admin/api/graphql.json', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ query: `query GetDiscountStatus($id: ID!) { discountNode(id: $id) { discount { ... on DiscountCodeBasic { status startsAt endsAt } ... on DiscountCodeFreeShipping { status startsAt endsAt } ... on DiscountAutomaticBasic { status startsAt endsAt } } } }`, variables: { id: discountId }, }), }); const { data } = await response.json(); const discount = data?.discountNode?.discount; // Only show action for active discounts const isActive = discount?.status === 'ACTIVE'; return { display: isActive }; } catch (err) { console.error('Error checking discount status:', err); return { display: false }; } };
Anchor to Best practicesBest practices
- Differentiate discount types: Discounts come in multiple types (basic, BXGY, free shipping, automatic vs code-based). Before displaying discount actions, check the discount type using GraphQL to ensure your extension supports it. For example, POS sync may only work with certain discount types.
- Validate discount dates: Always check
startsAtandendsAtwhen displaying or syncing discounts. Syncing expired or not-yet-active discounts to external systems can create customer confusion and needs special handling or filtering. - Show usage vs limits clearly: When displaying discount analytics, show both
asyncUsageCountandusageLimittogether. Merchants need to see how close a discount is to its usage limit to decide whether to extend it or create a new code. - Handle discount combinations: Shopify has complex discount combination rules. If your extension recommends or creates discounts, validate that they're compatible with existing discount configurations to avoid conflicts that prevent customers from completing checkouts.
- Account for attribution delays: Discount usage counts (
asyncUsageCount) update asynchronously and may lag by several minutes. When displaying real-time analytics, indicate that counts are approximate and mention the last update time if available from your system.
Anchor to LimitationsLimitations
- Single target per module: Each
[[extensions.targeting]]entry in your TOML configuration maps one target to one module file. - Multiple discount type schemas: Discounts are a GraphQL union type with eight possible types:
DiscountAutomaticBasic,DiscountCodeBasic,DiscountAutomaticBxgy,DiscountCodeBxgy,DiscountAutomaticFreeShipping,DiscountCodeFreeShipping,DiscountCodeApp,DiscountAutomaticApp. Each type has different fields. - Asynchronous usage count: The
asyncUsageCountfield on GraphQLDiscountunion types is updated asynchronously and might show a lower count until the process is completed.