Home › Blog › Gift cards & digital products
Shopify gift cards and digital products: the AI shopping agent blind spots most stores ignore
Two product categories — gift cards and digital downloads — sit in a structural dead zone. Shopify's default JSON-LD was written for physical goods with a fixed price and a shipping address. Neither category fits. The result: AI shopping agents skip them, score them incorrectly, or surface misleading delivery costs to shoppers who search by intent.
On this page
- Why gift cards and digital products are different
- Gift card structured data failures, one by one
- The complete gift card JSON-LD fix
- Digital product structured data failures
- Choosing the right schema type per digital product category
- The complete digital product JSON-LD fix
- How each AI agent handles these categories today
- Liquid implementation: detecting and branching per product type
- Verification checklist
Why gift cards and digital products are different
Most structured data guides for Shopify assume a simple model: a product has a SKU, a weight, a price, and ships to an address. Gift cards and digital products violate this model in complementary ways.
Gift cards are financial instruments, not goods. They have no GTIN. Their "price" is the denomination — not the cost of something delivered. A shopper purchasing a $50 gift card is buying purchasing power, not a product. Schema.org's Product type wasn't designed for this, and Shopify's default JSON-LD output for gift cards triggers GTIN validation warnings, outputs misleading offers, and never signals "this is a gift-giving product" to any downstream agent.
Digital products are goods, but they're not physical. They have no weight, no shipping address, no delivery time in the conventional sense. Yet Shopify's default theme outputs OfferShippingDetails on every product — including digital ones — which means AI agents either see a nonsensical shipping cost or, worse, a free-shipping signal that has nothing to do with the product type.
OfferShippingDetails on download pagesGift card structured data failures, one by one
Failure 1: GTIN validation errors
Shopify's default JSON-LD includes a gtin or gtin13 field populated from the product's barcode field. For physical products, this is correct. For gift cards, the barcode field is almost always empty — Shopify itself says gift cards don't require GTINs. But the default theme either outputs an empty string ("gtin": "") or omits the field entirely without the required signal that its absence is intentional.
Google's Rich Results validator and the schema.org validator both flag products with a missing or empty GTIN as "incomplete" unless identifier_exists: false is explicitly set. Without that signal, the parser treats the product as having an unknown identifier — not as a legitimately GTIN-free item. ChatGPT's crawler makes the same inference.
Failure 2: Single-price offer for variable-denomination cards
Most gift card products are set up with multiple variants: $10, $25, $50, $100. Shopify's default JSON-LD outputs a single Offer with the price of the first variant (or the minimum price with priceValidUntil). For a gift card, this is misleading: an AI agent seeing "Gift Card — $10" doesn't know that a $100 denomination exists, may cite the wrong price in an answer, and can't help a shopper looking for a specific gift amount.
The correct pattern is AggregateOffer with lowPrice, highPrice, and a full offers array containing one Offer per denomination.
Failure 3: No gift-use or recipient signals
A shopper who asks ChatGPT "what's a good gift for a coffee lover under $50" is expressing purchase intent that gift cards are perfectly positioned to satisfy — but only if the agent understands the product is a gift instrument. Schema.org's additionalProperty field can carry isGiftCard: true and giftRecipient signals, but no Shopify theme outputs these by default.
Google AI Mode specifically uses additionalProperty for contextual filtering in gift-related queries. Without these signals, your gift card competes only on price against physical products — not on gift-appropriateness, which is the intent you should win on.
Failure 4: Wrong itemCondition
Shopify's default JSON-LD outputs "itemCondition": "https://schema.org/NewCondition" for all products. For gift cards, condition is semantically meaningless — a gift card can't be "used" or "refurbished." Outputs this signal when it doesn't apply can confuse parsers that look for condition-appropriate product types.
The complete gift card JSON-LD fix
The fix has three components: the identifier_exists signal, an AggregateOffer with per-denomination offers, and gift-use additionalProperty values. Here's the complete Liquid snippet for your Shopify theme:
{% if product.gift_card? %}
{%- assign min_price = product.price_min | divided_by: 100.0 -%}
{%- assign max_price = product.price_max | divided_by: 100.0 -%}
{%- assign currency = cart.currency.iso_code -%}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": {{ product.title | json }},
"description": {{ product.description | strip_html | truncate: 300 | json }},
"url": "{{ shop.url }}{{ product.url }}",
"image": "{{ product.featured_image | img_url: '1024x1024' }}",
"identifier_exists": false,
"additionalProperty": [
{
"@type": "PropertyValue",
"name": "isGiftCard",
"value": "true"
},
{
"@type": "PropertyValue",
"name": "giftUsage",
"value": "RedeemInStore"
}
],
"offers": {
"@type": "AggregateOffer",
"lowPrice": {{ min_price | json }},
"highPrice": {{ max_price | json }},
"priceCurrency": {{ currency | json }},
"offerCount": {{ product.variants.size }},
"availability": "https://schema.org/InStock",
"offers": [
{%- for variant in product.variants -%}
{
"@type": "Offer",
"name": {{ variant.title | json }},
"price": {{ variant.price | divided_by: 100.0 | json }},
"priceCurrency": {{ currency | json }},
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"url": "{{ shop.url }}{{ product.url }}?variant={{ variant.id }}"
}{% unless forloop.last %},{% endunless %}
{%- endfor -%}
]
}
}
</script>
{% endif %}
identifier_exists: false replaces the empty GTIN. AggregateOffer replaces the single Offer. additionalProperty adds the gift-use signal. itemCondition is omitted from the aggregate (only meaningful per variant). No OfferShippingDetails — gift cards are delivered digitally.
Place this snippet in your product template file (sections/product-template.liquid or sections/main-product.liquid depending on your theme) after the default JSON-LD block, wrapped in {% if product.gift_card? %} so it only fires for gift card products. The {% if product.gift_card? %} tag is a native Shopify Liquid tag — no metafield setup required.
Digital product structured data failures
Failure 1: Phantom shipping costs
This is the most damaging error. Shopify's default theme outputs OfferShippingDetails on every product page. For digital products — PDFs, software, course access, music, design assets — there is no shipping. Yet the AI agent reads the shipping details block and may report a shipping cost (or a confusing "free shipping" signal) that doesn't map to what actually happens when someone buys.
Google AI Mode specifically uses OfferShippingDetails to filter for "free shipping" product queries. If your $29 digital download has a $0 shipping cost because the merchant ships nothing, AI agents may surface it in "free shipping" filters — which is technically correct but semantically misleading, and can drive mismatch traffic that bounces.
The fix is to explicitly suppress shippingDetails and instead add a hasDeliveryMethod signal with DigitalDeliveryMethod.
Failure 2: Wrong schema.org @type
Shopify's default JSON-LD outputs "@type": "Product" for everything. But schema.org has specific types for digital goods that AI agents actively use for query matching. A PDF guide should be typed as Book or DigitalDocument. Software should be SoftwareApplication. A course should be Course. Music should be MusicRecording. These type signals directly influence which query categories the product appears in.
When a shopper asks Perplexity "what's a good book about Shopify SEO," Perplexity looks for schema with @type: Book in its product index — not generic Product types with "book" in the title. The same is true for software (SoftwareApplication) and courses (Course).
Failure 3: Missing file format and access signals
AI agents increasingly use encodingFormat, fileFormat, and applicationCategory to surface digital products in format-specific queries ("where can I download a Shopify audit template PDF"). None of these appear in Shopify's default output. They require explicit structured data — and none of the major digital delivery apps (Sky Pilot, SendOwl, Shopify Digital Downloads, Fileflare, FetchApp) add them. See our digital products AI visibility guide for a complete app-by-app audit.
Failure 4: No software pricing model signal
For software products sold on subscription (SaaS tools, plugins, templates with annual licenses), the default Shopify Offer shows a flat price with no indication of the billing period. Schema.org's PriceSpecification with billingDuration and billingIncrement — combined with UnitPriceSpecification — communicate the pricing model to AI agents. Without it, a $99/year software product looks identical to a $99 one-time-purchase. This matters because AI agents filter on "subscription" vs "one-time" in purchase intent queries.
Choosing the right schema type per digital product category
| Product category | Primary @type | Key additional fields | AI agent query match |
|---|---|---|---|
| PDF guide / ebook | Book or DigitalDocument |
bookFormat: EBook, numberOfPages, inLanguage |
Book searches, "download PDF", topic + format queries |
| Software / plugin / app | SoftwareApplication |
applicationCategory, operatingSystem, softwareVersion, featureList |
Software searches, "plugin for X", "tool to do Y" |
| Online course / training | Course |
provider, courseWorkload, educationalLevel, hasCourseInstance |
Learning queries, "course on X", skill acquisition |
| Music / audio | MusicRecording or MusicAlbum |
byArtist, inAlbum, duration, encodingFormat |
Music searches, artist queries, format-specific (MP3, WAV) |
| Design asset / template | CreativeWork + VisualArtwork |
encodingFormat, fileFormat, license, copyrightHolder |
Design queries, "Figma template", "Canva asset" |
| Fonts / typefaces | DigitalDocument |
encodingFormat: font/ttf, license, additionalProperty for font style/weight |
Font searches, "serif font download", "commercial license font" |
| Photography / stock images | ImageObject |
contentUrl (preview), license, encodingFormat, width, height |
Stock photo searches, "royalty free X", image format queries |
| License key / access | Product + additionalProperty |
isRelatedTo pointing to the licensed software, additionalProperty for license type/duration |
License purchase queries, "buy license for X" |
The complete digital product JSON-LD fix
The following snippet covers the most common case: a standalone digital download sold at a fixed price. Adapt the @type and additional fields per the table above.
{% assign is_digital = false %}
{% assign digital_types = "PDF,Ebook,Download,Template,Plugin,Course,Font,Music,Preset,Software" | split: "," %}
{% for dtype in digital_types %}
{% if product.type contains dtype %}{% assign is_digital = true %}{% endif %}
{% endfor %}
{% if is_digital %}
{%- assign variant = product.selected_or_first_available_variant -%}
{%- assign price = variant.price | divided_by: 100.0 -%}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Product",
"name": {{ product.title | json }},
"description": {{ product.description | strip_html | truncate: 300 | json }},
"url": "{{ shop.url }}{{ product.url }}",
"image": "{{ product.featured_image | img_url: '1024x1024' }}",
"brand": {
"@type": "Brand",
"name": {{ shop.name | json }}
},
"offers": {
"@type": "Offer",
"price": {{ price | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"url": "{{ shop.url }}{{ product.url }}",
"hasDeliveryMethod": {
"@type": "DeliveryMethod",
"@id": "https://schema.org/DigitalDeliveryMethod"
}
},
"additionalProperty": [
{
"@type": "PropertyValue",
"name": "deliveryMethod",
"value": "Digital download"
},
{
"@type": "PropertyValue",
"name": "fileFormat",
"value": {{ product.metafields.custom.file_format | default: "PDF" | json }}
}
]
}
</script>
{% endif %}
Notice what's absent: OfferShippingDetails. This is intentional. When you explicitly provide hasDeliveryMethod: DigitalDeliveryMethod, schema.org validators treat the absence of shipping details as correct, not as an error. This stops the phantom shipping cost problem entirely.
Adding SoftwareApplication type for plugins and tools
If your digital product is software, extend the base schema with type-specific fields. The SoftwareApplication type unlocks dedicated software result types in Google AI Mode and Bing:
{% if product.type == "Plugin" or product.type == "Software" %}
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": {{ product.title | json }},
"applicationCategory": {{ product.metafields.custom.app_category | default: "BusinessApplication" | json }},
"operatingSystem": {{ product.metafields.custom.operating_system | default: "Any" | json }},
"softwareVersion": {{ product.metafields.custom.software_version | default: "1.0" | json }},
"featureList": {{ product.metafields.custom.feature_list | json }},
"offers": {
"@type": "Offer",
"price": {{ product.price | divided_by: 100.0 | json }},
"priceCurrency": {{ cart.currency.iso_code | json }},
"availability": "https://schema.org/InStock",
"hasDeliveryMethod": {
"@type": "DeliveryMethod",
"@id": "https://schema.org/DigitalDeliveryMethod"
}
}
}
</script>
{% endif %}
custom.file_format, custom.app_category, and custom.software_version. Create these as product metafields in Shopify admin (Settings → Custom data → Products). The default: fallback ensures the schema still outputs something useful even if the metafield is missing, so you can deploy the template change first and populate metafields over time.
How each AI agent handles these categories today
| Agent | Gift cards without fix | Gift cards with fix | Digital products without fix | Digital products with fix |
|---|---|---|---|---|
| ChatGPT Shopping | Often excluded (GTIN warning causes validation skip); if shown, only minimum denomination visible | Full denomination range shown; gift-use signals improve appearance in gift intent queries | Included but shipping cost shown as $0 (confusing); wrong type match for format queries | Correctly typed; appears in format-specific queries; no shipping noise |
| Google AI Mode | GTIN validation error → deprioritized from shopping surfaces; price snippet shows minimum | Appears in gift-intent queries; denomination range surfaced in answer; no validation errors | May appear but shipping filter mismatch; wrong type for Book/Software/Course queries | Correct type match; appears in domain-specific queries; no shipping mismatch |
| Perplexity Shopping | Included but denomination context missing; price shows as single value | AggregateOffer surfaced; gift-use context included in product card | Included; phantom shipping details sometimes surfaced in answer | Clean product card; DigitalDeliveryMethod correctly interpreted |
| Bing / Copilot | Typically included but incomplete; no gift signals | Gift intent queries matched; full offer range visible | Included with spurious shipping block | Clean; type-specific matching for Software/Book/Course types |
| Google Shopping | Missing GTIN flag; may be disapproved from shopping campaigns | identifier_exists:false exempts from GTIN requirement; approved for shopping | Shipping cost listed as $0 in Shopping tab (correct but looks like error) | DigitalDeliveryMethod → no shipping column; correctly listed as digital item |
Liquid implementation: detecting and branching per product type
The cleanest implementation uses a single entry point in your product template that detects product type and routes to the appropriate schema block. This avoids duplicating the detection logic across multiple snippet files:
{%- comment -%}
CatalogScan-compatible schema routing
Place in: sections/main-product.liquid (Dawn) or sections/product-template.liquid (older themes)
Position: before closing </main> tag, after default Shopify JSON-LD block
{%- endcomment -%}
{% if product.gift_card? %}
{%- render 'schema-gift-card', product: product -%}
{% elsif product.type == "Plugin" or product.type == "Software" %}
{%- render 'schema-software', product: product -%}
{% elsif product.type == "Course" or product.type == "Training" %}
{%- render 'schema-course', product: product -%}
{% elsif product.type contains "PDF" or product.type contains "Ebook" or product.type == "Download" %}
{%- render 'schema-digital-document', product: product -%}
{% elsif product.type contains "Music" or product.type contains "Audio" %}
{%- render 'schema-music', product: product -%}
{% elsif product.type contains "Template" or product.type contains "Design" %}
{%- render 'schema-creative-work', product: product -%}
{% endif %}
This pattern has a side effect worth noting: when you render a specialized schema block for digital products, you should also suppress the default Shopify JSON-LD block. Shopify themes inject their own <script type="application/ld+json"> block. If you don't suppress it, you'll have two Product schema blocks on the same page — which is valid JSON-LD but creates ambiguity for parsers that prioritize one block over the other.
In Dawn theme, the default schema is in sections/main-product.liquid, inside a {% schema %}-driven block. You can wrap it with your own detection logic:
{% unless product.gift_card? or digital_types contains product.type %}
{%- comment -%} Default Shopify JSON-LD — only for standard physical products {%- endcomment -%}
{{ default_product_schema }}
{% endunless %}
For a full reference on how Shopify themes inject structured data by default and where to intercept it, see our Shopify schema markup reference guide and the structured data testing tools guide for how to verify changes before pushing live.
Verification checklist
After implementing the changes, use the following sequence to confirm correctness. Don't rely on any single tool — each catches different error classes.
- Google Rich Results Test (
search.google.com/test/rich-results) — paste your gift card or digital product URL. Look specifically for: no GTIN validation warning on gift cards; correct@typeon digital products; no shipping details warning on digital downloads. - Schema.org validator (
validator.schema.org) — this catches type-mismatch errors the Rich Results Test misses. Verifyidentifier_exists: falseis parsed correctly andAggregateOfferhas all required fields. - Manual curl inspection — AI crawlers (GPTBot, PerplexityBot) don't execute JavaScript. Confirm your schema renders server-side:
curl -s -A "GPTBot/1.1" https://yourdomain.com/products/gift-card | grep -A 30 'application/ld+json' - Shopify Theme Check — run
shopify theme checkon your edited Liquid files to catch syntax errors before deploying. - CatalogScan — run a full free scan of your store. CatalogScan specifically flags gift card GTIN issues, digital product phantom shipping, and missing type signals as distinct line items in the AI-readiness report. It checks all products in your catalog, not just the URL you remember to test.
Does your store have gift card or digital product schema errors?
CatalogScan checks all 18 AI-agent signals across your full product catalog — including GTIN validation, schema type correctness, and phantom shipping details on digital products. Free, no login required.
Run a free store scan More guides