> ## Documentation Index
> Fetch the complete documentation index at: https://developers.appstle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build your own subscription frontend

> 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.

<Note>
  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](/subscription/integration-guide) instead — it covers the **Admin API** (`/api/external/v2/...`, authenticated with `X-API-Key`).
</Note>

## 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](/subscription/javascript-hooks) instead of replacing it entirely.

## Architecture overview

Appstle's Storefront APIs are served through [Shopify's App Proxy](https://shopify.dev/docs/apps/online-store/app-proxies). 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.

<Warning>
  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.
</Warning>

## 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.

## 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:

```liquid theme={null}
{% 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:

```liquid theme={null}
<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:

```javascript theme={null}
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}
```

```javascript theme={null}
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
```

<Note>
  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.
</Note>

## Adding a subscription to cart

Adding a subscription item to the Shopify cart uses Shopify's standard [Cart API](https://shopify.dev/docs/api/ajax/reference/cart) — you include the `selling_plan` ID alongside the variant ID.

### Using the Cart AJAX API

```javascript theme={null}
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:

```html theme={null}
<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>
```

<Warning>
  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.
</Warning>

## 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.

```javascript theme={null}
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}
```

```javascript theme={null}
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`.

```javascript theme={null}
// 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:

```javascript theme={null}
// 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`):

```json theme={null}
{
  "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}
```

<Info>
  For the full list of Storefront API endpoints with request/response schemas, see the
  **Storefront API** group in the sidebar.
</Info>

## 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

| Event                                                           | Fires when                                                                         | `event.detail`                                                                  |
| --------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `AppstleSubscription:SubscriptionWidget:widgetInitialised`      | Widget loads on a product page                                                     | Widget ID                                                                       |
| `AppstleSubscription:SubscriptionWidget:SellingPlanSelected`    | Customer selects a subscription option                                             | Widget ID                                                                       |
| `AppstleSubscription:SubscriptionWidget:SellingPlanDeSelected`  | Customer switches away from subscription                                           | Widget ID                                                                       |
| `AppstleSubscription:SubscriptionWidget:sellingPlanChanged`     | Selling plan changes (frequency, etc.)                                             | Selling Plan ID                                                                 |
| `AppstleSubscription:SubscriptionWidget:AddToCartIntent`        | Add-to-cart initiated with a subscription selected (fires before the cart request) | `{ trigger: 'form_submit' \| 'fetch_form_data' \| 'appstle_add_to_cart', ... }` |
| `AppstleSubscription:CartWidget:Updated`                        | Cart widget is updated                                                             | —                                                                               |
| `AppstleSubscription:SubscriptionWidget:SwitchedToSubscription` | Cart item switched from one-time to subscription                                   | Line item key                                                                   |
| `AppstleSubscription:SubscriptionWidget:SwitchedToOneTime`      | Cart item switched from subscription to one-time                                   | Line item key                                                                   |
| `AppstleSubscription:CustomerPortal:ReadyToEmbed`               | Portal ready to render                                                             | —                                                                               |
| `AppstleSubscription:CustomerPortal:Embedded`                   | Portal finished rendering                                                          | —                                                                               |

For the full event reference, see [JavaScript hooks](/subscription/javascript-hooks).

### Listening to events in your custom UI

```javascript theme={null}
// 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:

```javascript theme={null}
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.

```html theme={null}
<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>
```

<Note>
  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.
</Note>

## Further reading

<CardGroup cols={2}>
  <Card title="Webhooks" icon="bell" href="/subscription/webhooks">
    Receive real-time events when subscriptions change.
  </Card>

  <Card title="JavaScript hooks" icon="js" href="/subscription/javascript-hooks">
    Complete list of widget events you can listen to.
  </Card>

  <Card title="Authentication" icon="key" href="/subscription/authentication">
    Admin API keys, partner keys, and storefront auth details.
  </Card>

  <Card title="Integration guide" icon="plug" href="/subscription/integration-guide">
    Server-side Admin API integration for backend developers.
  </Card>
</CardGroup>
