Skip to main content
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).

When to build your own vs. use the widget

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.

Architecture overview

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.
Browser → https://your-shop.myshopify.com/apps/subscriptions/cp/api/...
         ↓ (Shopify App Proxy)
         Appstle backend (with shop + customer identity)
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.

Authentication

The Storefront API does not use API keys. Authentication depends on the context:
ContextHow 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.

Product page: rendering selling plans

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 Liquid

Every Shopify product with subscription selling plans exposes them in the selling_plan_groups array:
{% for group in product.selling_plan_groups %}
  <fieldset>
    <legend>{{ group.name }}</legend>
    {% for plan in group.selling_plans %}
      <label>
        <input
          type="radio"
          name="selling_plan"
          value="{{ plan.id }}"
        />
        {{ plan.name }}
        {% if plan.price_adjustments.size > 0 %}
          — Save {{ plan.price_adjustments[0].value }}%
        {% endif %}
      </label>
    {% endfor %}
  </fieldset>
{% endfor %}
You can also access this data as JSON for use in JavaScript:
<script>
  window.__subscriptionPlans = {{ product.selling_plan_groups | json }};
</script>

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.

Getting selling plan data from the Appstle Storefront API

For richer subscription data (beyond what Shopify’s native objects provide), use the Appstle endpoint:
GET /apps/subscriptions/cp/api/data/products-selling-plans?productIds={id1},{id2}
const productIds = ['8012345678901', '8012345678902'];
const response = await fetch(
  `/apps/subscriptions/cp/api/data/products-selling-plans?productIds=${productIds.join(',')}`
);
const sellingPlans = await response.json();
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.

Adding a subscription to cart

Adding a subscription item to the Shopify cart uses Shopify’s standard Cart API — you include the selling_plan ID alongside the variant ID.

Using the Cart AJAX API

async function addSubscriptionToCart(variantId, sellingPlanId, quantity = 1) {
  const response = await fetch('/cart/add.js', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      items: [{
        id: variantId,
        quantity: quantity,
        selling_plan: sellingPlanId  // This makes it a subscription
      }]
    })
  });

  if (!response.ok) {
    throw new Error(`Failed to add to cart: ${response.status}`);
  }

  return response.json();
}

Using a form

If you prefer a traditional form submission:
<form action="/cart/add" method="post">
  <input type="hidden" name="id" value="{{ variant.id }}" />
  <input type="hidden" name="selling_plan" value="{{ plan.id }}" />
  <input type="hidden" name="quantity" value="1" />
  <button type="submit">Subscribe</button>
</form>
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.

Customer portal: managing subscriptions

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:
https://your-shop.myshopify.com/apps/subscriptions/cp/api/
The customer must have an active Shopify session. Shopify’s App Proxy handles authentication automatically.

Identifying the logged-in customer

GET /apps/subscriptions/cp/api/logged-in-customer
Returns the Shopify customer ID as an integer. Returns 401 if no customer is logged in.
const response = await fetch('/apps/subscriptions/cp/api/logged-in-customer');
if (response.ok) {
  const customerId = await response.json();
  console.log('Logged-in customer:', customerId);
}

Listing a customer’s subscriptions

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.

Viewing upcoming and past orders

Upcoming orders for a contract:
GET /apps/subscriptions/cp/api/subscription-billing-attempts/top-orders?contractId={contractId}
Past orders (paginated):
GET /apps/subscriptions/cp/api/subscription-billing-attempts/past-orders?contractId={contractId}

Updating subscription status

Pause, resume, or cancel a subscription:
PUT /apps/subscriptions/cp/api/subscription-contracts-update-status
    ?contractId={contractId}
    &status={ACTIVE|PAUSED|CANCELLED}
Optional query parameters for pause: pauseReason, pauseFeedback, pauseDurationCycle.
// Pause a subscription
await fetch(
  `/apps/subscriptions/cp/api/subscription-contracts-update-status?contractId=12345&status=PAUSED&pauseReason=Too%20much%20product`,
  { method: 'PUT' }
);
To cancel with feedback:
// Cancel with feedback
await fetch(
  `/apps/subscriptions/cp/api/subscription-contracts/${contractId}?cancellationFeedback=too_expensive&cancellationNote=Budget%20reasons`,
  { method: 'DELETE' }
);

Skipping and unskipping orders

