Building AI-Powered Post-Purchase Upsells for Shopify: A Complete Implementation Guide

Ever wondered how to create smarter product recommendations that actually convert? I recently built a post-purchase upsell extension that uses OpenAI’s embeddings to suggest products based on what customers just bought—and I’m sharing exactly how I did it.

Key Insights You’ll Gain

  • Step-by-step implementation of Shopify’s post-purchase UI extensions
  • How to integrate OpenAI embeddings for intelligent product recommendations
  • JWT validation requirements for secure post-purchase flows
  • Real-world performance expectations and conversion insights
  • Extension and deployment strategies using Gadget and Shopify CLI

Understanding Post-Purchase Extensions

Post-purchase extensions appear after checkout completion but before the thank you page. They’re currently in beta, but you can build and deploy them to stores right now (you just need to request access for public apps).

The extension works in two phases:

  1. Should Render: Determines if an offer should be shown and fetches the recommendation
  2. Render: Displays the actual offer interface where customers accept or decline

Here’s what makes this approach powerful: instead of hardcoded offers, we’re using vector embeddings to find products that semantically match what customers just purchased.

The Technical Architecture

Core Components Setup

I started with a Shopify app template in Gadget, which handles the backend API and database operations. The post-purchase extension itself runs through Shopify CLI as a separate project.

Backend Routes (Gadget):

  • /post-offer - Generates AI-powered product recommendations
  • /post-sign-changeset - Handles order modifications when offers are accepted

Key Requirements:

  • JWT validation for security
  • OpenAI connection for embeddings
  • Shopify product data sync
  • CORS configuration for cross-origin requests

Setting Up Product Embeddings

First, I added an embedding field to the Shopify product model in Gadget:

// In product model's create/update actions
const createEmbedding = async (record, api, logger, connections) => {
  if (!record.embedding || record.title !== record.title || record.body !== record.body) {
    const response = await connections.openai.embeddings.create({
      model: "text-embedding-ada-002",
      input: `${record.title} ${record.body}`
    });
    
    await api.shopifyProduct.update(record.id, {
      embedding: response.data[0].embedding
    });
  }
};

This creates vector representations of all products automatically when they’re created or updated.

Building the Smart Recommendation Engine

The magic happens in the offer generation logic:

Step 1: Analyze Cart Contents

// Extract product information from cart items
const productsInCart = await Promise.all(
  variantIds.map(async (variantId) => {
    const variant = await api.shopifyProductVariant.findOne(variantId, {
      select: { product: { id: true, title: true, body: true } }
    });
    return variant.product;
  })
);

Step 2: Create Cart Embedding

// Generate embedding for cart contents
const input = `This is a list of product titles and descriptions in a shopping cart. I want to find a product that can be recommended based on similarity to these products: ${productsInCart.map(p => `${p.title} ${p.body}`).join(' ')}`;

const cartEmbedding = await connections.openai.embeddings.create({
  model: "text-embedding-ada-002",
  input: input
});

Step 3: Find Similar Products

// Find most similar products using cosine similarity
const recommendedProducts = await api.shopifyProduct.findMany({
  sort: {
    embedding: { cosineSimilarityTo: cartEmbedding.data[0].embedding }
  },
  first: 5,
  filter: {
    id: { notIn: productsInCart.map(p => p.id) }, // Exclude cart items
    shopId: { equals: connections.shopify.currentShopId }
  },
  select: {
    id: true,
    title: true,
    body: true,
    variants: { edges: { node: { id: true, price: true } } },
    images: { edges: { node: { source: true } } }
  }
});

Implementing the Extension

JWT Security Validation

Both routes require proper JWT validation to ensure requests come from Shopify:

const validateRequest = (request, reply) => {
  const token = getToken(request);
  const decoded = jwt.verify(token, process.env.SHOPIFY_API_SECRET);
  const { referenceId } = request.body;
  
  if (decoded.referenceId !== referenceId) {
    return reply.code(401).send({ error: "Unauthorized" });
  }
  
  return decoded;
};

Frontend Extension Code

The extension uses Shopify’s UI components and hooks:

