--- title: Create a post-purchase subscription description: Learn to add a subscription to your post-purchase checkout extension. source_url: html: >- https://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription md: >- https://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription.md --- ExpandOn this page * [What you'll learn](https://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription.md#what-youll-learn) * [Requirements](https://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription.md#requirements) * [Limitations](https://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription.md#limitations) * [Step 1: Add required data](https://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription.md#step-1-add-required-data) * [Step 2: Return subscription information from the app server](https://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription.md#step-2-return-subscription-information-from-the-app-server) * [Step 3: Update your extension code to offer subscriptions](https://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription.md#step-3-update-your-extension-code-to-offer-subscriptions) * [Step 4: Test your extension](https://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription.md#step-4-test-your-extension) * [Next steps](https://shopify.dev/docs/apps/build/checkout/product-offers/create-a-post-purchase-subscription.md#next-steps) # Create a post-purchase subscription Beta Post-purchase checkout extensions are in beta and can be used without restrictions in a [development store](https://shopify.dev/docs/api/development-stores). To use post-purchase extensions on a live store, you need to [request access](https://shopify.dev/docs/apps/build/checkout/product-offers/build-a-post-purchase-offer#step-5-request-access). In this tutorial, you'll learn how to use Shopify Extensions to create a post-purchase offer that allows a buyer to add a subscription to their order. ![Overview of a post-purchase subscription added to a single item](https://shopify.dev/assets/assets/images/api/post-purchase/post-purchase-subscription-overview-CsvBlHsM.png) *** ## What you'll learn In this tutorial, you’ll learn how to do the following tasks: * Update your app with the required scopes to manage subscriptions * Add UI elements to allow buyers to select one time or subscription products * Update the app server to return subscription data *** ## Requirements * You've completed [Build a post-purchase product offer checkout extension](https://shopify.dev/docs/apps/build/checkout/product-offers/build-a-post-purchase-offer). *** ## Limitations You can't create a post-purchase subscription that does any of the following things: * Modifies a subscription on an order with an existing subscription * Adds a subscription to an order with an existing subscription * Converts a one-time purchase into a subscription order *** ## Step 1: Add required data To offer a customer a subscription, a product needs to have an associated selling plan group. [Selling plans](https://shopify.dev/docs/apps/build/purchase-options/subscriptions/selling-plans) represent how products can be sold and purchased. When you create a selling plan, you can determine the policies under which a product can be sold. For example, you can create a selling plan where a customer can purchase a subscription on a monthly billing cycle, and where you offer a 15% discount for weekly deliveries of the product. Note If your app already has the capability to manage selling plans, or if you're integrating with an app that already has this capability, then you can skip to [step 2](#step-2-return-subscription-information-from-the-app-server). To complete this tutorial, the product on the store you will have in the upsell offer needs to be associated with a selling plan. ### Update app scopes To create a selling plan and associate it to a product, you need to add the `write_purchase_options` scope to the app. Update the app scopes in the `shopify.app.toml` file to include the `write_purchase_options` scope. This scope allows you to create selling plan groups. After you update scopes, when you navigate to the app home in the Shopify admin, you should be prompted to reauthorize the app to allow editing of purchase options. If you're not prompted to reauthorize, restart your server, and then navigate to the app home in the Shopify admin. ## shopify.app.toml ```toml scopes = "write_products,write_purchase_options" ``` ### Create a selling plan group Use the GraphQL Admin API to create a selling plan group, and associate a product with the selling plan group. In the following `cURL` command, add the `myshopify` domain of your development store, the access token for the app, and the product and variant IDs of the product that you're offering in the upsell. Note You can run `npm run prisma studio` to view your data in your browser. The access token is stored in the `Session` table in the `accessToken` column. ## Terminal ```shell curl -X POST \ -H 'X-Shopify-Access-Token: ' \ -H 'Content-Type: application/graphql' \ -d 'mutation { sellingPlanGroupCreate( input: { name: "Subscribe and save" merchantCode: "subscribe-and-save" options: ["Delivery every"] position: 1 sellingPlansToCreate: [ { name: "Delivered every week" options: "1 Week(s)" position: 1 category: SUBSCRIPTION billingPolicy: { recurring: { interval: WEEK, intervalCount: 1 anchors: { type: WEEKDAY, day: 1 } } } deliveryPolicy: { recurring: { interval: WEEK, intervalCount: 1 anchors: { type: WEEKDAY, day: 1 } preAnchorBehavior: ASAP cutoff: 0 intent: FULFILLMENT_BEGIN } } pricingPolicies: [ { fixed: { adjustmentType: PERCENTAGE adjustmentValue: { percentage: 15.0 } } } ] } { name: "Delivered every two weeks" options: "2 Week(s)" position: 2 category: SUBSCRIPTION billingPolicy: { recurring: { interval: WEEK, intervalCount: 2 anchors: { type: WEEKDAY, day: 1 } } } deliveryPolicy: { recurring: { interval: WEEK, intervalCount: 2 anchors: { type: WEEKDAY, day: 1 } preAnchorBehavior: ASAP cutoff: 0 intent: FULFILLMENT_BEGIN } } pricingPolicies: [ { fixed: { adjustmentType: PERCENTAGE adjustmentValue: { percentage: 10.0 } } } ] } { name: "Delivered every month" options: "1 Month" position: 3 category: SUBSCRIPTION billingPolicy: { recurring: { interval: MONTH, intervalCount: 1 anchors: { type: MONTHDAY, day: 15 } } } deliveryPolicy: { recurring: { interval: MONTH, intervalCount: 1 anchors: { type: MONTHDAY, day: 15 } preAnchorBehavior: ASAP cutoff: 0 intent: FULFILLMENT_BEGIN } } pricingPolicies: [ { fixed: { adjustmentType: PERCENTAGE adjustmentValue: { percentage: 5.0 } } } ] } ] } resources: { productIds: [\"gid://shopify/Product/\"], productVariantIds: [\"gid://shopify/ProductVariant/\"] } ) { sellingPlanGroup { id sellingPlans(first: 1) { edges { node { id } } } } userErrors { field message } } }' \ 'https://.myshopify.com/admin/api/2023-07/graphql.json' ``` *** ## Step 2: Return subscription information from the app server Update the `OFFERS` array in the `app/offer.server.js` file that you created in the [previous tutorial](https://shopify.dev/docs/apps/build/checkout/product-offers/build-a-post-purchase-offer) to return an offer with the selling plan information that you created in the previous step. ## app/offer.server.js ```js const OFFERS = [ { id: 1, title: "One time offer", productTitle: "The S-Series Snowboard", productImageURL: "https://cdn.shopify.com/s/files/1/", // Replace with product image's URL. productDescription: ["This PREMIUM snowboard is so SUPER DUPER awesome!"], originalPrice: "699.95", discountedPrice: "699.95", changes: [ { type: "add_variant", variantID: 123456789, // Replace with the variant ID. quantity: 1, discount: { value: 15, valueType: "percentage", title: "15% off", }, }, ], }, { id: 2, title: "Weekly subscription offer", productTitle: "The S-Series Snowboard", productImageURL: "https://cdn.shopify.com/s/files/1/0", // Replace with the product image's URL. productDescription: ["This PREMIUM snowboard is so SUPER DUPER awesome!"], originalPrice: "699.95", discountedPrice: "699.95", sellingPlanName: "Subscribe and save", sellingPlanInterval: "WEEK", changes: [ { type: "add_subscription", variantID: 123456789, // Replace with the variant ID. quantity: 1, sellingPlanId: "987654321", // Replace with the selling plan ID. initialShippingPrice: 10, recurringShippingPrice: 10, discount: { value: 15, valueType: "percentage", title: "15% off", }, shippingOption: { title: "Subscription shipping line", presentmentTitle: "Subscription shipping line", }, }, ], }, ]; ``` *** ## Step 3: Update your extension code to offer subscriptions Replace the content of your extension script with the following code. ## src/index ## src/index.jsx ```jsx import { useEffect, useState } from "react"; import { extend, render, useExtensionInput, BlockStack, Button, BuyerConsent, CalloutBanner, Heading, Image, Text, TextContainer, Separator, Select, Tiles, TextBlock, Layout, } from "@shopify/post-purchase-ui-extensions-react"; // For local development, replace APP_URL with your local tunnel URL. const APP_URL = "https://abcd-1234.trycloudflare.com"; // Preload data from your app server to ensure the extension loads quickly. extend( "Checkout::PostPurchase::ShouldRender", async ({ inputData, storage }) => { const postPurchaseOffer = await fetch(`${APP_URL}/api/offer`, { method: "POST", headers: { Authorization: `Bearer ${inputData.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ referenceId: inputData.initialPurchase.referenceId, }), ``` ```js import { extend, BlockStack, Button, CalloutBanner, Heading, Image, Text, TextContainer, Separator, Tiles, TextBlock, Layout, BuyerConsent, Select, } from "@shopify/post-purchase-ui-extensions"; // For local development, replace APP_URL with your local tunnel URL. const APP_URL = "https://abcd-1234.trycloudflare.com"; // Preload data from your app server to ensure the extension loads quickly. extend( "Checkout::PostPurchase::ShouldRender", async ({ inputData, storage }) => { const postPurchaseOffer = await fetch(`${APP_URL}/api/offer`, { method: "POST", headers: { Authorization: `Bearer ${inputData.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ referenceId: inputData.initialPurchase.referenceId, }), }).then((response) => response.json()); await storage.update(postPurchaseOffer); ``` ##### React ``` import { useEffect, useState } from "react"; import { extend, render, useExtensionInput, BlockStack, Button, BuyerConsent, CalloutBanner, Heading, Image, Text, TextContainer, Separator, Select, Tiles, TextBlock, Layout, } from "@shopify/post-purchase-ui-extensions-react"; // For local development, replace APP_URL with your local tunnel URL. const APP_URL = "https://abcd-1234.trycloudflare.com"; // Preload data from your app server to ensure the extension loads quickly. extend( "Checkout::PostPurchase::ShouldRender", async ({ inputData, storage }) => { const postPurchaseOffer = await fetch(`${APP_URL}/api/offer`, { method: "POST", headers: { Authorization: `Bearer ${inputData.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ referenceId: inputData.initialPurchase.referenceId, }), }).then((response) => response.json()); await storage.update(postPurchaseOffer); // For local development, always show the post-purchase page return { render: true }; } ); render("Checkout::PostPurchase::Render", () => ); export function App() { const { storage, inputData, calculateChangeset, applyChangeset, done } = useExtensionInput(); const [loading, setLoading] = useState(true); const [calculatedPurchase, setCalculatedPurchase] = useState(); // Track the buyer's approval to the subscriptions policies. const [buyerConsent, setBuyerConsent] = useState(false); const [buyerConsentError, setBuyerConsentError] = useState(); const [selectedPurchaseOption, setSelectedPurchaseOption] = useState(0); const { offers } = storage.initialData; const purchaseOptions = offers; const purchaseOption = purchaseOptions[selectedPurchaseOption]; // Define the changes that you want to make to the purchase, including the discount to the product. useEffect(() => { async function calculatePurchase() { // Call Shopify to calculate the new price of the purchase, if the above changes are applied. const result = await calculateChangeset({ changes: purchaseOptions[selectedPurchaseOption].changes, }); setCalculatedPurchase(result.calculatedPurchase); setLoading(false); } calculatePurchase(); // Add the selectedPurchaseOption to the dependency of the useEffect. // This will ensure that when the buyer selects a new purchase option, the price breakdown is recalculated. }, [calculateChangeset, purchaseOptions, selectedPurchaseOption]); // Extract values from the calculated purchase. const shipping = calculatedPurchase?.addedShippingLines[0]?.priceSet?.presentmentMoney ?.amount; const taxes = calculatedPurchase?.addedTaxLines[0]?.priceSet?.presentmentMoney?.amount; const total = calculatedPurchase?.totalOutstandingSet.presentmentMoney.amount; const discountedPrice = calculatedPurchase?.updatedLineItems[0].totalPriceSet.presentmentMoney .amount; const originalPrice = calculatedPurchase?.updatedLineItems[0].priceSet.presentmentMoney.amount; async function acceptOffer() { setLoading(true); // Make a request to your app server to sign the changeset with your app's API secret key. const token = await fetch(`${APP_URL}/api/sign-changeset`, { method: "POST", headers: { Authorization: `Bearer ${inputData.token}`, "Content-Type": "application/json", }, body: JSON.stringify({ referenceId: inputData.initialPurchase.referenceId, changes: purchaseOptions[selectedPurchaseOption].id, }), }) .then((response) => response.json()) .then((response) => response.token) .catch((e) => console.log(e)); // Send the value of the buyer consent with the changeset to Shopify to add the subscription const result = await applyChangeset(token, { buyerConsentToSubscriptions: buyerConsent, }); // Ensure that there is no buyer consent error if ( result.errors.find((error) => error.code === "buyer_consent_required") ) { // Show an error if the buyer didn't accept the subscriptions policy setBuyerConsentError("You need to accept the subscriptions policy."); setLoading(false); } else { // Redirect to the Thank you page. done(); } // Redirect to the thank-you page. done(); } function declineOffer() { setLoading(true); // Redirect to the thank-you page. done(); } return ( It's not too late to add this to your order Add the {purchaseOption.productTitle} to your order and{" "} save {purchaseOption.changes[0].discount.title} {purchaseOption.productTitle} {purchaseOptions.length > 1 && ( setSelectedPurchaseOption(parseInt(value, 10)) } value={selectedPurchaseOption.toString()} options={purchaseOptions.map((option, index) => ({ label: option.title, value: index.toString(), }))} /> ``` ```js const purchaseOptionSelect = root.createComponent(Select, { label: 'Purchase options', value: selectedPurchaseOption.toString(), options: purchaseOptions.map((option, index) => ({ label: option.title, value: index.toString(), })), onChange: (value) => { const previousPurchaseOption = purchaseOptions[selectedPurchaseOption]; selectedPurchaseOption = parseInt(value, 10); updatePriceBreakdownUI(previousPurchaseOption); }, }); if (purchaseOptions.length > 1) { wrapperComponent.insertChildBefore( purchaseOptionSelect, priceBreakdownComponent ); } ``` ##### React ```