PUT /apps/subscriptions/cp/api/subscription-billing-attempts/skip-order/{billingAttemptId}
    ?subscriptionContractId={contractId}
PUT /apps/subscriptions/cp/api/subscription-billing-attempts/unskip-order/{billingAttemptId}
    ?subscriptionContractId={contractId}

Managing line items

Add a product:
PUT /apps/subscriptions/cp/api/v2/subscription-contracts-add-line-item
    ?contractId={contractId}&variantId={variantId}&quantity={qty}
Remove a product:
PUT /apps/subscriptions/cp/api/subscription-contracts-remove-line-item
    ?contractId={contractId}&lineId={lineId}
Update quantity:
PUT /apps/subscriptions/cp/api/subscription-contracts-update-line-item-quantity
    ?contractId={contractId}&lineId={lineId}&quantity={qty}

Discounts

Apply a discount code:
PUT /apps/subscriptions/cp/api/subscription-contracts-apply-discount
    ?contractId={contractId}&discountCode={code}
Remove a discount:
PUT /apps/subscriptions/cp/api/subscription-contracts-remove-discount
    ?contractId={contractId}&discountId={id}

Updating shipping address

PUT /apps/subscriptions/cp/api/subscription-contracts-update-shipping-address
    ?contractId={contractId}
Request body (application/json):
{
  "address1": "123 Main St",
  "city": "San Francisco",
  "province": "California",
  "country": "United States",
  "zip": "94105"
}

Changing billing date

PUT /apps/subscriptions/cp/api/subscription-contracts-update-billing-date
    ?contractId={contractId}&nextBillingDate=2026-08-15T00:00:00Z

Changing frequency

Switch to a compatible selling plan frequency:
PUT /apps/subscriptions/cp/api/subscription-contracts-update-frequency-by-selling-plan
Or change billing interval directly:
PUT /apps/subscriptions/cp/api/subscription-contracts-update-billing-interval

Portal settings and styling

To load the merchant’s portal configuration (labels, features, display settings):
GET /apps/subscriptions/cp/api/customer-portal-settings/{shopId}
This endpoint does not require customer authentication — it returns the portal’s display configuration. For custom CSS:
GET /apps/subscriptions/cp/api/subscription-custom-csses/{shopId}
For the full list of Storefront API endpoints with request/response schemas, see the Storefront API group in the sidebar.

Using JavaScript widget events

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.

Key events for custom frontends

EventFires whenevent.detail
AppstleSubscription:SubscriptionWidget:widgetInitialisedWidget loads on a product pageWidget ID
AppstleSubscription:SubscriptionWidget:SellingPlanSelectedCustomer selects a subscription optionWidget ID
AppstleSubscription:SubscriptionWidget:SellingPlanDeSelectedCustomer switches away from subscriptionWidget ID
AppstleSubscription:SubscriptionWidget:sellingPlanChangedSelling plan changes (frequency, etc.)Selling Plan ID
AppstleSubscription:SubscriptionWidget:AddToCartIntentAdd-to-cart initiated with a subscription selected (fires before the cart request){ trigger: 'form_submit' | 'fetch_form_data' | 'appstle_add_to_cart', ... }
AppstleSubscription:CartWidget:UpdatedCart widget is updated
AppstleSubscription:SubscriptionWidget:SwitchedToSubscriptionCart item switched from one-time to subscriptionLine item key
AppstleSubscription:SubscriptionWidget:SwitchedToOneTimeCart item switched from subscription to one-timeLine item key
AppstleSubscription:CustomerPortal:ReadyToEmbedPortal ready to render
AppstleSubscription:CustomerPortal:EmbeddedPortal finished rendering
For the full event reference, see JavaScript hooks.

Listening to events in your custom UI

// Sync your custom UI when Appstle's cart logic changes the subscription state
document.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 elements
document.addEventListener('AppstleSubscription:CustomerPortal:Embedded', () => {
  // Portal has rendered — add your custom actions, hide loading state, etc.
  document.querySelector('.portal-loading').style.display = 'none';
});

Widget initialization event

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
});

End-to-end example: custom product page subscription UI

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.

Further reading

Webhooks

Receive real-time events when subscriptions change.

JavaScript hooks

Complete list of widget events you can listen to.

Authentication

Admin API keys, partner keys, and storefront auth details.

Integration guide

Server-side Admin API integration for backend developers.