export default reactExtension("purchase.checkout.block.render", () => <App />);

function App() {
  const { storage, inputData } = useExtensionInput();
  const { applyChangeset, calculateChangeset } = useExtensionApi();
  
  // Fetch offers during should render phase
  const offers = storage.read('offers');
  
  const acceptOffer = async () => {
    // Sign changeset with backend
    const response = await api.fetch('/post-sign-changeset', {
      method: 'POST',
      headers: { authorization: `Bearer ${inputData.token}` },
      body: JSON.stringify({
        referenceId: inputData.referenceId,
        changes: purchaseOption.changes
      })
    });
    
    await applyChangeset(response.token);
  };
  
  return (
    // Shopify UI components for the offer display
  );
}

Real-World Performance and Testing

I tested the system with different product combinations:

  • Rain boots + raincoat → Recommended umbrella :white_check_mark:
  • Ski goggles + helmet → Recommended snowboard (though price mismatch was an issue)

The recommendations were semantically accurate, showing the embedding approach works well for finding related products.

Conversion Rate Expectations

Based on industry research and testing:

  • Typical conversion: 4-10% for well-targeted offers
  • Success factors: Price appropriateness, product relevance, timing
  • Best practices: Offer complementary items at reasonable price points relative to cart value

Extension and Customization Options

Advanced Filtering Options

You can enhance the recommendation logic with:

// Add price-based filtering
filter: {
  variants: {
    some: {
      price: { lessThan: maxPriceThreshold }
    }
  },
  // Add collection-based targeting
  collections: {
    some: { id: { in: targetCollectionIds } }
  }
}

Admin Interface Integration

Consider building an embedded admin app to:

  • Whitelist products available for upsells
  • Set custom discount percentages per product
  • Configure collection-based triggers
  • Monitor conversion analytics

Beyond Product Upsells

Post-purchase extensions aren’t limited to product offers. Other effective uses include:

  • Newsletter signups with incentives
  • Loyalty program enrollment
  • Customer feedback collection
  • Donation requests
  • Subscription offerings for consumable products

Deployment and Hosting

Extension hosting: Shopify handles hosting automatically via yarn deploy
Backend hosting: Gadget manages all infrastructure and scaling
No custom hosting required: Both platforms handle everything

Key Technical Considerations

JWT Token Management

The most critical aspect is proper JWT validation. Both your offer and changeset routes must verify tokens against your Shopify API secret.

Rate Limiting

When generating embeddings for existing products, be mindful of OpenAI rate limits. Consider batch processing for large catalogs.

Error Handling

Always provide fallback options if AI recommendations fail—perhaps a curated list of bestsellers or sale items.

Practical Implementation Tips

Start Simple: Begin with hardcoded offers to get the extension flow working, then add AI intelligence.

Test Thoroughly: Use Shopify’s browser extension for testing post-purchase flows during development.

Monitor Performance: Track both technical performance (response times) and business metrics (conversion rates).

Price Sensitivity: Ensure recommended products align with cart value—a $900 snowboard after a $130 purchase rarely converts well.

Next Steps and Extensions

This foundation can power multiple types of purchase experiences:

  • Pre-purchase recommendations in checkout
  • Cross-sell suggestions in cart drawers
  • Email follow-up campaigns with personalized products
  • Product page “frequently bought together” sections

The same embedding comparison logic works across all these touchpoints, creating a consistent and intelligent recommendation system throughout your store.

FAQ

Can we close offers after a certain time period?
Yes, you can implement timeouts using setTimeout in the render function and call the done() method to close the extension.

Do accepted offers create separate orders?
No, accepted offers are appended to the original order as additional line items, processed as separate payments but combined in one order.

What about the new OpenAI features?
The implementation supports all latest OpenAI models including GPT-4 Turbo and GPT-4 Turbo with Vision through the standard OpenAI client.

Can we target specific collections as triggers?
Absolutely. You can write custom logic in the should render phase or offer route to target specific collections or any other product criteria.

Is this available outside beta preview?
Post-purchase extensions are in beta but available for both public and custom apps. Public apps require requesting access, and the feature is limited to Shopify Plus merchants.