Use Appstle’s Storefront APIs and JavaScript events to build a fully custom subscription UI — from rendering selling plans on product pages to managing subscriptions in a customer portal.
This guide is for frontend developers who want to use Appstle’s subscription backend but build their own storefront UI instead of using the default widget. It covers the product page (rendering selling plans, adding subscriptions to cart), the customer portal (viewing and managing active subscriptions), and the JavaScript events you can hook into.
This guide covers the Storefront API surface (/cp/api/...), which is designed for customer-facing storefront code.
If you are building a server-side integration, see the Integration guide instead — it covers the Admin API (/api/external/v2/..., authenticated with X-API-Key).
The Appstle subscription widget (appstle-subscription.js) is injected automatically on your storefront and handles selling-plan rendering, cart integration, and the customer portal out of the box. Build your own frontend when you need:
A product page UI that doesn’t match what the widget renders (custom layout, framework-specific components, headless storefronts)
A customer portal embedded in your own account page design rather than the default portal
Programmatic control over subscription flows (e.g. a React/Vue app that manages state itself)
If you only need to react to widget interactions — tracking analytics, showing/hiding elements, applying conditional logic — you can keep the default widget and listen to its JavaScript events instead of replacing it entirely.
Appstle’s Storefront APIs are served through Shopify’s App Proxy. When a request hits your shop’s domain at the proxy path, Shopify forwards it to Appstle’s backend with authentication parameters attached.
The default proxy path is /apps/subscriptions. Merchants can customize this in their Shopify admin, but apps/subscriptions is the standard default.
Storefront API requests must go through the App Proxy on your shop’s domain. You cannot call Appstle’s backend directly from the browser — requests that bypass the proxy will fail authentication.
The Storefront API does not use API keys. Authentication depends on the context:
Context
How it works
Product pages (unauthenticated)
Selling plan data is available via Shopify’s Liquid product.selling_plan_groups object and Shopify’s Product JSON (/products/{handle}.json). No Appstle API call is needed.
Customer portal (authenticated)
The customer must be logged in to your Shopify storefront. When a request goes through the App Proxy, Shopify appends logged_in_customer_id as a query parameter. Appstle uses this to identify the customer. Most /cp/api/ endpoints return 401 if the customer is not logged in.
A few portal endpoints work without a logged-in customer (portal settings, custom CSS, magic link emails). Everything else — viewing contracts, skipping orders, updating addresses — requires the customer to be authenticated through a Shopify session.
On product pages, you typically don’t need the Storefront API at all. Shopify’s native Liquid and Product JSON already include the selling plan data that Appstle configures.
Getting selling plan data from Shopify Product JSON
For headless or JavaScript-driven storefronts, fetch the product JSON directly:
const response = await fetch('/products/your-product-handle.json');const { product } = await response.json();// product.variants[].selling_plan_allocations contains per-variant pricing// The selling_plan_groups are not in the product JSON directly —// use the Storefront API endpoint below for richer data.
There is also a v2 endpoint that accepts optional variantIds for variant-specific data:
GET /apps/subscriptions/cp/api/data/v2/products-selling-plans?productIds={id}&variantIds={vid1},{vid2}
To list all selling plans available in the shop:
GET /apps/subscriptions/cp/api/subscription-groups/all-selling-plans
For product pages visited by unauthenticated customers, prefer Shopify’s native Liquid objects or Product JSON.
The /cp/api/data/ endpoints go through the App Proxy and may require a customer session depending on the shop’s configuration.
Omitting the selling_plan field (or setting it to empty) adds the item as a one-time purchase. If a product has requires_selling_plan: true, the cart add will fail without a valid selling plan ID.
Once a customer is logged in, you can call the Storefront API through the App Proxy to build a custom subscription management UI. All of these endpoints are under:
Get the full subscription profile (active, paused, and cancelled contracts):
GET /apps/subscriptions/cp/api/subscription-customers
Or get just the valid (active/paused) contract IDs:
GET /apps/subscriptions/cp/api/subscription-customers/valid/{customerId}
For detailed contract data:
GET /apps/subscriptions/cp/api/subscription-customers-detail/valid/{customerId}
const customerId = 7654321;const response = await fetch( `/apps/subscriptions/cp/api/subscription-customers-detail/valid/${customerId}`);const contracts = await response.json();// Array of subscription contracts with line items, status, next billing date, etc.
Even when building a custom UI, the Appstle widget script (appstle-subscription.js) is still loaded on your storefront. It dispatches events on both document and window that you can use to coordinate your custom components with Appstle’s cart and portal logic.
// Sync your custom UI when Appstle's cart logic changes the subscription statedocument.addEventListener('AppstleSubscription:SubscriptionWidget:SellingPlanSelected', (event) => { // Update your custom product page to reflect that a subscription is selected document.querySelector('.my-subscribe-badge').classList.add('active');});document.addEventListener('AppstleSubscription:SubscriptionWidget:SellingPlanDeSelected', () => { document.querySelector('.my-subscribe-badge').classList.remove('active');});// Know when the portal is ready so you can inject custom elementsdocument.addEventListener('AppstleSubscription:CustomerPortal:Embedded', () => { // Portal has rendered — add your custom actions, hide loading state, etc. document.querySelector('.portal-loading').style.display = 'none';});
The appstle:subscription-widget:loaded event fires when the widget script has fully initialized. Its detail includes a reference to the widget API:
document.addEventListener('appstle:subscription-widget:loaded', (event) => { const api = event.detail.api; // window.AppstleSubscriptionWidget // Use this to check if the widget is available before interacting with it});
This example shows how to render selling plans, let the customer select one, and add a subscription to cart — all without using the default widget UI.
<div id="subscription-options"></div><button id="subscribe-btn" disabled>Subscribe & Save</button><script>(function() { // 1. Get the product data (injected via Liquid) const product = {{ product | json }}; const container = document.getElementById('subscription-options'); let selectedPlan = null; let selectedVariant = product.variants[0]; // 2. Render selling plan options product.selling_plan_groups.forEach(function(group) { const fieldset = document.createElement('fieldset'); const legend = document.createElement('legend'); legend.textContent = group.name; fieldset.appendChild(legend); group.selling_plans.forEach(function(plan) { const label = document.createElement('label'); label.style.display = 'block'; const radio = document.createElement('input'); radio.type = 'radio'; radio.name = 'selling_plan'; radio.value = plan.id; radio.addEventListener('change', function() { selectedPlan = plan.id; document.getElementById('subscribe-btn').disabled = false; }); label.appendChild(radio); label.appendChild(document.createTextNode(' ' + plan.name)); // Show price adjustment if available if (plan.price_adjustments && plan.price_adjustments.length > 0) { const adj = plan.price_adjustments[0]; if (adj.value_type === 'percentage') { label.appendChild(document.createTextNode(' — Save ' + adj.value + '%')); } } fieldset.appendChild(label); }); container.appendChild(fieldset); }); // 3. Add to cart with the selected selling plan document.getElementById('subscribe-btn').addEventListener('click', async function() { if (!selectedPlan) return; try { const response = await fetch('/cart/add.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [{ id: selectedVariant.id, quantity: 1, selling_plan: selectedPlan }] }) }); if (!response.ok) throw new Error('Cart add failed'); // Redirect to cart or update cart drawer window.location.href = '/cart'; } catch (err) { console.error('Failed to add subscription to cart:', err); } });})();</script>
This example uses Shopify’s native product data and Cart API. It works independently of the Appstle widget.
If the widget is also loaded on the page, it will fire AddToCartIntent and SellingPlanSelected events that your other components can listen to.