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

# Sell a membership from your own page

> Add an Appstle membership to the Shopify cart from your own page or theme code, with the plan's selling plan ID and a quantity, then send the customer to checkout.

This guide is for frontend developers who start a membership purchase from their own page: a custom plan picker, a landing page, a team or seat plan with a quantity picker, or a link in an email. You don't call an Appstle API to sell a membership. Shopify's cart and checkout take the order, and Appstle creates the membership once checkout completes.

> [!NOTE]
>
> The cart calls on this page are [Shopify's Ajax Cart API](https://shopify.dev/docs/api/ajax/reference/cart). They run on pages of the store's Online Store theme and need no Appstle API key. The only Appstle call, looking up a plan's IDs, is an **Admin API** call from your server with an API key in the `X-API-Key` header (see [Authentication](https://developers.appstle.com/memberships/authentication.md)). Never put an API key in browser code. A headless storefront builds its cart with Shopify's Storefront GraphQL API instead. There, each cart line takes the selling plan as `sellingPlanId`, in the form `gid://shopify/SellingPlan/690000000001`. See [`cartCreate`](https://shopify.dev/docs/api/storefront/latest/mutations/cartCreate).

## How a membership purchase works

A membership is a Shopify product that the customer buys on a **selling plan**. A cart line becomes a membership only when it carries that plan's selling plan ID. Your page does three things:

1. Look up the variant ID and the selling plan ID.
2. Add the variant to the cart with `selling_plan` and a `quantity`.
3. Send the customer to checkout.

When the customer completes checkout, Shopify creates a subscription contract, and Appstle turns it into a membership for that customer. It usually appears in the [contract list](https://developers.appstle.com/memberships/integration-guide.md#list-and-filter-membership-contracts) within a few seconds, and the [`membership.created` webhook](https://developers.appstle.com/memberships/webhooks) fires.

## Find the variant ID and the selling plan ID

Pick the place that matches where your code runs.

| Where you are | How to get the IDs |
| - | - |
| A page of the store's theme | `GET /products/<handle>.js` returns the variants and their selling plans. See the example below. |
| Your server | `GET /api/external/v2/subscription-groups/{id}` with your Admin API key returns the plan's `subscriptionPlans[].id` and the plan's `variantIds`. |
| The merchant's Appstle admin | Open **Plans & Perks**, then the plan. The **Renewal Cycle** heading shows the selling plan ID in brackets, for example **Renewal Cycle (690000000001)**. |

### From the product's `.js` endpoint

```javascript theme={null}
const product = await (await fetch('/products/coffee-lovers-club.js')).json();
const variant = product.variants[0];
const sellingPlanId = variant.selling_plan_allocations[0].selling_plan_id;
```

The response holds the IDs you need (trimmed):

```json theme={null}
{
  "id": 8100000000001,
  "requires_selling_plan": true,
  "variants": [
    {
      "id": 44000000000001,
      "selling_plan_allocations": [
        {
          "price": 999,
          "per_delivery_price": 999,
          "selling_plan_id": 690000000001,
          "selling_plan_group_id": "4f6c2a9e1b7d3c5a8e0f2b4d6a8c0e1f3a5b7c9d"
        }
      ]
    }
  ]
}
```

A plan with more than one renewal option has one entry in `selling_plan_allocations` for each. The names are in `product.selling_plan_groups[].selling_plans[]`, so match on `id` to let the customer choose. If the product also sells through another app's selling plans, use only the groups whose `app_id` is `appstle-memberships`.

### From the Admin API

Call this from your server only. Take the plan's `id` from the list call `GET /api/external/v2/subscription-groups`, then [get the plan](https://developers.appstle.com/memberships-admin-api/membership-plans/get-a-membership-plan-group.md) by that ID. The list itself can't give you either ID: it leaves `subscriptionPlans` empty and `variantIds` `null`.

```bash theme={null}
curl "https://membership-admin.appstle.com/api/external/v2/subscription-groups/80000000001" \
  -H "X-API-Key: apst_your-api-key-here"
```

```json theme={null}
{
  "id": 80000000001,
  "groupName": "Coffee Lovers Club",
  "variantIds": "[{\"id\":44000000000001,\"title\":\"Coffee Lovers Club - Default Title\"}]",
  "subscriptionPlans": [
    {
      "id": "gid://shopify/SellingPlan/690000000001",
      "frequencyName": "Coffee Lovers Club monthly",
      "frequencyInterval": "MONTH",
      "frequencyCount": 1
    }
  ]
}
```

`variantIds` is a JSON-encoded string, so parse it before you read it. The selling plan ID is the number after `gid://shopify/SellingPlan/`.

## Add the membership to the cart

Post the variant, the selling plan and the quantity to `/cart/add.js`:

```javascript theme={null}
async function addMembershipToCart(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, selling_plan: sellingPlanId }],
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.description || `Cart error ${response.status}`);
  }

  return response.json();
}

await addMembershipToCart(44000000000001, 690000000001, 3);
window.location.href = '/checkout';
```

A successful call returns `200` with one cart line (trimmed). The `selling_plan_allocation` is what makes the line a membership:

```json theme={null}
{
  "items": [
    {
      "id": 44000000000001,
      "quantity": 3,
      "title": "Coffee Lovers Club",
      "price": 999,
      "line_price": 2997,
      "final_line_price": 2997,
      "requires_shipping": false,
      "selling_plan_allocation": {
        "price": 999,
        "per_delivery_price": 999,
        "selling_plan": {
          "id": 690000000001,
          "name": "Coffee Lovers Club monthly",
          "recurring_deliveries": true
        }
      }
    }
  ]
}
```

> [!WARNING]
>
> Always send `selling_plan`. In the Appstle plan editor, merchants are told to make the membership product **subscription only** in Shopify. On a product set that way, a cart add without a selling plan fails with `422`:
>
> ```json theme={null}
> {
>   "status": 422,
>   "message": "Cart Error",
>   "description": "Variant can only be purchased with a selling plan."
> }
> ```
>
> On a product that isn't subscription only, the same call adds a one-time purchase, and the order doesn't create a membership.

### If the cart already has the membership

If the cart already has a line with the same variant and selling plan, `/cart/add.js` adds your quantity to that line. A customer who picks 3 seats, comes back and picks 2 checks out with 5. To send only the seats the customer just picked, clear the cart first, as a Quick Checkout link does. This also removes everything else in the cart:

```javascript theme={null}
await fetch('/cart/clear.js', { method: 'POST' });
await addMembershipToCart(44000000000001, 690000000001, 3);
window.location.href = '/checkout';
```

### Errors

`/cart/add.js` answers a bad request with a JSON body whose `description` says what went wrong. The helper above throws that text:

| What you sent | Status | `description` |
| - | - | - |
| No `selling_plan` (subscription-only product) | `422` | `Variant can only be purchased with a selling plan.` |
| Another product's `selling_plan` | `422` | `Cannot apply selling plan to variant` |
| The Admin API's GID string | `400` | `expected String to be a Integer: selling_plan` |

Send the number after `gid://shopify/SellingPlan/`, not the whole string.

### Quantity

`quantity` works like it does for any cart line. A cart add with `quantity: 3` on a 9.99 plan creates one membership with one line at `quantity: 3`. In the contract that [`GET /api/external/v2/subscription-contract-details`](https://developers.appstle.com/memberships/integration-guide#list-and-filter-membership-contracts) returns, that line's `currentPrice` is the price of one seat (`9.99`) and its `discountedPrice` is the line total (`29.97`). The contract's `orderAmount` and `contractAmount` are both `29.97` (trimmed):

```json theme={null}
{
  "status": "active",
  "customerEmail": "member@example.com",
  "orderName": "#1001",
  "minCycles": 3,
  "orderAmount": 29.97,
  "contractAmount": 29.97,
  "contractDetailsJSON": "[{\"title\":\"Coffee Lovers Club\",\"quantity\":3,\"currentPrice\":\"9.99\",\"discountedPrice\":\"29.97\",\"sellingPlanId\":\"gid://shopify/SellingPlan/690000000001\"}]"
}
```

Appstle creates one membership, owned by the customer who checked out, not one per seat. To give the extra seats meaning, read the line's `quantity` from `contractDetailsJSON` in your own system. If every person on a team needs their own membership, each of them has to buy one.

## Send the customer to checkout

Redirect to `/checkout` once the cart add succeeds, as in the example above.

To start the same purchase from a link, such as a button in an email, build a cart link the way the app's **Quick Checkout** link does. The link clears the cart, adds the item and continues to checkout:

```javascript theme={null}
function membershipLink(variantId, sellingPlanId, quantity = 1) {
  const add = `/cart/add?items[0][id]=${variantId}&items[0][quantity]=${quantity}&items[0][selling_plan]=${sellingPlanId}&return_to=${encodeURIComponent('/checkout')}`;
  return `https://your-shop.myshopify.com/cart/clear?return_to=${encodeURIComponent(add)}`;
}
```

Merchants can create these links without code under **More → Subscriber Acquisition & Automation → Quick Checkout**, in the **Quick Checkout Link** card with its **Copy** button. A Quick Checkout link carries the quantity the merchant chose when they created it. For a quantity the customer picks, build the link with `membershipLink`, use the cart call above, or use the form below.

### Using a form

A plain theme form is the simplest seat picker, because a `quantity` input carries the customer's choice and no JavaScript is needed. The form adds the item and `return_to` sends the customer to checkout:

```html theme={null}
<form action="/cart/add" method="post">
  <input type="hidden" name="id" value="44000000000001">
  <input type="hidden" name="selling_plan" value="690000000001">
  <input type="number" name="quantity" value="1" min="1" max="10">
  <input type="hidden" name="return_to" value="/checkout">
  <button type="submit">Join</button>
</form>
```

Like the cart call, the form adds to a line that is already in the cart, so a returning customer's seats add up.

## The checkout rule that can refuse the cart

Appstle adds a Shopify checkout rule, **Membership Validations**, that can refuse a cart with memberships in it. It refuses carts only when both of these are true:

* The merchant has activated the rule in Shopify admin: under **Settings → Checkout**, **Membership Validations by Appstle Memberships** shows as **Active**.
* The merchant has turned on **Allow membership per customer** in Appstle under **More → General Settings → Shop Settings**, in **Member Settings**.

When both are on, the rule refuses any cart that holds more than one membership in total, whoever the customer is: two lines on membership plans, or one line with a `quantity` above `1`. So a seat picker that sends a `quantity` above `1` works only on a store where **Allow membership per customer** is off.

The setting's four options also refuse customers who already have a membership. If the merchant never picked an option, **One Overall** applies.

| Option | Refuses the purchase when the customer |
| - | - |
| One Overall | has a membership in any status |
| One Active Overall | has an active membership |
| One Per Frequency | has a membership on the same selling plan, in any status |
| One Active Per Frequency | has an active membership on the same selling plan |

When the rule refuses a cart, `/cart/add.js` still returns `200`, because the rule runs when the customer reaches checkout. Checkout then shows the merchant's **Duplicate membership checkout validation message**, or `Duplicate membership purchases are not allowed.` when that field is empty. The customer can't pay until the cart changes.

## Next steps

> [card] [Webhooks](https://developers.appstle.com/memberships/webhooks.md)
>
> React to `membership.created` and the other membership events on your server.

> [card] [Integration guide](https://developers.appstle.com/memberships/integration-guide.md)
>
> Find members and manage memberships with the Admin API.

> [card] [Metafields & tags](https://developers.appstle.com/memberships/metafields-and-tags.md)
>
> Read plan and membership data in Liquid.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.